Filterable Thumbnail Grid
Shows how to combine PDF processor and customizable nature of the thumbnail grid to create filters.
/* * 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.net.Uriimport android.os.Bundleimport android.view.Viewimport android.widget.RelativeLayoutimport android.widget.Toastimport androidx.core.view.ViewCompatimport androidx.lifecycle.ViewModelimport androidx.lifecycle.ViewModelProviderimport com.pspdfkit.annotations.AnnotationTypeimport com.pspdfkit.bookmarks.Bookmarkimport com.pspdfkit.catalog.Rimport com.pspdfkit.catalog.SdkExampleimport com.pspdfkit.catalog.ui.FilterPickerViewimport com.pspdfkit.catalog.ui.FilterPickerView.Filterimport com.pspdfkit.configuration.activity.PdfActivityConfigurationimport com.pspdfkit.document.DocumentSourceimport com.pspdfkit.document.PdfDocumentimport com.pspdfkit.document.processor.PdfProcessorimport com.pspdfkit.document.processor.PdfProcessorTaskimport com.pspdfkit.document.providers.AssetDataProviderimport com.pspdfkit.listeners.OnVisibilityChangedListenerimport com.pspdfkit.ui.DocumentDescriptorimport com.pspdfkit.ui.PdfActivityimport com.pspdfkit.ui.PdfActivityIntentBuilderimport com.pspdfkit.ui.PdfThumbnailGridimport io.reactivex.rxjava3.android.schedulers.AndroidSchedulersimport io.reactivex.rxjava3.core.SingleObserverimport io.reactivex.rxjava3.disposables.Disposableimport io.reactivex.rxjava3.schedulers.Schedulersimport io.reactivex.rxjava3.subscribers.DefaultSubscriberimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.launchimport java.io.Fileimport java.io.IOExceptionimport java.util.EnumSet
/** * Example on how to display filters in the thumbnail grid. Shows how to combine PDF processor and * customizability of the thumbnail grid to create filters. */class FilterableThumbnailGridExample(context: Context) : SdkExample( context, R.string.filterableThumbnailGridExampleTitle, R.string.filterableThumbnailGridExampleDescription, ) { override fun launchExample(context: Context, configuration: PdfActivityConfiguration.Builder) { val intent = PdfActivityIntentBuilder .fromDataProvider(context, AssetDataProvider(WELCOME_DOC)) .configuration(configuration.build()) .activityClass(FilterableThumbnailGridActivity::class.java) .build()
context.startActivity(intent) }}
/** [ViewModel] used for storing the pdf document in the [FilterableThumbnailGridExample]. */class FilterableThumbnailGridViewModel : ViewModel() { var pdfDocument: PdfDocument? = null}
/** * An example activity that demonstrates how to take the [PdfThumbnailGrid], add a custom view * to it, and connect it with the [PdfProcessor] and [com.pspdfkit.ui.DocumentCoordinator] to create an * example where we can filter pages. This example incorporates the filter with choices to show all * pages, just annotated pages, or just bookmarked pages. * * Each filtering process first retrieves the pages that need to be in the document, and then * uses the [PdfProcessor] to keep those pages. Finally it reloads the document. */class FilterableThumbnailGridActivity : PdfActivity(), FilterPickerView.OnFilterClickedListener { /** A view with selectable filters that we will add to the thumbnail grid layout. */ private var filterPickerView: FilterPickerView? = null
/** * In this example, we use this [androidx.lifecycle.ViewModel] to store the original * (unfiltered) document across configurations changes and reloads. View Model is part of the * new Android architecture components and is currently the way to go when it comes to restoring * the state across configuration changes (among other things). * * @see [ViewModel - Developers Guide](https://developer.android.com/topic/libraries/architecture/viewmodel) */ private lateinit var viewModel: FilterableThumbnailGridViewModel
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
// View model provider is used to instantiate the view model. If this activity is // recreated, it will receive the instance of the same view model. viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()) .get(FilterableThumbnailGridViewModel::class.java)
// Try to restore the last selected filter, otherwise use the default one. @Suppress("DEPRECATION") val restoredFilter = savedInstanceState?.getSerializable(ARG_LAST_SELECTED_FILTER) as? Filter val selectedFilter = restoredFilter ?: Filter.ALL
// Get the thumbnail grid view. val thumbnailGridView = pspdfKitViews.thumbnailGridView if (thumbnailGridView != null) { // We don't inflate thumbnail grid view until it's displayed. // Check if displayed before generating view. If not, wait for it to show. if (thumbnailGridView.isDisplayed) { generateFilterView(thumbnailGridView, selectedFilter) } else { pspdfKitViews.addOnVisibilityChangedListener( object : OnVisibilityChangedListener { override fun onShow(view: View) { if (view == thumbnailGridView) { generateFilterView(thumbnailGridView, selectedFilter) } }
override fun onHide(view: View) { // No action needed when hiding the thumbnail bar. } }, ) } } }
override fun onDocumentLoaded(document: PdfDocument) { super.onDocumentLoaded(document)
// We save the original document in the view model when first loaded. if (viewModel.pdfDocument == null) { viewModel.pdfDocument = document } }
override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState)
// Save last selected filter. filterPickerView?.let { picker -> outState.putSerializable(ARG_LAST_SELECTED_FILTER, picker.selectedFilter) } }
/** * Adds a filter picker to the provided thumbnail grid view. * * @param thumbnailGridView The thumbnail grid view in which to add the [FilterPickerView]. * @param selectedFilter The default filter that will be selected. */ private fun generateFilterView(thumbnailGridView: PdfThumbnailGrid, selectedFilter: Filter) { // Generate only once. if (filterPickerView?.parent != null) return
// Initialize filter picker view. val picker = FilterPickerView(this@FilterableThumbnailGridActivity) filterPickerView = picker
// Pick filters that will be displayed. picker.setFilters(listOf(Filter.ALL, Filter.ANNOTATED, Filter.BOOKMARKED))
// Set which filter to select. picker.setSelectedFilter(selectedFilter)
// Attach listener to get the filter picker clicks. picker.setOnFilterClickedListener(this@FilterableThumbnailGridActivity)
// Set elevation on the picker bar. ViewCompat.setElevation( picker, resources.getDimension(R.dimen.pspdf__thumbnailGridFilterPickerViewElevation), )
// Add view to the thumbnail grid (which is a RelativeLayout). thumbnailGridView.addView( picker, RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT, )
// Get the recycle view and position it below the newly added view. val recyclerView = thumbnailGridView.findViewById<View>(com.pspdfkit.R.id.pspdf__thumbnail_grid_recycler_view) if (recyclerView != null) { val params = recyclerView.layoutParams as RelativeLayout.LayoutParams params.addRule(RelativeLayout.BELOW, R.id.filter_view) recyclerView.layoutParams = params } }
override fun onFilterClicked(filter: Filter) { when (filter) { Filter.ALL -> { filterPickerView?.setSelectedFilter(Filter.ALL) loadOriginalDocument() }
Filter.ANNOTATED -> { // Get pages that contain annotations and filter them out in the document. val originalDocument = viewModel.pdfDocument ?: return
CoroutineScope(Dispatchers.Main).launch { // Get all annotations and extract unique page indices. val annotations = originalDocument.annotationProvider.getAllAnnotationsOfType( EnumSet.allOf(AnnotationType::class.java), ) val pageIndices = annotations.map { it.pageIndex }.distinct()
if (pageIndices.isEmpty()) { // If there are no annotations, we don't have any document to switch to. Toast .makeText( this@FilterableThumbnailGridActivity, "There are no annotated pages in the document.", Toast.LENGTH_SHORT, ).show() return@launch }
// Refresh selection. filterPickerView?.setSelectedFilter(Filter.ANNOTATED)
// Filter the original document to contain just the pages // with annotations, and load it. loadFilteredDocument(originalDocument, pageIndices.toHashSet(), "annotated") } }
Filter.BOOKMARKED -> { // Get pages that contain bookmarks and filter them out in the document. val originalDocument = viewModel.pdfDocument ?: return // Get the provider for fetching the bookmarks. originalDocument .bookmarkProvider // Get all bookmarks. .bookmarksAsync // Split the list and emit elements separately. .flatMapIterable { bookmarks: List<Bookmark> -> bookmarks } // Filter out bookmarks without page index and map to page index. .filter { bookmark: Bookmark -> bookmark.pageIndex != null } // Map bookmarks to the page index they have (that's the only thing we need // for filtering). .map { bookmark: Bookmark -> bookmark.pageIndex as Int } // Avoid duplicates. .distinct() // Convert to list. .toList() .observeOn(AndroidSchedulers.mainThread()) .subscribe( object : SingleObserver<List<Int>> { override fun onSubscribe(d: Disposable) { // Do something once the filtering process starts. Not // implemented in this example. }
override fun onSuccess(integers: List<Int>) { if (integers.isEmpty()) { // If there are no bookmarked pages, we don't have any document to switch to. Toast .makeText( this@FilterableThumbnailGridActivity, "There are no bookmarked pages in the document.", Toast.LENGTH_SHORT, ).show() return }
// Refresh selection. filterPickerView?.setSelectedFilter(Filter.BOOKMARKED)
// Filter the original document to contain just the // bookmarked pages, and load it. loadFilteredDocument(originalDocument, integers.toHashSet(), "bookmarked") }
override fun onError(e: Throwable) { // Handle filtering error here. } }, ) } } }
/** Loads the original document. */ private fun loadOriginalDocument() { val originalDoc = viewModel.pdfDocument ?: return val coordinator = documentCoordinator val originalDocumentDescriptor = DocumentDescriptor.fromDocument(originalDoc) coordinator.addOnDocumentVisibleListener { pspdfKitViews.thumbnailGridView?.show() } coordinator.setDocument(originalDocumentDescriptor) }
/** * Loads the filtered document. * * @param originalDocument Original document on which to perform filtering. * @param pages Pages that will be taken from the original document. * @param fileSuffix Suffix to add to the filename when creating the filtered document. */ private fun loadFilteredDocument(originalDocument: PdfDocument, pages: Set<Int>, fileSuffix: String) { val pdfProcessorTask = PdfProcessorTask.fromDocument(originalDocument).keepPages(pages) val newDocumentFile = File(filesDir, "${originalDocument.title}-$fileSuffix") PdfProcessor .processDocumentAsync(pdfProcessorTask, newDocumentFile) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( object : DefaultSubscriber<PdfProcessor.ProcessorProgress>() { override fun onNext(processorProgress: PdfProcessor.ProcessorProgress) { // Consume processing progress here. Could be used to show the // filtering progress in some UI. }
override fun onError(t: Throwable) { // Handle processing error here. Not handled in this particular // example. }
override fun onComplete() { try { val documentSource = DocumentSource(Uri.fromFile(newDocumentFile.canonicalFile)) val documentDescriptor = DocumentDescriptor.fromDocumentSource(documentSource) val coordinator = documentCoordinator coordinator.addOnDocumentVisibleListener { pspdfKitViews.thumbnailGridView?.show() } coordinator.setDocument(documentDescriptor) } catch (e: IOException) { e.printStackTrace() } } }, ) }
companion object { /** Argument used for storing last selected filter in the [FilterPickerView]. */ private const val ARG_LAST_SELECTED_FILTER = "FilterableThumbnailGridActivity.LAST_SELECTED_FILTER" }}This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.