| /* |
| * Copyright 2024 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * https://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.compose.material.navigation |
| |
| import androidx.activity.compose.BackHandler |
| import androidx.compose.animation.core.AnimationSpec |
| import androidx.compose.animation.core.SpringSpec |
| import androidx.compose.foundation.layout.ColumnScope |
| import androidx.compose.material.ModalBottomSheetState |
| import androidx.compose.material.ModalBottomSheetValue |
| import androidx.compose.material.rememberModalBottomSheetState |
| import androidx.compose.runtime.Composable |
| import androidx.compose.runtime.LaunchedEffect |
| import androidx.compose.runtime.collectAsState |
| import androidx.compose.runtime.getValue |
| import androidx.compose.runtime.mutableStateOf |
| import androidx.compose.runtime.produceState |
| import androidx.compose.runtime.remember |
| import androidx.compose.runtime.saveable.rememberSaveableStateHolder |
| import androidx.compose.runtime.setValue |
| import androidx.compose.ui.util.fastForEach |
| import androidx.navigation.FloatingWindow |
| import androidx.navigation.NavBackStackEntry |
| import androidx.navigation.NavDestination |
| import androidx.navigation.NavOptions |
| import androidx.navigation.Navigator |
| import androidx.navigation.NavigatorState |
| import kotlinx.coroutines.CancellationException |
| import kotlinx.coroutines.flow.MutableStateFlow |
| import kotlinx.coroutines.flow.StateFlow |
| import kotlinx.coroutines.flow.transform |
| |
| /** |
| * The state of a [ModalBottomSheetLayout] that the [BottomSheetNavigator] drives |
| * |
| * @param sheetState The sheet state that is driven by the [BottomSheetNavigator] |
| */ |
| public class BottomSheetNavigatorSheetState(private val sheetState: ModalBottomSheetState) { |
| /** |
| * @see ModalBottomSheetState.isVisible |
| */ |
| public val isVisible: Boolean |
| get() = sheetState.isVisible |
| |
| /** |
| * @see ModalBottomSheetState.currentValue |
| */ |
| public val currentValue: ModalBottomSheetValue |
| get() = sheetState.currentValue |
| |
| /** |
| * @see ModalBottomSheetState.targetValue |
| */ |
| public val targetValue: ModalBottomSheetValue |
| get() = sheetState.targetValue |
| } |
| |
| /** |
| * Create and remember a [BottomSheetNavigator] |
| */ |
| @Composable |
| public fun rememberBottomSheetNavigator( |
| animationSpec: AnimationSpec<Float> = SpringSpec() |
| ): BottomSheetNavigator { |
| val sheetState = rememberModalBottomSheetState( |
| ModalBottomSheetValue.Hidden, |
| animationSpec = animationSpec |
| ) |
| return remember(sheetState) { BottomSheetNavigator(sheetState) } |
| } |
| |
| /** |
| * Navigator that drives a [ModalBottomSheetState] for use of [ModalBottomSheetLayout]s |
| * with the navigation library. Every destination using this Navigator must set a valid |
| * [Composable] by setting it directly on an instantiated [Destination] or calling |
| * [androidx.compose.material.navigation.bottomSheet]. |
| * |
| * <b>The [sheetContent] [Composable] will always host the latest entry of the back stack. When |
| * navigating from a [BottomSheetNavigator.Destination] to another |
| * [BottomSheetNavigator.Destination], the content of the sheet will be replaced instead of a |
| * new bottom sheet being shown.</b> |
| * |
| * When the sheet is dismissed by the user, the [state]'s [NavigatorState.backStack] will be popped. |
| * |
| * The primary constructor is not intended for public use. Please refer to |
| * [rememberBottomSheetNavigator] instead. |
| * |
| * @param sheetState The [ModalBottomSheetState] that the [BottomSheetNavigator] will use to |
| * drive the sheet state |
| */ |
| @Navigator.Name("bottomSheet") |
| public class BottomSheetNavigator( |
| internal val sheetState: ModalBottomSheetState |
| ) : Navigator<BottomSheetNavigator.Destination>() { |
| |
| private var attached by mutableStateOf(false) |
| |
| /** |
| * Get the back stack from the [state]. In some cases, the [sheetContent] might be composed |
| * before the Navigator is attached, so we specifically return an empty flow if we aren't |
| * attached yet. |
| */ |
| private val backStack: StateFlow<List<NavBackStackEntry>> |
| get() = if (attached) { |
| state.backStack |
| } else { |
| MutableStateFlow(emptyList()) |
| } |
| |
| /** |
| * Get the transitionsInProgress from the [state]. In some cases, the [sheetContent] might be |
| * composed before the Navigator is attached, so we specifically return an empty flow if we |
| * aren't attached yet. |
| */ |
| internal val transitionsInProgress: StateFlow<Set<NavBackStackEntry>> |
| get() = if (attached) { |
| state.transitionsInProgress |
| } else { |
| MutableStateFlow(emptySet()) |
| } |
| |
| /** |
| * Access properties of the [ModalBottomSheetLayout]'s [ModalBottomSheetState] |
| */ |
| public val navigatorSheetState: BottomSheetNavigatorSheetState = |
| BottomSheetNavigatorSheetState(sheetState) |
| |
| /** |
| * A [Composable] function that hosts the current sheet content. This should be set as |
| * sheetContent of your [ModalBottomSheetLayout]. |
| */ |
| internal val sheetContent: @Composable ColumnScope.() -> Unit = { |
| val saveableStateHolder = rememberSaveableStateHolder() |
| val transitionsInProgressEntries by transitionsInProgress.collectAsState() |
| |
| // The latest back stack entry, retained until the sheet is completely hidden |
| // While the back stack is updated immediately, we might still be hiding the sheet, so |
| // we keep the entry around until the sheet is hidden |
| val retainedEntry by produceState<NavBackStackEntry?>( |
| initialValue = null, |
| key1 = backStack |
| ) { |
| backStack |
| .transform { backStackEntries -> |
| // Always hide the sheet when the back stack is updated |
| // Regardless of whether we're popping or pushing, we always want to hide |
| // the sheet first before deciding whether to re-show it or keep it hidden |
| try { |
| sheetState.hide() |
| } catch (_: CancellationException) { |
| // We catch but ignore possible cancellation exceptions as we don't want |
| // them to bubble up and cancel the whole produceState coroutine |
| } finally { |
| emit(backStackEntries.lastOrNull()) |
| } |
| } |
| .collect { |
| value = it |
| } |
| } |
| |
| if (retainedEntry != null) { |
| LaunchedEffect(retainedEntry) { |
| sheetState.show() |
| } |
| |
| BackHandler { |
| state.popWithTransition(popUpTo = retainedEntry!!, saveState = false) |
| } |
| } |
| |
| SheetContentHost( |
| backStackEntry = retainedEntry, |
| sheetState = sheetState, |
| saveableStateHolder = saveableStateHolder, |
| onSheetShown = { |
| transitionsInProgressEntries.forEach(state::markTransitionComplete) |
| }, |
| onSheetDismissed = { backStackEntry -> |
| // Sheet dismissal can be started through popBackStack in which case we have a |
| // transition that we'll want to complete |
| if (transitionsInProgressEntries.contains(backStackEntry)) { |
| state.markTransitionComplete(backStackEntry) |
| } |
| // If there is no transition in progress, the sheet has been dimissed by the |
| // user (for example by tapping on the scrim or through an accessibility action) |
| // In this case, we will immediately pop without a transition as the sheet has |
| // already been hidden |
| else { |
| state.pop(popUpTo = backStackEntry, saveState = false) |
| } |
| } |
| ) |
| } |
| |
| override fun onAttach(state: NavigatorState) { |
| super.onAttach(state) |
| attached = true |
| } |
| |
| override fun createDestination(): Destination = Destination( |
| navigator = this, |
| content = {} |
| ) |
| |
| override fun navigate( |
| entries: List<NavBackStackEntry>, |
| navOptions: NavOptions?, |
| navigatorExtras: Extras? |
| ) { |
| entries.fastForEach { entry -> |
| state.pushWithTransition(entry) |
| } |
| } |
| |
| override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) { |
| state.popWithTransition(popUpTo, savedState) |
| } |
| |
| /** |
| * [NavDestination] specific to [BottomSheetNavigator] |
| */ |
| @NavDestination.ClassType(Composable::class) |
| public class Destination( |
| navigator: BottomSheetNavigator, |
| internal val content: @Composable ColumnScope.(NavBackStackEntry) -> Unit |
| ) : NavDestination(navigator), FloatingWindow |
| } |