ui:material3: AdaptiveOpenSourceDependenciesScreen supports predictive backhandler
--- a/ui/material3/src/commonMain/kotlin/com/geekorum/aboutoss/ui/material3/AdaptiveOpenSourceDependenciesScreen.kt Tue May 06 13:53:02 2025 -0400
+++ b/ui/material3/src/commonMain/kotlin/com/geekorum/aboutoss/ui/material3/AdaptiveOpenSourceDependenciesScreen.kt Tue May 06 14:34:26 2025 -0400
@@ -59,6 +59,7 @@
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue
+import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.material3.rememberTopAppBarState
@@ -73,7 +74,6 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
-import androidx.compose.ui.backhandler.BackHandler
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.LinkAnnotation
@@ -159,12 +159,7 @@
modifier: Modifier = Modifier
) {
val navigator = rememberListDetailPaneScaffoldNavigator<String>()
- val coroutineScope = rememberCoroutineScope()
- BackHandler(navigator.canNavigateBack()) {
- coroutineScope.launch {
- navigator.navigateBack()
- }
- }
+ ThreePaneScaffoldPredictiveBackHandler(navigator, BackNavigationBehavior.PopUntilScaffoldValueChange)
val scope = remember(navigator) { DefaultOpenSourcePaneScope(navigator) }
ListDetailPaneScaffold(
@@ -412,8 +407,24 @@
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private val ThreePaneScaffoldValue.expandedCount: Int
+ get() {
+ var count = 0
+ if (primary == PaneAdaptedValue.Expanded) {
+ count++
+ }
+ if (secondary == PaneAdaptedValue.Expanded) {
+ count++
+ }
+ if (tertiary == PaneAdaptedValue.Expanded) {
+ count++
+ }
+ return count
+ }
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
private fun ThreePaneScaffoldValue.isSinglePane(): Boolean {
- return primary == PaneAdaptedValue.Expanded && secondary == PaneAdaptedValue.Hidden && tertiary == PaneAdaptedValue.Hidden
+ return expandedCount == 1
}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/ui/material3/src/commonMain/kotlin/com/geekorum/aboutoss/ui/material3/ThreePaneScaffoldPredictiveBackHandler.kt Tue May 06 14:34:26 2025 -0400
@@ -0,0 +1,133 @@
+/*
+ * AboutOss is an utility library to retrieve and display
+ * opensource licenses in Android applications.
+ *
+ * Copyright (C) 2023-2025 by Frederic-Charles Barthelery.
+ *
+ * This file is part of AboutOss.
+ *
+ * AboutOss is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * AboutOss is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with AboutOss. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.aboutoss.ui.material3
+
+import androidx.compose.animation.core.CubicBezierEasing
+import androidx.compose.animation.core.Easing
+import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
+import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
+import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
+import androidx.compose.material3.adaptive.layout.SupportingPaneScaffold
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldState
+import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue
+import androidx.compose.material3.adaptive.navigation.BackNavigationBehavior
+import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.key
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.backhandler.PredictiveBackHandler
+import kotlinx.coroutines.NonCancellable
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.cancellation.CancellationException
+
+/* A common implementation of Android ThreePaneScaffoldPredictiveBackHandler
+ * TODO: removed when it is ported to multiplatform
+ */
+
+/**
+ * An effect to add predictive back handling to a three pane scaffold.
+ *
+ * [NavigableListDetailPaneScaffold] and [NavigableSupportingPaneScaffold] apply this effect
+ * automatically. If instead you are using [ListDetailPaneScaffold] or [SupportingPaneScaffold], use
+ * the overloads that accept a [ThreePaneScaffoldState] and pass
+ * [navigator.scaffoldState][ThreePaneScaffoldNavigator.scaffoldState] to the scaffold after adding
+ * this effect to your composition.
+ *
+ * A predictive back gesture will cause the [navigator] to
+ * [seekBack][ThreePaneScaffoldNavigator.seekBack] to the previous scaffold value. The progress can
+ * be read from the [progressFraction][ThreePaneScaffoldState.progressFraction] of the navigator's
+ * scaffold state. It will range from 0 (representing the start of the predictive back gesture) to
+ * some fraction less than 1 (representing a "peek" or "preview" of the previous scaffold value). If
+ * the gesture is committed, back navigation is performed. If the gesture is cancelled, the
+ * navigator's scaffold state is reset.
+ *
+ * @param navigator The navigator instance to navigate through the scaffold.
+ * @param backBehavior The back navigation behavior when the system back event happens. See
+ * [BackNavigationBehavior].
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+@ExperimentalMaterial3AdaptiveApi
+@Composable
+fun <T> ThreePaneScaffoldPredictiveBackHandler(
+ navigator: ThreePaneScaffoldNavigator<T>,
+ backBehavior: BackNavigationBehavior,
+) {
+ key(navigator, backBehavior) {
+ PredictiveBackHandler(enabled = navigator.canNavigateBack(backBehavior)) { progress ->
+ // code for gesture back started
+ try {
+ progress.collect { backEvent ->
+ navigator.seekBack(
+ backBehavior,
+ fraction =
+ backProgressToStateProgress(
+ progress = backEvent.progress,
+ scaffoldValue = navigator.scaffoldValue
+ ),
+ )
+ }
+ // code for completion
+ navigator.navigateBack(backBehavior)
+ } catch (e: CancellationException) {
+ // code for cancellation
+ withContext(NonCancellable) { navigator.seekBack(backBehavior, fraction = 0f) }
+ }
+ }
+ }
+}
+
+/**
+ * Converts a progress value originating from a predictive back gesture into a progress value to
+ * control a [ThreePaneScaffoldState].
+ */
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private fun backProgressToStateProgress(
+ progress: Float,
+ scaffoldValue: ThreePaneScaffoldValue,
+): Float =
+ ThreePaneScaffoldPredictiveBackEasing.transform(progress) *
+ when (scaffoldValue.expandedCount) {
+ 1 -> SinglePaneProgressRatio
+ 2 -> DualPaneProgressRatio
+ else -> TriplePaneProgressRatio
+ }
+
+private val ThreePaneScaffoldPredictiveBackEasing: Easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f)
+private const val SinglePaneProgressRatio: Float = 0.1f
+private const val DualPaneProgressRatio: Float = 0.15f
+private const val TriplePaneProgressRatio: Float = 0.2f
+
+@OptIn(ExperimentalMaterial3AdaptiveApi::class)
+private val ThreePaneScaffoldValue.expandedCount: Int
+ get() {
+ var count = 0
+ if (primary == PaneAdaptedValue.Expanded) {
+ count++
+ }
+ if (secondary == PaneAdaptedValue.Expanded) {
+ count++
+ }
+ if (tertiary == PaneAdaptedValue.Expanded) {
+ count++
+ }
+ return count
+ }