Multiple document pager
Shows how multiple pdfs can be utilised in a Compose Horizontal Pager
/* * Copyright © 2020-2026 PSPDFKit GmbH. All rights reserved. * * The PSPDFKit Sample applications are licensed with a modified BSD license. * Please see License for details. This notice may not be removed from this file. */
package com.pspdfkit.catalog.examples.kotlin
import android.content.Contextimport android.content.Intentimport android.net.Uriimport android.os.Bundleimport android.widget.Toastimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport androidx.appcompat.app.AppCompatActivityimport androidx.compose.animation.AnimatedVisibilityimport androidx.compose.animation.expandVerticallyimport androidx.compose.animation.fadeInimport androidx.compose.animation.fadeOutimport androidx.compose.animation.shrinkVerticallyimport androidx.compose.animation.slideInVerticallyimport androidx.compose.animation.slideOutVerticallyimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.PaddingValuesimport androidx.compose.foundation.layout.WindowInsetsimport androidx.compose.foundation.layout.captionBarimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.heightimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.statusBarsPaddingimport androidx.compose.foundation.pager.HorizontalPagerimport androidx.compose.foundation.pager.rememberPagerStateimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.PrimaryTabRowimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Tabimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.collectAsStateimport androidx.compose.runtime.derivedStateOfimport androidx.compose.runtime.getValueimport androidx.compose.runtime.keyimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.rememberCoroutineScopeimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.platform.LocalDensityimport androidx.compose.ui.unit.Densityimport androidx.compose.ui.unit.dpimport androidx.core.net.toUriimport com.pspdfkit.catalog.Rimport com.pspdfkit.catalog.SdkExampleimport com.pspdfkit.catalog.SdkExample.Companion.WELCOME_DOCimport com.pspdfkit.catalog.ui.theming.CatalogThemeimport com.pspdfkit.compose.theme.MainToolbarColorsimport com.pspdfkit.compose.theme.ToolbarPopupColorsimport com.pspdfkit.compose.theme.getUiColorsimport com.pspdfkit.configuration.activity.PdfActivityConfigurationimport com.pspdfkit.configuration.page.PageFitModeimport com.pspdfkit.configuration.page.PageLayoutModeimport com.pspdfkit.configuration.page.PageScrollDirectionimport com.pspdfkit.configuration.page.PageScrollModeimport com.pspdfkit.jetpack.compose.components.MainToolbarimport com.pspdfkit.jetpack.compose.interactors.DefaultListenersimport com.pspdfkit.jetpack.compose.interactors.DocumentStateimport com.pspdfkit.jetpack.compose.interactors.getDefaultDocumentManagerimport com.pspdfkit.jetpack.compose.interactors.rememberDocumentStateimport com.pspdfkit.jetpack.compose.views.DocumentViewimport kotlinx.coroutines.launchimport java.io.Fileimport java.lang.ref.WeakReference
/** * List of PDF files to be displayed in the example */private val FILES = listOf( WELCOME_DOC.removeSuffix(".pdf"), "Scientific-paper", "The-Cosmic-Context-for-Life")
/** * Example entry point showing how to display multiple PDF documents with individual configurations */class DocumentPagerExample(context: Context) : SdkExample(context, R.string.documentPagerExampleTitle, R.string.documentPagerExampleDescription) {
override fun launchExample(context: Context, configuration: PdfActivityConfiguration.Builder) { // Extract the documents from assets before launching the activity FileUtils.copyFilesFromAssetsToLocalStorage(context, FILES) { val intent = Intent(context, DocumentPagerExampleActivity::class.java) context.startActivity(intent) } }}
/** * Activity hosting the multiple PDF viewer Composable */class DocumentPagerExampleActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState)
setContent { CatalogTheme { PdfViewerPager( files = FILES, getFileUri = { filename -> File(filesDir, "$filename.pdf").toUri() } ) } } }}
/** * Manages DocumentState instances for multiple PDFs using WeakReferences to prevent memory leaks */class DocumentStateManager( private val files: List<String>, private val getFileUri: (String) -> Uri) { private val documentStates = mutableMapOf<Int, WeakReference<DocumentState>>()
/** * Returns an existing DocumentState or creates a new one if needed * * @param index The index of the PDF in the files list * @return A DocumentState for the PDF at the given index */ @Composable fun getOrCreateDocumentState(index: Int): DocumentState { val weakRef = documentStates[index] val existingState = weakRef?.get()
// Return existing state if available if (existingState != null) { return existingState }
// Create a new state with different configuration based on document index val context = LocalContext.current val commonConfig = PdfActivityConfiguration.Builder(context) .defaultToolbarEnabled(false).fitMode(PageFitMode.FIT_TO_WIDTH) val uri = getFileUri(files[index]) val configuration = createConfigurationForIndex(index, commonConfig) val newState = rememberDocumentState(uri, configuration)
// Store weak reference and return the new state documentStates[index] = WeakReference(newState) return newState }
/** * Creates a specific configuration based on the document index */ private fun createConfigurationForIndex(index: Int, commonConfig: PdfActivityConfiguration.Builder) = when (index) { 0 -> commonConfig.scrollMode(PageScrollMode.PER_PAGE).build() 1 -> commonConfig.scrollDirection(PageScrollDirection.VERTICAL).build() else -> commonConfig.layoutMode(PageLayoutMode.DOUBLE).build() }}
/** * Main Composable that displays multiple PDFs with tabs for navigation * * @param files List of PDF filenames to display * @param getFileUri Function to convert a filename to a URI */@Composablefun PdfViewerPager( files: List<String>, getFileUri: (String) -> Uri) { val localDensity = LocalDensity.current val documentStateManager = remember(files) { DocumentStateManager(files, getFileUri) }
val pagerState = rememberPagerState(pageCount = { files.size }) val coroutineScope = rememberCoroutineScope() var hideTopBar by remember { mutableStateOf(true) }
Column(modifier = Modifier.fillMaxSize()) { Scaffold( topBar = { AnimatedVisibility( visible = hideTopBar, enter = slideInVertically { with(localDensity) { -40.dp.roundToPx() } } + expandVertically(expandFrom = Alignment.Top) + fadeIn(initialAlpha = 0.3f), exit = slideOutVertically() + shrinkVertically() + fadeOut() ) { // Tab row for document selection PdfTabRow( files = files, currentPage = pagerState.currentPage, onTabSelected = { index -> coroutineScope.launch { pagerState.animateScrollToPage(index) } } ) } } ) { paddingValues -> // Horizontal pager for PDF documents HorizontalPager( userScrollEnabled = false, state = pagerState, modifier = Modifier.weight(1f) ) { page -> key(page) { PdfDocumentPage( page = page, documentStateManager = documentStateManager, paddingValues = paddingValues, localDensity = localDensity ) { hideTopBar = it } } } } }}
/** * Composable for the tab row to switch between documents */@OptIn(ExperimentalMaterial3Api::class)@Composableprivate fun PdfTabRow( files: List<String>, currentPage: Int, onTabSelected: (Int) -> Unit) { PrimaryTabRow( selectedTabIndex = currentPage, modifier = Modifier .fillMaxWidth().background(MaterialTheme.colorScheme.background) .statusBarsPadding() ) { files.forEachIndexed { index, title -> Tab( selected = currentPage == index, onClick = { onTabSelected(index) }, text = { Column( Modifier .padding(4.dp) .fillMaxWidth(), verticalArrangement = Arrangement.SpaceBetween ) { Text( text = title, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.align(Alignment.CenterHorizontally) ) } } ) } }}
/** * Composable for an individual PDF document page */@Composableprivate fun PdfDocumentPage( page: Int, documentStateManager: DocumentStateManager, paddingValues: PaddingValues, localDensity: Density, updateTopBarVisibility: (Boolean) -> Unit) { val context = LocalContext.current val documentState = documentStateManager.getOrCreateDocumentState(page)
// State for toolbar visibility and height var toolbarVisibility by remember { mutableStateOf(true) } var toolbarHeight by remember { mutableStateOf(0.dp) }
// Calculate spacer height based on view overlap state val enableViewSpacer by documentState.viewWithOverlappingToolbarShown.collectAsState() val viewSpacerHeight by remember { derivedStateOf { if (enableViewSpacer && toolbarVisibility) toolbarHeight else 0.dp } }
Box( contentAlignment = Alignment.TopCenter ) { Column { // Spacer to prevent content from being covered by toolbar Box(modifier = Modifier.height(viewSpacerHeight).padding(top = paddingValues.calculateTopPadding()))
// PDF Document View DocumentView( documentState = documentState, modifier = Modifier.weight(1f), documentManager = createDocumentManager(context) { toolbarVisibility = !it updateTopBarVisibility.invoke(!it) } ) }
// Animated toolbar AnimatedVisibility( visible = toolbarVisibility, enter = slideInVertically { with(localDensity) { -40.dp.roundToPx() } } + expandVertically(expandFrom = Alignment.Top) + fadeIn(initialAlpha = 0.3f), exit = slideOutVertically() + shrinkVertically() + fadeOut() ) { // Common toolbar for all pages MainToolbar( modifier = Modifier.padding(top = paddingValues.calculateTopPadding()), documentState = documentState, windowInsets = WindowInsets.captionBar, colorScheme = createCustomUiColors(), onHeightChanged = { height -> toolbarHeight = with(localDensity) { height.toDp() } } ) } }}
/** * Creates a document manager with listeners for document events */@Composableprivate fun createDocumentManager( context: Context, onImmersiveModeChanged: (Boolean) -> Unit) = getDefaultDocumentManager( documentListener = DefaultListeners.documentListeners( onDocumentLoaded = { Toast.makeText(context, "Document loaded", Toast.LENGTH_SHORT).show() } ), annotationListener = DefaultListeners.annotationListeners( onAnnotationSelected = { annotation, _ -> Toast.makeText( context, "${annotation.type} selected", Toast.LENGTH_SHORT ).show() }, onAnnotationDeselected = { annotation, _ -> Toast.makeText( context, "${annotation.type} deselected", Toast.LENGTH_SHORT ).show() } ), uiListener = DefaultListeners.uiListeners( onImmersiveModeEnabled = onImmersiveModeChanged ))
/** * Creates custom UI colors for the toolbar */@Composableprivate fun createCustomUiColors() = getUiColors().copy( mainToolbar = MainToolbarColors( backgroundColor = MaterialTheme.colorScheme.primary, textColor = MaterialTheme.colorScheme.onPrimary, popup = ToolbarPopupColors(backgroundColor = MaterialTheme.colorScheme.primary), titleTextColor = MaterialTheme.colorScheme.onPrimary ))
/** * Utility object for file operations */object FileUtils { /** * Copies files from assets to local storage * * @param context Android context * @param fileNames List of filenames to copy * @param onComplete Callback to run after copying completes */ fun copyFilesFromAssetsToLocalStorage( context: Context, fileNames: List<String>, onComplete: () -> Unit = {} ) { fileNames.forEach { fileName -> val destinationFile = File(context.filesDir, "$fileName.pdf")
if (!destinationFile.exists()) { try { context.assets.open("$fileName.pdf").use { inputStream -> destinationFile.outputStream().use { outputStream -> inputStream.copyTo(outputStream, BUFFER_SIZE) } } } catch (e: Exception) { e.printStackTrace() } } }
onComplete() }
private const val BUFFER_SIZE = 8192}This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.