# HG changeset patch # User Da Risk # Date 1746556466 14400 # Node ID fc904f4edea81329a32544edd10f353fd86144a3 # Parent d0a027c33366a262b63e051fdfea1d262ef55aab ui:material3: AdaptiveOpenSourceDependenciesScreen supports predictive backhandler diff -r d0a027c33366 -r fc904f4edea8 ui/material3/src/commonMain/kotlin/com/geekorum/aboutoss/ui/material3/AdaptiveOpenSourceDependenciesScreen.kt --- 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() - 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 } diff -r d0a027c33366 -r fc904f4edea8 ui/material3/src/commonMain/kotlin/com/geekorum/aboutoss/ui/material3/ThreePaneScaffoldPredictiveBackHandler.kt --- /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 . + */ +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 ThreePaneScaffoldPredictiveBackHandler( + navigator: ThreePaneScaffoldNavigator, + 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 + }