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