1 /*
<lambda>null2 * Copyright 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.material.navigation
18
19 import androidx.activity.compose.BackHandler
20 import androidx.compose.animation.core.AnimationSpec
21 import androidx.compose.animation.core.SpringSpec
22 import androidx.compose.foundation.layout.ColumnScope
23 import androidx.compose.material.ModalBottomSheetState
24 import androidx.compose.material.ModalBottomSheetValue
25 import androidx.compose.material.rememberModalBottomSheetState
26 import androidx.compose.runtime.Composable
27 import androidx.compose.runtime.LaunchedEffect
28 import androidx.compose.runtime.collectAsState
29 import androidx.compose.runtime.getValue
30 import androidx.compose.runtime.mutableStateOf
31 import androidx.compose.runtime.produceState
32 import androidx.compose.runtime.remember
33 import androidx.compose.runtime.saveable.rememberSaveableStateHolder
34 import androidx.compose.runtime.setValue
35 import androidx.compose.ui.util.fastForEach
36 import androidx.navigation.FloatingWindow
37 import androidx.navigation.NavBackStackEntry
38 import androidx.navigation.NavDestination
39 import androidx.navigation.NavOptions
40 import androidx.navigation.Navigator
41 import androidx.navigation.NavigatorState
42 import kotlinx.coroutines.CancellationException
43 import kotlinx.coroutines.flow.MutableStateFlow
44 import kotlinx.coroutines.flow.StateFlow
45 import kotlinx.coroutines.flow.transform
46
47 /**
48 * The state of a [ModalBottomSheetLayout] that the [BottomSheetNavigator] drives
49 *
50 * @param sheetState The sheet state that is driven by the [BottomSheetNavigator]
51 */
52 public class BottomSheetNavigatorSheetState(private val sheetState: ModalBottomSheetState) {
53 /** @see ModalBottomSheetState.isVisible */
54 public val isVisible: Boolean
55 get() = sheetState.isVisible
56
57 /** @see ModalBottomSheetState.currentValue */
58 public val currentValue: ModalBottomSheetValue
59 get() = sheetState.currentValue
60
61 /** @see ModalBottomSheetState.targetValue */
62 public val targetValue: ModalBottomSheetValue
63 get() = sheetState.targetValue
64 }
65
66 /**
67 * Create and remember a [BottomSheetNavigator].
68 *
69 * @param animationSpec The default animation that will be used to animate to a new state.
70 */
71 @Composable
rememberBottomSheetNavigatornull72 public fun rememberBottomSheetNavigator(
73 animationSpec: AnimationSpec<Float> = SpringSpec()
74 ): BottomSheetNavigator {
75 val sheetState =
76 rememberModalBottomSheetState(ModalBottomSheetValue.Hidden, animationSpec = animationSpec)
77 return remember(sheetState) { BottomSheetNavigator(sheetState) }
78 }
79
80 /**
81 * Navigator that drives a [ModalBottomSheetState] for use of [ModalBottomSheetLayout]s with the
82 * navigation library. Every destination using this Navigator must set a valid [Composable] by
83 * setting it directly on an instantiated [Destination] or calling
84 * [androidx.compose.material.navigation.bottomSheet].
85 *
86 * <b>The [sheetContent] [Composable] will always host the latest entry of the back stack. When
87 * navigating from a [BottomSheetNavigator.Destination] to another
88 * [BottomSheetNavigator.Destination], the content of the sheet will be replaced instead of a new
89 * bottom sheet being shown.</b>
90 *
91 * When the sheet is dismissed by the user, the [state]'s [NavigatorState.backStack] will be popped.
92 *
93 * @param sheetState The [ModalBottomSheetState] that the [BottomSheetNavigator] will use to drive
94 * the sheet state
95 * @see rememberBottomSheetNavigator()
96 */
97 @Navigator.Name("bottomSheet")
98 public class BottomSheetNavigator(internal val sheetState: ModalBottomSheetState) :
99 Navigator<BottomSheetNavigator.Destination>() {
100
101 private var attached by mutableStateOf(false)
102
103 /**
104 * Get the back stack from the [state]. In some cases, the [sheetContent] might be composed
105 * before the Navigator is attached, so we specifically return an empty flow if we aren't
106 * attached yet.
107 */
108 private val backStack: StateFlow<List<NavBackStackEntry>>
109 get() =
110 if (attached) {
111 state.backStack
112 } else {
113 MutableStateFlow(emptyList())
114 }
115
116 /**
117 * Get the transitionsInProgress from the [state]. In some cases, the [sheetContent] might be
118 * composed before the Navigator is attached, so we specifically return an empty flow if we
119 * aren't attached yet.
120 */
121 internal val transitionsInProgress: StateFlow<Set<NavBackStackEntry>>
122 get() =
123 if (attached) {
124 state.transitionsInProgress
125 } else {
126 MutableStateFlow(emptySet())
127 }
128
129 /** Access properties of the [ModalBottomSheetLayout]'s [ModalBottomSheetState] */
130 public val navigatorSheetState: BottomSheetNavigatorSheetState =
131 BottomSheetNavigatorSheetState(sheetState)
132
133 /**
134 * A [Composable] function that hosts the current sheet content. This should be set as
135 * sheetContent of your [ModalBottomSheetLayout].
136 */
<lambda>null137 internal val sheetContent: @Composable ColumnScope.() -> Unit = {
138 val saveableStateHolder = rememberSaveableStateHolder()
139 val transitionsInProgressEntries by transitionsInProgress.collectAsState()
140
141 // The latest back stack entry, retained until the sheet is completely hidden
142 // While the back stack is updated immediately, we might still be hiding the sheet, so
143 // we keep the entry around until the sheet is hidden
144 val retainedEntry by
145 produceState<NavBackStackEntry?>(initialValue = null, key1 = backStack) {
146 backStack
147 .transform { backStackEntries ->
148 // Always hide the sheet when the back stack is updated
149 // Regardless of whether we're popping or pushing, we always want to hide
150 // the sheet first before deciding whether to re-show it or keep it hidden
151 try {
152 sheetState.hide()
153 } catch (_: CancellationException) {
154 // We catch but ignore possible cancellation exceptions as we don't want
155 // them to bubble up and cancel the whole produceState coroutine
156 } finally {
157 emit(backStackEntries.lastOrNull())
158 }
159 }
160 .collect { value = it }
161 }
162
163 if (retainedEntry != null) {
164 LaunchedEffect(retainedEntry) { sheetState.show() }
165
166 BackHandler { state.popWithTransition(popUpTo = retainedEntry!!, saveState = false) }
167 }
168
169 SheetContentHost(
170 backStackEntry = retainedEntry,
171 sheetState = sheetState,
172 saveableStateHolder = saveableStateHolder,
173 onSheetShown = { transitionsInProgressEntries.forEach(state::markTransitionComplete) },
174 onSheetDismissed = { backStackEntry ->
175 // Sheet dismissal can be started through popBackStack in which case we have a
176 // transition that we'll want to complete
177 if (transitionsInProgressEntries.contains(backStackEntry)) {
178 state.markTransitionComplete(backStackEntry)
179 }
180 // If there is no transition in progress, the sheet has been dimissed by the
181 // user (for example by tapping on the scrim or through an accessibility action)
182 // In this case, we will immediately pop without a transition as the sheet has
183 // already been hidden
184 else {
185 state.pop(popUpTo = backStackEntry, saveState = false)
186 }
187 }
188 )
189 }
190
onAttachnull191 override fun onAttach(state: NavigatorState) {
192 super.onAttach(state)
193 attached = true
194 }
195
<lambda>null196 override fun createDestination(): Destination = Destination(navigator = this, content = {})
197
navigatenull198 override fun navigate(
199 entries: List<NavBackStackEntry>,
200 navOptions: NavOptions?,
201 navigatorExtras: Extras?
202 ) {
203 entries.fastForEach { entry -> state.pushWithTransition(entry) }
204 }
205
popBackStacknull206 override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
207 state.popWithTransition(popUpTo, savedState)
208 }
209
210 /**
211 * [NavDestination] specific to [BottomSheetNavigator].
212 *
213 * @param navigator The navigator used to navigate to this destination
214 * @param content The content to be displayed for this destination
215 */
216 @NavDestination.ClassType(Composable::class)
217 public class Destination(
218 navigator: BottomSheetNavigator,
219 internal val content: @Composable ColumnScope.(NavBackStackEntry) -> Unit
220 ) : NavDestination(navigator), FloatingWindow
221 }
222