Persistent Annotation Sidebar
Shows how to show a persistent sidebar containing a list of all annotations.
/* * 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.annotation.SuppressLintimport android.content.Contextimport android.content.Intentimport android.net.Uriimport android.os.Bundleimport android.view.LayoutInflaterimport android.view.Viewimport android.view.ViewGroupimport android.widget.TextViewimport androidx.appcompat.app.AppCompatActivityimport androidx.recyclerview.widget.DividerItemDecorationimport androidx.recyclerview.widget.LinearLayoutManagerimport androidx.recyclerview.widget.RecyclerViewimport com.pspdfkit.annotations.Annotationimport com.pspdfkit.annotations.AnnotationProviderimport com.pspdfkit.annotations.AnnotationTypeimport com.pspdfkit.catalog.Rimport com.pspdfkit.catalog.SdkExampleimport com.pspdfkit.catalog.examples.kotlin.PersistentAnnotationSidebarActivity.Companion.EXTRA_CONFIGURATIONimport com.pspdfkit.catalog.examples.kotlin.PersistentAnnotationSidebarActivity.Companion.EXTRA_URIimport com.pspdfkit.catalog.tasks.ExtractAssetTaskimport com.pspdfkit.configuration.activity.PdfActivityConfigurationimport com.pspdfkit.configuration.sharing.ShareFeaturesimport com.pspdfkit.document.PdfDocumentimport com.pspdfkit.listeners.DocumentListenerimport com.pspdfkit.ui.PdfUiFragmentimport com.pspdfkit.ui.PdfUiFragmentBuilderimport com.pspdfkit.utils.getSupportParcelableimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersimport kotlinx.coroutines.Jobimport kotlinx.coroutines.SupervisorJobimport kotlinx.coroutines.cancelChildrenimport kotlinx.coroutines.launchimport java.util.EnumSet
class PersistentAnnotationSidebarExample(context: Context) : SdkExample(context, R.string.annotationSidebarExampleTitle, R.string.annotationSidebarExampleDescription) { override fun launchExample(context: Context, configuration: PdfActivityConfiguration.Builder) { // We don't need to show it in the outline since we will build our own UI for this. configuration.annotationListEnabled(false)
// We also disable the rest of the outline to make a bit more space in the toolbar. configuration.outlineEnabled(false) configuration.bookmarkListEnabled(false) configuration.documentInfoViewEnabled(false)
// We also hide the share option to make a bit more space. configuration.setEnabledShareFeatures(EnumSet.noneOf(ShareFeatures::class.java)) configuration.printingEnabled(false)
ExtractAssetTask.extract(WELCOME_DOC, title, context) { documentFile -> val intent = Intent(context, PersistentAnnotationSidebarActivity::class.java) intent.putExtra(EXTRA_URI, Uri.fromFile(documentFile)) intent.putExtra(EXTRA_CONFIGURATION, configuration.build()) context.startActivity(intent) } }}
class PersistentAnnotationSidebarActivity : AppCompatActivity() {
/** The adapter we use for our recycler view. */ private val annotationRecyclerAdapter = AnnotationRecyclerAdapter(this)
/** View that is shown in the side bar when no annotations are in the document. */ private lateinit var noAnnotationsView: View
/** The currently displayed PdfUiFragment. */ private lateinit var pdfUiFragment: PdfUiFragment
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // We need to load our layout first. setContentView(R.layout.activity_persistent_sidebar) noAnnotationsView = findViewById(R.id.noAnnotationsView) val recyclerView: RecyclerView = findViewById(R.id.annotationList) recyclerView.adapter = annotationRecyclerAdapter recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) annotationRecyclerAdapter.annotationRecyclerAdapterListener = object : AnnotationRecyclerAdapter.AnnotationRecyclerAdapterListener { override fun onAnnotationClicked(annotation: Annotation) { // When an annotation is clicked we scroll to the page containing it. // And also select the annotation for editing if possible. pdfUiFragment.setPageIndex(annotation.pageIndex, true) pdfUiFragment.pdfFragment?.setSelectedAnnotation(annotation) }
override fun onAnnotationsLoaded(annotations: List<Annotation>) { // We want to show a short description of what's going on if there are no annotations. if (annotations.isEmpty()) { noAnnotationsView.visibility = View.VISIBLE } else { noAnnotationsView.visibility = View.GONE } } }
window.setBackgroundDrawableResource(R.color.primaryLight)
// Finally we can setup our PDF fragment. obtainPdfFragment() }
/** This adds or retrieves the [PdfUiFragment] we use to display the PDF. */ private fun obtainPdfFragment() { // We either grab the existing fragment or add a new one. pdfUiFragment = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as? PdfUiFragment // There is no existing fragment, create a new one. ?: PdfUiFragmentBuilder.fromUri(this, intent.extras!!.getSupportParcelable(EXTRA_URI, Uri::class.java)) .configuration(intent.extras!!.getSupportParcelable(EXTRA_CONFIGURATION, PdfActivityConfiguration::class.java)) .build() .apply { // After creation we actually add it to the fragment manager. supportFragmentManager.beginTransaction().add(R.id.fragmentContainer, this, FRAGMENT_TAG).commit() } }
override fun onStart() { super.onStart() // We need to be notified when the document was loaded. pdfUiFragment.pdfFragment?.addDocumentListener(object : DocumentListener { override fun onDocumentLoaded(document: PdfDocument) { // When the document is loaded clear the previous annotations. annotationRecyclerAdapter.clear()
// We need to set the current document so we can load the annotations. annotationRecyclerAdapter.currentDocument = document
// We need to be aware of any change to the annotations so we can keep our list updated. pdfUiFragment.pdfFragment?.addOnAnnotationUpdatedListener(object : AnnotationProvider.OnAnnotationUpdatedListener { override fun onAnnotationCreated(annotation: Annotation) { annotationRecyclerAdapter.refreshAnnotationsForPage(annotation.pageIndex) }
override fun onAnnotationUpdated(annotation: Annotation) { annotationRecyclerAdapter.refreshAnnotationsForPage(annotation.pageIndex) }
override fun onAnnotationRemoved(annotation: Annotation) { annotationRecyclerAdapter.refreshAnnotationsForPage(annotation.pageIndex) }
override fun onAnnotationZOrderChanged( pageIndex: Int, oldOrder: List<Annotation>, newOrder: List<Annotation> ) { annotationRecyclerAdapter.refreshAnnotationsForPage(pageIndex) } })
// We also need to initialize the list of annotations to begin with. // This is a bit ineffective since we refresh the RecyclerView adapter for each page but this is fine for our small example. for (pageIndex in 0 until document.pageCount) { annotationRecyclerAdapter.refreshAnnotationsForPage(pageIndex) } } }) }
override fun onDestroy() { super.onDestroy() // This will cancel all running operations. annotationRecyclerAdapter.clear() }
companion object { /** Tag we give to our PdfUiFragment. */ const val FRAGMENT_TAG = "PersistentAnnotationSidebarActivity.Fragment" const val EXTRA_URI = "PersistentAnnotationSidebarActivity.DocumentUri" const val EXTRA_CONFIGURATION = "PersistentAnnotationSidebarActivity.PdfConfiguration" }}
class AnnotationRecyclerAdapter(private val context: Context) : RecyclerView.Adapter<AnnotationRecyclerAdapterViewHolder>() {
/** We keep a list of all annotations we display for easy access. */ private val displayedItems = mutableListOf<Annotation>()
/** We keep a list of annotations per page so we can update only single pages easily. */ private val annotationsPerPage = mutableMapOf<Int, List<Annotation>>()
private val adapterScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private val loadingJobs = mutableMapOf<Int, Job>()
var currentDocument: PdfDocument? = null var annotationRecyclerAdapterListener: AnnotationRecyclerAdapterListener? = null
// We only list certain annotation types. private val listedAnnotationTypes = AnnotationType.entries.toMutableSet().apply { // We don't want to clutter the list with widget or link annotations. remove(AnnotationType.WIDGET) remove(AnnotationType.LINK) }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnotationRecyclerAdapterViewHolder { val root = LayoutInflater.from(context).inflate(R.layout.item_annotation, parent, false) return AnnotationRecyclerAdapterViewHolder(root) }
override fun getItemCount(): Int = displayedItems.size
override fun onBindViewHolder(holder: AnnotationRecyclerAdapterViewHolder, position: Int) { val item = displayedItems[position] // In the top text view we display whatever information we can get on the annotation. holder.titleView.text = item.contents ?: item.name ?: item.uuid
// In the bottom text view we display the annotation type. holder.infoView.text = item.type.toString() holder.itemView.setOnClickListener { annotationRecyclerAdapterListener?.onAnnotationClicked(item) } }
/** Removes all currently loaded annotations, and clears the state */ fun clear() { displayedItems.clear() annotationsPerPage.clear() loadingJobs.values.forEach { it.cancel() } loadingJobs.clear() adapterScope.coroutineContext.cancelChildren() currentDocument = null }
/** Reloads the list of annotations for the given page. */ fun refreshAnnotationsForPage(pageIndex: Int) { // If no document is set we don't to anything. val document = currentDocument ?: return
// Cancel any already running loading operation for this page. loadingJobs[pageIndex]?.cancel()
// We grab the annotations for the current page index. // This operates on a background scheduler so we have to explicitly observe it on the main thread. loadingJobs[pageIndex] = adapterScope.launch { val annotations = document.annotationProvider.getAllAnnotationsOfType(listedAnnotationTypes, pageIndex, 1) annotationsPerPage[pageIndex] = annotations refreshDisplayedItems() } }
@SuppressLint("NotifyDataSetChanged") private fun refreshDisplayedItems() { val document = currentDocument ?: return displayedItems.clear() for (pageIndex in 0 until document.pageCount) { // We add all pages we already loaded here. val items = annotationsPerPage[pageIndex] if (items != null) { displayedItems.addAll(items) } }
// We notify the listener so we can update the visibility of our empty view. annotationRecyclerAdapterListener?.onAnnotationsLoaded(displayedItems)
notifyDataSetChanged() }
interface AnnotationRecyclerAdapterListener { fun onAnnotationClicked(annotation: Annotation)
fun onAnnotationsLoaded(annotations: List<Annotation>) }}
class AnnotationRecyclerAdapterViewHolder(root: View) : RecyclerView.ViewHolder(root) { val titleView: TextView = root.findViewById(R.id.annotation_list_item_title) val infoView: TextView = root.findViewById(R.id.annotation_list_item_info)}This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.