ViewPager2 Example
Demonstrates using multiple documents in a Viewpager2
/* * Copyright © 2024-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.content.res.Configurationimport android.net.Uriimport android.os.Bundleimport android.util.SparseArrayimport android.widget.TextViewimport androidx.appcompat.app.AppCompatActivityimport androidx.appcompat.app.AppCompatDelegateimport androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NOimport androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YESimport androidx.core.net.toUriimport androidx.fragment.app.Fragmentimport androidx.fragment.app.FragmentActivityimport androidx.viewpager2.adapter.FragmentStateAdapterimport androidx.viewpager2.widget.ViewPager2import com.google.android.material.tabs.TabLayoutMediatorimport com.pspdfkit.catalog.Rimport com.pspdfkit.catalog.SdkExampleimport com.pspdfkit.catalog.SdkExample.Companion.WELCOME_DOCimport com.pspdfkit.configuration.PdfConfigurationimport com.pspdfkit.configuration.activity.PdfActivityConfigurationimport com.pspdfkit.configuration.page.PageScrollDirectionimport com.pspdfkit.configuration.page.PageScrollModeimport com.pspdfkit.document.PdfDocumentimport com.pspdfkit.listeners.DocumentListenerimport com.pspdfkit.ui.PdfFragmentimport com.pspdfkit.ui.PdfUiFragmentimport com.pspdfkit.ui.PdfUiFragmentBuilderimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.delayimport kotlinx.coroutines.launch
private const val DOCUMENT_LOAD_DELAY_MS = 400L // Delay to ensure document is fully loaded before restoring state
/** * An example demonstrating how to use multiple [PdfFragment]s within a [ViewPager2]. * This example showcases: * - ViewPager2 integration with PDF fragments * - Theme switching (dark/light mode) with state preservation * - Fragment state management across configuration changes * - TabLayout integration for navigation */class ViewPager2Example(context: Context) : SdkExample(context, R.string.viewPager2ExampleTitle, R.string.viewPager2ExampleDescription) { override fun launchExample(context: Context, configuration: PdfActivityConfiguration.Builder) { context.startActivity(Intent(context, ViewPager2Activity::class.java)) }}
/** * Main activity that demonstrates ViewPager2 integration with PDF fragments. * Features: * - Displays multiple PDF documents in a swipeable ViewPager2 * - Supports theme switching between light and dark modes * - Preserves fragment state across theme changes */class ViewPager2Activity : AppCompatActivity() { /** ViewPager2 component for hosting PDF fragments */ private lateinit var pager: ViewPager2
/** Adapter for managing PDF fragments in the ViewPager2 */ private var adapter: PdfFragmentAdapter? = null
/** Coroutine scope for handling asynchronous operations */ private val scope = CoroutineScope(Dispatchers.Main)
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) initialise() }
/** * Initializes the UI components after PDF files have been copied to local storage. * This method sets up the ViewPager2, adapter, TabLayout, and event listeners. */ private fun initialise() { setContentView(R.layout.activity_view_pager)
// Initialize ViewPager2 pager = findViewById(R.id.view_pager)
// Create a new adapter when initializing to ensure it uses the current theme // The callback restores fragment state after document loading adapter = PdfFragmentAdapter(this, true) { position -> scope.launch { // Small delay to ensure document is fully loaded before restoring state delay(DOCUMENT_LOAD_DELAY_MS) restoreState(position) } } pager.adapter = adapter
// Disable user input to prevent manual swiping (navigation via tabs only) pager.isUserInputEnabled = false
// Set current item to the selected page pager.setCurrentItem(selectedPage, false)
// Setup TabLayout TabLayoutMediator(findViewById(R.id.tab_layout), pager) { tab, position -> tab.text = "Tab ${position + 1}" }.attach()
// Setup page change callback to track current page and refresh when idle pager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { super.onPageSelected(position) // Update the globally tracked selected page selectedPage = position }
override fun onPageScrollStateChanged(state: Int) { super.onPageScrollStateChanged(state) // Refresh the current fragment when scrolling stops if (state == ViewPager2.SCROLL_STATE_IDLE) { refresh(selectedPage) } } })
// Setup UI controls (theme toggle button) setupButtons() }
override fun onRestart() { super.onRestart() // Refresh the PDF source when activity restarts (e.g., returning from another app) updateSource(selectedPage) }
/** * Sets up the theme toggle button with appropriate text and click behavior. * The button allows switching between light and dark modes while preserving state. */ private fun setupButtons() { findViewById<TextView>(R.id.night_mode).apply { // Set button text based on current theme val targetMode = if (isDarkModeActive(context = this@ViewPager2Activity)) "Light" else "Dark" val labelText = "Change to $targetMode Mode" text = labelText
setOnClickListener { // Save current fragment state before theme change to preserve user's position/zoom saveState(selectedPage)
// Clean up current fragments to prevent memory leaks clearFragments() adapter = null
// Toggle theme mode - this will recreate the activity AppCompatDelegate.setDefaultNightMode( if (isDarkModeActive(context = this@ViewPager2Activity)) { MODE_NIGHT_NO } else { MODE_NIGHT_YES } ) // Note: Activity will be recreated automatically when night mode changes } } }
companion object { /** Tracks the currently selected page across activity recreations */ var selectedPage = 0 }}
/** * FragmentStateAdapter that provides [PdfFragment] instances for each page in the ViewPager2. * This adapter: * - Creates 4 PDF fragments with different documents * - Applies appropriate theme configuration (dark/light) * - Handles document loading callbacks for state restoration * * @param context The FragmentActivity that hosts this adapter * @param onDocLoaded Callback invoked when a document finishes loading, receives the position */class PdfFragmentAdapter( private val context: FragmentActivity, private val usePdfFragment: Boolean = true, private val onDocLoaded: (position: Int) -> Unit) : FragmentStateAdapter(context) {
/** Returns the total number of pages/fragments */ override fun getItemCount(): Int = FileRepo.size
/** * Creates a new PdfFragment for the specified position. * @param position The position in the ViewPager2 (0-3) * @return A configured PdfFragment with the appropriate PDF document and theme */ override fun createFragment(position: Int): Fragment { PdfUiFragmentWrapper.callback = onDocLoaded val path = FileRepo.getPdfPath(position)
return if (usePdfFragment) { PdfFragment.newInstance( path.toUri(), if (isDarkModeActive(context = context)) themeDark else themeLight ).apply { // Add listener to handle document loading completion addDocumentListener(object : DocumentListener { override fun onDocumentLoaded(document: PdfDocument) { super.onDocumentLoaded(document) // Notify that this document has loaded so state can be restored onDocLoaded.invoke(position) } }) } } else { PdfUiFragmentWrapper.paths[path] = position PdfUiFragmentBuilder.fromUri( context, path.toUri() ) .configuration( PdfActivityConfiguration.Builder(context) .configuration(if (isDarkModeActive(context = context)) themeDark else themeLight) .build() ) .fragmentClass(PdfUiFragmentWrapper::class.java) .build() } }}
class PdfUiFragmentWrapper : PdfUiFragment() { override fun onDocumentLoaded(document: PdfDocument) { super.onDocumentLoaded(document) callback?.let { callback -> val position = extractPositionFromUri(document.documentSource.fileUri) callback(position) } }
private fun extractPositionFromUri(uri: Uri?): Int { return paths[uri!!.path!!] ?: return 0 }
companion object { var callback: ((Int) -> Unit)? = null var paths = HashMap<String, Int>() }}
/** * Helper object that manages the state of PDF fragments across different positions. * This singleton provides functionality to save and retrieve fragment states, * particularly useful for ViewPager implementations where fragments need to persist their state. */object NutrientHelper { /** * Internal storage for fragment bundles indexed by position. * Uses SparseArray for efficient memory usage when dealing with sparse key-value pairs. */ private val bundleList = SparseArray<Bundle>()
/** * Saves the state bundle for a fragment at the specified position. * @param position The position index of the fragment in the ViewPager * @param bundle The Bundle containing the fragment's state data */ fun saveState(position: Int, bundle: Bundle) { bundleList.put(position, bundle) }
/** * Retrieves the saved state bundle for a fragment at the specified position. * @param position The position index of the fragment in the ViewPager * @return The saved Bundle, or null if no state exists for this position */ fun getState(position: Int): Bundle? { return bundleList.get(position) }}
/** * Base configuration for PDF viewer with vertical continuous scrolling. * This configuration is shared between dark and light themes. */private val baseConfig = PdfConfiguration.Builder() .scrollMode(PageScrollMode.CONTINUOUS) .scrollDirection(PageScrollDirection.VERTICAL)
/** * PDF configuration for dark theme with inverted colors. */val themeDark = baseConfig.invertColors(true).build()
/** * PDF configuration for light theme with normal colors. */val themeLight = baseConfig.invertColors(false).build()
/** * Extension function to find a PDF fragment by its position in the ViewPager. * Fragments are tagged with "f{position}" pattern (e.g., "f0", "f1", etc.). * * @param position The position index of the fragment * @return The PdfFragment if found, null otherwise */fun AppCompatActivity.findFragment(position: Int): PdfFragment? = supportFragmentManager.findFragmentByTag("f$position")?.let { when (it) { is PdfFragment -> it is PdfUiFragment -> it.pdfFragment else -> null } }
/** * Extension function to remove all Pdf(Ui) fragments from the activity's FragmentManager. * This is useful for cleanup operations or when resetting the ViewPager state. */fun AppCompatActivity.clearFragments() { val fragmentManager = supportFragmentManager val pdfUiPredicate: (Fragment) -> Boolean = { it is PdfUiFragment } val pdfPredicate: (Fragment) -> Boolean = { it is PdfFragment }
// check if we have any PdfUiFragments, if so, we want to remove those instead of PdfFragments val filterPredicate = if (fragmentManager.fragments.any(pdfUiPredicate)) pdfUiPredicate else pdfPredicate
fragmentManager.fragments.filter(filterPredicate).forEach { fragment -> fragmentManager.beginTransaction().remove(fragment).commitNow() }}
/** * Extension function to save the current state of a PDF fragment. * The state is stored in NutrientHelper for later retrieval. * * @param position The position index of the fragment whose state should be saved */fun AppCompatActivity.saveState(position: Int) { findFragment(position)?.state?.let { NutrientHelper.saveState(position, it) }}
/** * Extension function to update the PDF source for a fragment at the specified position. * This preserves the existing document source and reapplies it to the fragment. * * @param position The position index of the fragment to update */fun AppCompatActivity.updateSource(position: Int) { val pdfFragment = findFragment(position) ?: return pdfFragment.document?.documentSource?.let { pdfFragment.setCustomPdfSource(it) }}
/** * Extension function to restore the saved state for a PDF fragment. * The state is retrieved from NutrientHelper and applied to the fragment. * * @param position The position index of the fragment whose state should be restored */fun AppCompatActivity.restoreState(position: Int) { val pdfFragment = findFragment(position) ?: return NutrientHelper.getState(position)?.let { pdfFragment.state = it }}
/** * Extension function to refresh a PDF fragment. * This invalidates the fragment and its document cache, then refreshes the pages. * Useful for updating the display when the PDF content has changed. * * @param position The position index of the fragment to refresh */fun AppCompatActivity.refresh(position: Int) { findFragment(position)?.apply { invalidate() document?.invalidateCache() refreshPages() }}
/** * Determines whether dark mode is currently active by examining the system UI configuration. * This approach is more reliable than AppCompatDelegate.getDefaultNightMode() because it reflects * the actual applied theme rather than just the configured setting. * * @param context The context used to access system resources and configuration * @return true if dark mode is currently active, false otherwise */fun isDarkModeActive(context: Context): Boolean { return when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { Configuration.UI_MODE_NIGHT_YES -> true else -> false }}
/** * Repository object that manages the list of PDF files used in the ViewPager2 example. * Provides methods to retrieve file names and their corresponding paths. */object FileRepo { private val fileNames = listOf( WELCOME_DOC, "Scientific-paper.pdf", "Teacher.pdf", "The-Cosmic-Context-for-Life.pdf" )
val size get() = fileNames.size
fun getFileName(position: Int) = fileNames[position]
fun getPdfPath(position: Int): String = "file:///android_asset/${getFileName(position)}"}This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.