Main Toolbar: Floating Pill (M3 Expressive)
Material 3 Expressive floating toolbar: drag the pill to dock it at any window edge, horizontally or vertically — or switch it into the collapsible variant that shrinks to its active item after a tap.
/* * Copyright © 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.Buildimport android.os.Bundleimport androidx.activity.compose.setContentimport androidx.appcompat.app.AppCompatActivityimport androidx.compose.animation.AnimatedVisibilityimport androidx.compose.animation.fadeInimport androidx.compose.animation.fadeOutimport androidx.compose.animation.scaleInimport androidx.compose.animation.scaleOutimport androidx.compose.foundation.isSystemInDarkThemeimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.navigationBarsPaddingimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.layout.statusBarsPaddingimport androidx.compose.material3.ColorSchemeimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.material3.Switchimport androidx.compose.material3.Textimport androidx.compose.material3.darkColorSchemeimport androidx.compose.material3.dynamicDarkColorSchemeimport androidx.compose.material3.dynamicLightColorSchemeimport androidx.compose.material3.lightColorSchemeimport androidx.compose.runtime.Composableimport androidx.compose.runtime.collectAsStateimport androidx.compose.runtime.getValueimport androidx.compose.runtime.keyimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.saveable.rememberSaveableimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.platform.LocalContextimport androidx.compose.ui.res.stringResourceimport androidx.compose.ui.unit.dpimport com.pspdfkit.catalog.Rimport com.pspdfkit.catalog.SdkExampleimport com.pspdfkit.catalog.tasks.ExtractAssetTaskimport com.pspdfkit.catalog.ui.theming.enableConfigurationEdgeToEdgeimport com.pspdfkit.compose.theme.NutrientThemeimport com.pspdfkit.compose.toolbar.composables.FloatingToolbarEdgeimport com.pspdfkit.compose.toolbar.composables.NutrientFloatingToolbarimport com.pspdfkit.compose.toolbar.composables.rememberFloatingToolbarStateimport com.pspdfkit.configuration.activity.PdfActivityConfigurationimport com.pspdfkit.configuration.search.SearchTypeimport com.pspdfkit.jetpack.compose.interactors.rememberDocumentStateimport com.pspdfkit.jetpack.compose.views.DocumentViewimport com.pspdfkit.utils.getSupportParcelableExtra
/** * Renders the main-toolbar model as a **Material 3 Expressive floating toolbar** — a draggable * pill built with [NutrientFloatingToolbar] on top of the document's * [com.pspdfkit.compose.toolbar.adapter.ToolbarCoordinator]: * * - Drag the pill by its handle and release: it springs to the nearest window edge, flipping * between horizontal (top/bottom) and vertical (start/end) layouts. * - All motion (show/hide, orientation flip, drag release, press feedback) runs on the Material 3 * expressive motion scheme's spring tokens, built into [NutrientFloatingToolbar]. * - Items and clicks flow through the same coordinator APIs as the regular main toolbar. * - The **Collapsible** toggle — a labeled Material 3 [Switch] — switches the pill into * its collapsible variant: tapping any item in the expanded menu runs it and collapses the pill * down to just that item — the *active* item stays visible as a single floating button, and * tapping it springs the menu back open (nothing is dispatched). The pill starts docked at the * trailing edge, showing off the vertical layout right away. The toggle rides the top edge * whenever that edge is free — with the pill docked at the bottom or vertically at either * side — and drops to the bottom, clear of the floating thumbnail bar, once the pill or the * inline-search pill takes the top edge. It animates away entirely while a contextual toolbar * (annotation creation, content editing, …) is up. */class MainToolbarFloatingExample(context: Context) : SdkExample(context, R.string.mainToolbarFloatingExampleTitle, R.string.mainToolbarFloatingExampleDescription) { override fun launchExample(context: Context, configuration: PdfActivityConfiguration.Builder) { ExtractAssetTask.extract(WELCOME_DOC, title, context) { documentFile -> val intent = Intent(context, MainToolbarFloatingActivity::class.java) intent.putExtra(MainToolbarFloatingActivity.EXTRA_URI, Uri.fromFile(documentFile)) context.startActivity(intent) } }}
class MainToolbarFloatingActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
val uri = intent.getSupportParcelableExtra(EXTRA_URI, Uri::class.java)!! val configuration = PdfActivityConfiguration.Builder(this) // The floating toolbar replaces the SDK's edge-to-edge top toolbar entirely. .defaultToolbarEnabled(false) .documentTitleOverlayEnabled(false) // Inline search renders as an expressive search pill at the top edge while active. .setSearchType(SearchType.INLINE) .build()
enableConfigurationEdgeToEdge(configuration) setContent { // The floating toolbar reads its container/button colors from the ambient Material 3 // color scheme; its expressive spring motion is built in. MaterialTheme(colorScheme = expressiveColorScheme()) { FloatingToolbarDocument(uri, configuration) } } }
companion object { const val EXTRA_URI = "MainToolbarFloatingActivity.DocumentUri" }}
/** Dynamic (Material You) color when available (API 31+), falling back to the baseline schemes. */@Composableprivate fun expressiveColorScheme(): ColorScheme { val dark = isSystemInDarkTheme() return when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (dark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) }
dark -> darkColorScheme()
else -> lightColorScheme() }}
/** * Vertical room the toggle leaves above the bottom window edge so it clears the SDK's floating * thumbnail bar: the bar's own height (`pspdf__thumbnail_height`, 28dp, inside * `pspdf__thumbnail_bar_content_padding` of 6dp top and bottom), plus the 36dp * `pspdf__floating_thumbnail_bar_margin_bottom` it keeps from the window edge, plus an 8dp gap so * the two pieces of chrome don't touch. */private val THUMBNAIL_BAR_CLEARANCE = 28.dp + (6.dp * 2) + 36.dp + 8.dp
@Composableprivate fun FloatingToolbarDocument(uri: Uri, configuration: PdfActivityConfiguration) { val documentState = rememberDocumentState(uri, configuration)
// The DocumentView publishes its fragment-scoped toolbar coordinator here once the UI is ready. val coordinator by documentState.toolbarCoordinator.collectAsState() var collapsible by rememberSaveable { mutableStateOf(false) }
Box(modifier = Modifier.fillMaxSize()) { DocumentView( documentState = documentState, modifier = Modifier.fillMaxSize(), )
coordinator?.let { c -> NutrientTheme { // Hoisted out of the `key` below on purpose: the pill's docked edge survives the // variant flip (a drag to the bottom edge stays put when the user toggles), and // the toggle's own placement reads `edge` to stay out from under the pill. It // starts docked at the trailing edge, showing the vertical layout right away. val toolbarState = rememberFloatingToolbarState(initialEdge = FloatingToolbarEdge.End) // Re-seed the pill's *internal* state (expanded/collapsed, active item) when the // variant flips, so each variant starts from a clean pill. key(collapsible) { NutrientFloatingToolbar( coordinator = c, state = toolbarState, vibrant = true, draggable = true, collapsible = collapsible, ) }
// Deliberately outside the `key`: the toggle drives `collapsible`, so re-keying it // on its own tap would rebuild the Switch mid-gesture and snap the thumb instead // of animating it. // // Keep the toggle out from under the toolbar. The pill docks centered on an // edge, so the toggle rides the top edge for every dock except the top one: // docked at the bottom or vertically at either side, the top edge is free. // The top edge is taken while the pill docks there, and also while inline // search is active — the pill hides and a full-width search pill takes its // place — so the toggle drops to the bottom, lifted clear of the floating // thumbnail bar that lives there. val searchActive = c.searchSession.collectAsState().value?.isActive?.collectAsState()?.value == true val topEdgeTaken = searchActive || toolbarState.edge == FloatingToolbarEdge.Top
// A contextual toolbar (annotation creation, content editing, …) owns the // screen while it is up, so the toggle springs away until it is dismissed. val contextualToolbar by c.contextualToolbar.collectAsState() // The pill itself honors `frame.isVisible`, so the toggle that drives it has to // as well: tapping the document into immersive mode takes the pill away, and a // switch for a toolbar that is not on screen would be left floating alone. val frame by c.frame.collectAsState() AnimatedVisibility( visible = contextualToolbar == null && frame.isVisible, enter = fadeIn() + scaleIn(), exit = fadeOut() + scaleOut(), modifier = Modifier .align(if (topEdgeTaken) Alignment.BottomCenter else Alignment.TopCenter) .statusBarsPadding() .navigationBarsPadding() .padding( top = 8.dp, bottom = if (topEdgeTaken) THUMBNAIL_BAR_CLEARANCE else 8.dp, ), ) { Surface( onClick = { collapsible = !collapsible }, shape = MaterialTheme.shapes.extraLarge, color = MaterialTheme.colorScheme.surfaceContainer, shadowElevation = 4.dp, ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp), ) { Text( text = stringResource(R.string.mainToolbarFloatingCollapsibleToggle), style = MaterialTheme.typography.labelLarge, ) Switch(checked = collapsible, onCheckedChange = { collapsible = it }) } } } } } }}This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.