Construction Floor Plan
Display a building floor plan with custom pin stamps, hide/show annotations, and a custom toolbar.
/* * Copyright © 2022-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. */@file:Suppress("DEPRECATION")
package com.pspdfkit.catalog.examples.kotlin
import android.content.Contextimport android.net.Uriimport android.os.Bundleimport androidx.appcompat.content.res.AppCompatResourcesimport androidx.core.graphics.drawable.toBitmapimport androidx.lifecycle.lifecycleScopeimport com.pspdfkit.annotations.Annotationimport com.pspdfkit.annotations.AnnotationFlagsimport com.pspdfkit.annotations.AnnotationTypeimport com.pspdfkit.annotations.actions.HideActionimport com.pspdfkit.annotations.configuration.StampAnnotationConfigurationimport com.pspdfkit.annotations.stamps.StampPickerItemimport com.pspdfkit.catalog.Rimport com.pspdfkit.catalog.tasks.ExtractAssetTaskimport com.pspdfkit.compose.toolbar.state.ToolbarIconimport com.pspdfkit.compose.toolbar.state.ToolbarItemimport com.pspdfkit.compose.toolbar.state.ToolbarItemStateimport com.pspdfkit.configuration.activity.PdfActivityConfigurationimport com.pspdfkit.document.PdfDocumentimport com.pspdfkit.jetpack.compose.toolbar.nutrientToolbarCoordinatorimport com.pspdfkit.jetpack.compose.toolbar.setMenuItemGroupingRuleimport com.pspdfkit.ui.PdfActivityimport com.pspdfkit.ui.PdfActivityIntentBuilderimport com.pspdfkit.ui.annotations.OnAnnotatingModeChangeListenerimport com.pspdfkit.ui.annotations.OnAnnotationSelectedListenerimport com.pspdfkit.ui.special_mode.controller.AnnotatingControllerimport com.pspdfkit.ui.special_mode.controller.AnnotationSelectionControllerimport com.pspdfkit.ui.toolbar.grouping.presets.MenuItemimport com.pspdfkit.ui.toolbar.grouping.presets.PresetMenuItemGroupingRuleimport kotlinx.coroutines.launch
class ConstructionExample(context: Context) : AssetExample( context, R.string.constructionExampleTitle, R.string.constructionExampleDescription, ) { override val assetPath: String = "Floor Plan.pdf"
override fun prepareConfiguration(configuration: PdfActivityConfiguration.Builder) { super.prepareConfiguration(configuration) configuration.autosaveEnabled(false) // turn off the display of the note icon for annotations with an attached note configuration.setAnnotationNoteHintingEnabled(false) // Apply a custom theme which overrides the icon for the stamp toolbar button in the annotation creation toolbar configuration.theme(R.style.PSPDFCatalog_Theme_ConstructionExample) // Turn on measurements. This is on by default if you have it in your license. configuration.setMeasurementToolsEnabled(true) }
override fun launchExample(context: Context, configuration: PdfActivityConfiguration.Builder) { prepareConfiguration(configuration)
// Since Nutrient does not directly read documents from the assets, we extract them // to the internal device storage using a custom AsyncTask implementation. ExtractAssetTask.extract(assetPath, title, context) { documentFile -> // Now, as the PDF document file is sitting in the internal device storage, we can // start the ConstructionExampleActivity activity by passing it the Uri of the file. val intent = PdfActivityIntentBuilder .fromUri(context, Uri.fromFile(documentFile)) .activityClass(ConstructionExampleActivity::class.java) .configuration(configuration.build()) .build() context.startActivity(intent) } }}
class ConstructionExampleActivity : PdfActivity(), OnAnnotationSelectedListener, OnAnnotatingModeChangeListener { var annotationsHidden = false val desiredAnnotationTypes = AnnotationType.entries.toMutableSet().apply { // ignore LINK annotations remove(AnnotationType.LINK) }
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
// Register the grouping rule that tells the annotation creation toolbar how to group its // menu items. This uses the deprecated MenuItemGroupingRule bridge so existing // PresetMenuItemGroupingRule subclasses keep working; new code should implement // ContextualToolbarGroupingProvider and register it with // nutrientToolbarCoordinator().setContextualToolbarGroupingProvider(...) instead. nutrientToolbarCoordinator().setMenuItemGroupingRule( this, CustomAnnotationCreationToolbarGroupingRule(this), )
// Customize the main toolbar declaratively via the Compose-toolbar API. This replaces the // legacy onGenerateMenuItemIds / onCreateOptionsMenu / onPrepareOptionsMenu / // onOptionsItemSelected overrides. We prepend our custom show/hide items, remove the // thumbnail-grid default, and toggle each item's visibility based on the hidden state. setToolbarItemsProvider { defaultItems -> val hideItem = ToolbarItem.Button( actionId = "custom_action_hide", id = R.id.custom_action_hide, contentDescription = "Hide", icon = ToolbarIcon.Resource(R.drawable.ic_hide), state = ToolbarItemState(isVisible = !annotationsHidden), overflowBehavior = ToolbarItem.OverflowBehavior.IfRoom, ) val showItem = ToolbarItem.Button( actionId = "custom_action_show", id = R.id.custom_action_show, contentDescription = "Show", icon = ToolbarIcon.Resource(R.drawable.ic_show), state = ToolbarItemState(isVisible = annotationsHidden), overflowBehavior = ToolbarItem.OverflowBehavior.IfRoom, )
// Prepend our two custom items and drop the thumbnail-grid default item. listOf(hideItem, showItem) + defaultItems.filterNot { it.id == MENU_OPTION_THUMBNAIL_GRID } }
// Handle clicks on our custom show/hide toolbar items. setOnToolbarItemClickListener { id -> when (id) { R.id.custom_action_hide, R.id.custom_action_show, -> { // Show/hide annotations accordingly and refresh the toolbar. annotationsHidden = !annotationsHidden setAnnotationVisibility(annotationsHidden) invalidateToolbarItems() true }
else -> false } } }
override fun onDocumentLoaded(document: PdfDocument) { super.onDocumentLoaded(document)
// Check if we have any hidden annotation. If so, we should show the "show" item in the options menu lifecycleScope.launch { val hasHiddenAnnotations = document.annotationProvider.getAllAnnotationsOfType(desiredAnnotationTypes).firstOrNull { it.hasFlag(AnnotationFlags.HIDDEN) } != null
if (hasHiddenAnnotations != annotationsHidden) { annotationsHidden = hasHiddenAnnotations invalidateToolbarItems() } }
setupPinStamp() }
/** * Create a custom configuration for Stamp annotations which creates a Pin annotation. * This can be achieved by setting a single StampPickerItem (which visualizes a pin in our case) to the configuration, * which leads to a behavior change in the annotation creation. Instead of showing a stamp picker dialog, the stamp * annotation is created immediatly from this only item. */ private fun setupPinStamp() { val fragment = requirePdfFragment()
val pinBitmap = AppCompatResources.getDrawable(this, R.drawable.ic_pin_drop)?.toBitmap() ?: return
// First we create our custom StampPickerItem val pinStamp = StampPickerItem .fromBitmap(pinBitmap) .withSize(50f, 50f) .build()
// Then put it into a StampAnnotationConfiguration val stampConfig = StampAnnotationConfiguration .builder(this) // Here we return list of stamp picker items that are going to be available in the stamp picker. .setAvailableStampPickerItems(listOf(pinStamp)) .build()
// Replace the default StampAnnotationConfiguration with our custom one fragment .annotationConfiguration .put(AnnotationType.STAMP, stampConfig)
// When creating such a pin stamp we always want to add a note to it. // To spare the user the extra click for adding a note, we invoke the NoteEditor automatically // Therefore we need two listeners which inform us when an annotation is created and when // the editing mode is invoked (both events happen right one after the other) fragment.addOnAnnotationSelectedListener(this) fragment.addOnAnnotatingModeChangeListener(this) }
// add or remove the HIDDEN flag to the annotations to achieve the hide/show effect private fun setAnnotationVisibility(shouldHide: Boolean) { val doc = document ?: return lifecycleScope.launch { val annotations = doc .annotationProvider .getAllAnnotationsOfType(desiredAnnotationTypes)
pdfFragment?.executeAction(HideAction(annotations, null, shouldHide)) } }
private var newPinAnnotationCreated = false
//region OnAnnotationSelectedListener override fun onPrepareAnnotationSelection( controller: AnnotationSelectionController, annotation: Annotation, annotationCreated: Boolean, ): Boolean { // since our custom pin is the only stamp that we create in this example we can just use the type for detection newPinAnnotationCreated = annotationCreated && annotation.type == AnnotationType.STAMP return true }
override fun onAnnotationSelected(annotation: Annotation, annotationCreated: Boolean) {} //endregion
//region OnAnnotatingModeChangeListener override fun onEnterAnnotatingMode(controller: AnnotatingController) { // in case the last annotation selection was in context with a pin creation, invoke the note editor if (newPinAnnotationCreated && controller.hasCurrentlySelectedAnnotations()) { controller.currentSingleSelectedAnnotation?.let { controller.showAnnotationEditor(it) } } }
override fun onChangeAnnotatingMode(controller: AnnotatingController) {}
override fun onExitAnnotatingMode(controller: AnnotatingController) {} //endregion
/** * Class that implements the [PresetMenuItemGroupingRule], used to tell the toolbar how to * group menu items in the toolbar. * * @see com.pspdfkit.catalog.ui.CustomAnnotationCreationToolbarGroupingRule for more details. */ private class CustomAnnotationCreationToolbarGroupingRule constructor(context: Context) : PresetMenuItemGroupingRule(context) { companion object { private const val LOW_CAPACITY_ITEMS_COUNT = 5 private const val HIGH_CAPACITY_ITEMS_COUNT = 7 }
private val lowCapacityItemsGrouping: MutableList<MenuItem> = ArrayList(LOW_CAPACITY_ITEMS_COUNT) private val highCapacityItemsGrouping: MutableList<MenuItem> = ArrayList(HIGH_CAPACITY_ITEMS_COUNT)
override fun getGroupPreset(capacity: Int, itemsCount: Int): List<MenuItem> = when { capacity >= HIGH_CAPACITY_ITEMS_COUNT -> highCapacityItemsGrouping
capacity >= LOW_CAPACITY_ITEMS_COUNT -> lowCapacityItemsGrouping
// in case we don't have enough capacity, return an empty list else -> ArrayList(capacity) }
override fun areGeneratedGroupItemsSelectable(): Boolean = true
init { lowCapacityItemsGrouping.addAll( listOf( MenuItem( com.pspdfkit.R.id.pspdf__annotation_toolbar_group_drawing, intArrayOf( com.pspdfkit.R.id.pspdf__annotation_toolbar_item_line, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_line_arrow, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_ink_pen, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_magic_ink, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_freetext, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_freetext_callout, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_note, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_measurement_distance, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_measurement_perimeter, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_measurement_area_polygon, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_measurement_area_rect, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_measurement_area_ellipse, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_measurement_scale_calibration, ), ), MenuItem( com.pspdfkit.R.id.pspdf__annotation_toolbar_group_markup, intArrayOf( com.pspdfkit.R.id.pspdf__annotation_toolbar_item_cloudy_square, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_cloudy_circle, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_dashed_square, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_dashed_circle, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_square, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_circle, ), ), MenuItem(com.pspdfkit.R.id.pspdf__annotation_toolbar_item_image), MenuItem(com.pspdfkit.R.id.pspdf__annotation_toolbar_item_stamp), MenuItem(com.pspdfkit.R.id.pspdf__annotation_toolbar_item_picker), MenuItem( com.pspdfkit.R.id.pspdf__annotation_toolbar_group_undo_redo, intArrayOf( com.pspdfkit.R.id.pspdf__annotation_toolbar_item_undo, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_redo, ), ), ), ) highCapacityItemsGrouping.addAll( listOf( MenuItem( com.pspdfkit.R.id.pspdf__annotation_toolbar_group_measurement, intArrayOf( com.pspdfkit.R.id.pspdf__annotation_toolbar_item_measurement_distance, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_measurement_perimeter, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_measurement_area_polygon, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_measurement_area_rect, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_measurement_area_ellipse, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_measurement_scale_calibration, ), ), MenuItem( com.pspdfkit.R.id.pspdf__annotation_toolbar_group_drawing, intArrayOf( com.pspdfkit.R.id.pspdf__annotation_toolbar_item_line, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_line_arrow, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_ink_pen, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_magic_ink, ), ), MenuItem( com.pspdfkit.R.id.pspdf__annotation_toolbar_group_markup, intArrayOf( com.pspdfkit.R.id.pspdf__annotation_toolbar_item_cloudy_square, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_cloudy_circle, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_dashed_square, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_dashed_circle, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_square, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_circle, ), ), MenuItem( com.pspdfkit.R.id.pspdf__annotation_toolbar_group_writing, intArrayOf( com.pspdfkit.R.id.pspdf__annotation_toolbar_item_freetext, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_freetext_callout, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_note, ), ), MenuItem(com.pspdfkit.R.id.pspdf__annotation_toolbar_item_image), MenuItem(com.pspdfkit.R.id.pspdf__annotation_toolbar_item_stamp), MenuItem(com.pspdfkit.R.id.pspdf__annotation_toolbar_item_picker), MenuItem( com.pspdfkit.R.id.pspdf__annotation_toolbar_group_undo_redo, intArrayOf( com.pspdfkit.R.id.pspdf__annotation_toolbar_item_undo, com.pspdfkit.R.id.pspdf__annotation_toolbar_item_redo, ), ), ), ) } }}This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.