• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 2022 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 @file:OptIn(ExperimentalWearFoundationApi::class)
18 
19 package com.google.android.horologist.compose.navscaffold
20 
21 import androidx.compose.foundation.ScrollState
22 import androidx.compose.foundation.gestures.ScrollableState
23 import androidx.compose.foundation.layout.Box
24 import androidx.compose.foundation.layout.fillMaxSize
25 import androidx.compose.foundation.lazy.LazyListState
26 import androidx.compose.runtime.Composable
27 import androidx.compose.runtime.DisposableEffect
28 import androidx.compose.runtime.State
29 import androidx.compose.runtime.derivedStateOf
30 import androidx.compose.runtime.getValue
31 import androidx.compose.runtime.key
32 import androidx.compose.runtime.mutableStateOf
33 import androidx.compose.runtime.remember
34 import androidx.compose.ui.Modifier
35 import androidx.compose.ui.focus.FocusRequester
36 import androidx.compose.ui.platform.LocalLifecycleOwner
37 import androidx.lifecycle.Lifecycle
38 import androidx.lifecycle.LifecycleEventObserver
39 import androidx.lifecycle.viewmodel.compose.viewModel
40 import androidx.navigation.NamedNavArgument
41 import androidx.navigation.NavBackStackEntry
42 import androidx.navigation.NavDeepLink
43 import androidx.navigation.NavGraphBuilder
44 import androidx.navigation.NavHostController
45 import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
46 import androidx.wear.compose.foundation.HierarchicalFocusCoordinator
47 import androidx.wear.compose.foundation.lazy.ScalingLazyListState
48 import androidx.wear.compose.material.PositionIndicator
49 import androidx.wear.compose.material.Scaffold
50 import androidx.wear.compose.material.TimeText
51 import androidx.wear.compose.material.Vignette
52 import androidx.wear.compose.navigation.SwipeDismissableNavHost
53 import androidx.wear.compose.navigation.SwipeDismissableNavHostState
54 import androidx.wear.compose.navigation.composable
55 import androidx.wear.compose.navigation.currentBackStackEntryAsState
56 import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState
57 import com.google.android.horologist.annotations.ExperimentalHorologistApi
58 import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults
59 import com.google.android.horologist.compose.layout.ScalingLazyColumnState
60 import com.google.android.horologist.compose.layout.scrollAway
61 
62 /**
63  * A Navigation and Scroll aware [Scaffold].
64  *
65  * In addition to [NavGraphBuilder.scrollable], 3 additional extensions are supported
66  * [scalingLazyColumnComposable], [scrollStateComposable] and
67  * [lazyListComposable].
68  *
69  * These should be used to build the [ScrollableState] or [FocusRequester] as well as
70  * configure the behaviour of [TimeText], [PositionIndicator] or [Vignette].
71  */
72 @Composable
73 public fun WearNavScaffold(
74         startDestination: String,
75         navController: NavHostController,
76         modifier: Modifier = Modifier,
77         snackbar: @Composable () -> Unit = {},
<lambda>null78         timeText: @Composable (Modifier) -> Unit = {
79             TimeText(
80                     modifier = it,
81             )
82         },
83         state: SwipeDismissableNavHostState = rememberSwipeDismissableNavHostState(),
84         builder: NavGraphBuilder.() -> Unit,
85 ) {
86     val currentBackStackEntry: NavBackStackEntry? by navController.currentBackStackEntryAsState()
87 
<lambda>null88     val viewModel: NavScaffoldViewModel? = currentBackStackEntry?.let {
89         viewModel(viewModelStoreOwner = it)
90     }
91 
<lambda>null92     val scrollState: State<ScrollableState?> = remember(viewModel) {
93         derivedStateOf {
94             viewModel?.timeTextScrollableState()
95         }
96     }
97 
98     Scaffold(
99             modifier = modifier.fillMaxSize(),
<lambda>null100             timeText = {
101                 timeText(Modifier.scrollAway(scrollState))
102             },
<lambda>null103             positionIndicator = {
104                 key(currentBackStackEntry?.destination?.route) {
105                     val mode = viewModel?.positionIndicatorMode
106 
107                     if (mode == NavScaffoldViewModel.PositionIndicatorMode.On) {
108                         NavPositionIndicator(viewModel)
109                     }
110                 }
111             },
<lambda>null112             vignette = {
113                 key(currentBackStackEntry?.destination?.route) {
114                     val vignettePosition = viewModel?.vignettePosition
115                     if (vignettePosition is NavScaffoldViewModel.VignetteMode.On) {
116                         Vignette(vignettePosition = vignettePosition.position)
117                     }
118                 }
119             },
<lambda>null120     ) {
121         Box {
122             SwipeDismissableNavHost(
123                     navController = navController,
124                     startDestination = startDestination,
125                     state = state,
126             ) {
127                 builder()
128             }
129 
130             snackbar()
131         }
132     }
133 }
134 
135 @Composable
NavPositionIndicatornull136 private fun NavPositionIndicator(viewModel: NavScaffoldViewModel) {
137     when (viewModel.scrollType) {
138         NavScaffoldViewModel.ScrollType.ScrollState ->
139             PositionIndicator(
140                     scrollState = viewModel.scrollableState as ScrollState,
141             )
142 
143         NavScaffoldViewModel.ScrollType.ScalingLazyColumn -> {
144             PositionIndicator(
145                     scalingLazyListState = viewModel.scrollableState as ScalingLazyListState,
146             )
147         }
148 
149         NavScaffoldViewModel.ScrollType.LazyList ->
150             PositionIndicator(
151                     lazyListState = viewModel.scrollableState as LazyListState,
152             )
153 
154         else -> {}
155     }
156 }
157 
158 /**
159  * Add a screen to the navigation graph featuring a ScalingLazyColumn.
160  *
161  * The scalingLazyListState must be taken from the [ScaffoldContext].
162  */
163 @Deprecated(
164         "Use listComposable",
165 )
scalingLazyColumnComposablenull166 public fun NavGraphBuilder.scalingLazyColumnComposable(
167         route: String,
168         arguments: List<NamedNavArgument> = emptyList(),
169         deepLinks: List<NavDeepLink> = emptyList(),
170         scrollStateBuilder: () -> ScalingLazyListState,
171         content: @Composable (ScaffoldContext<ScalingLazyListState>) -> Unit,
172 ) {
173     composable(route, arguments, deepLinks) {
174         FocusedDestination {
175             val viewModel: NavScaffoldViewModel = viewModel(it)
176 
177             val scrollState = viewModel.initializeScalingLazyListState(scrollStateBuilder)
178 
179             content(ScaffoldContext(it, scrollState, viewModel))
180         }
181     }
182 }
183 
184 /**
185  * Add a screen to the navigation graph featuring a ScalingLazyColumn.
186  *
187  * The [ScalingLazyColumnState] must be taken from the [ScrollableScaffoldContext].
188  */
189 @ExperimentalHorologistApi
scrollablenull190 public fun NavGraphBuilder.scrollable(
191         route: String,
192         arguments: List<NamedNavArgument> = emptyList(),
193         deepLinks: List<NavDeepLink> = emptyList(),
194         columnStateFactory: ScalingLazyColumnState.Factory =
195                 ScalingLazyColumnDefaults.belowTimeText(),
196         content: @Composable (ScrollableScaffoldContext) -> Unit,
197 ) {
198     this@scrollable.composable(route, arguments, deepLinks) {
199         FocusedDestination {
200             val columnState = columnStateFactory.create()
201 
202             val viewModel: NavScaffoldViewModel = viewModel(it)
203 
204             viewModel.initializeScalingLazyListState(columnState)
205 
206             content(ScrollableScaffoldContext(it, columnState, viewModel))
207         }
208     }
209 }
210 
211 /**
212  * Add a screen to the navigation graph featuring a Scrollable item.
213  *
214  * The scrollState must be taken from the [ScaffoldContext].
215  */
scrollStateComposablenull216 public fun NavGraphBuilder.scrollStateComposable(
217         route: String,
218         arguments: List<NamedNavArgument> = emptyList(),
219         deepLinks: List<NavDeepLink> = emptyList(),
220         scrollStateBuilder: () -> ScrollState = { ScrollState(0) },
221         content: @Composable (ScaffoldContext<ScrollState>) -> Unit,
222 ) {
<lambda>null223     composable(route, arguments, deepLinks) {
224         FocusedDestination {
225             val viewModel: NavScaffoldViewModel = viewModel(it)
226 
227             val scrollState = viewModel.initializeScrollState(scrollStateBuilder)
228 
229             content(ScaffoldContext(it, scrollState, viewModel))
230         }
231     }
232 }
233 
234 /**
235  * Add a screen to the navigation graph featuring a Lazy list such as LazyColumn.
236  *
237  * The scrollState must be taken from the [ScaffoldContext].
238  */
lazyListComposablenull239 public fun NavGraphBuilder.lazyListComposable(
240         route: String,
241         arguments: List<NamedNavArgument> = emptyList(),
242         deepLinks: List<NavDeepLink> = emptyList(),
243         lazyListStateBuilder: () -> LazyListState = { LazyListState() },
244         content: @Composable (ScaffoldContext<LazyListState>) -> Unit,
245 ) {
<lambda>null246     composable(route, arguments, deepLinks) {
247         FocusedDestination {
248             val viewModel: NavScaffoldViewModel = viewModel(it)
249 
250             val scrollState = viewModel.initializeLazyList(lazyListStateBuilder)
251 
252             content(ScaffoldContext(it, scrollState, viewModel))
253         }
254     }
255 }
256 
257 /**
258  * Add non scrolling screen to the navigation graph. The [NavBackStackEntry] and
259  * [NavScaffoldViewModel] are passed into the [content] block so that
260  * the Scaffold may be customised, such as disabling TimeText.
261  */
262 @Deprecated(
263         "Use composable",
264         ReplaceWith("composable(route, arguments, deepLinks, lazyListStateBuilder, content)"),
265 )
wearNavComposablenull266 public fun NavGraphBuilder.wearNavComposable(
267         route: String,
268         arguments: List<NamedNavArgument> = emptyList(),
269         deepLinks: List<NavDeepLink> = emptyList(),
270         content: @Composable (NavBackStackEntry, NavScaffoldViewModel) -> Unit,
271 ) {
272     composable(route, arguments, deepLinks) {
273         FocusedDestination {
274             val viewModel: NavScaffoldViewModel = viewModel()
275 
276             content(it, viewModel)
277         }
278     }
279 }
280 
281 /**
282  * Add non scrolling screen to the navigation graph. The [NavBackStackEntry] and
283  * [NavScaffoldViewModel] are passed into the [content] block so that
284  * the Scaffold may be customised, such as disabling TimeText.
285  */
286 @ExperimentalHorologistApi
composablenull287 public fun NavGraphBuilder.composable(
288         route: String,
289         arguments: List<NamedNavArgument> = emptyList(),
290         deepLinks: List<NavDeepLink> = emptyList(),
291         content: @Composable (NonScrollableScaffoldContext) -> Unit,
292 ) {
293     this@composable.composable(route, arguments, deepLinks) {
294         FocusedDestination {
295             val viewModel: NavScaffoldViewModel = viewModel()
296 
297             content(NonScrollableScaffoldContext(it, viewModel))
298         }
299     }
300 }
301 
302 @Composable
FocusedDestinationnull303 internal fun FocusedDestination(content: @Composable () -> Unit) {
304     val lifecycle = LocalLifecycleOwner.current.lifecycle
305     val focused =
306             remember { mutableStateOf(lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) }
307 
308     DisposableEffect(lifecycle) {
309         val listener = LifecycleEventObserver { _, _ ->
310             focused.value = lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)
311         }
312         lifecycle.addObserver(listener)
313         onDispose {
314             lifecycle.removeObserver(listener)
315         }
316     }
317 
318     HierarchicalFocusCoordinator(requiresFocus = { focused.value }) {
319         content()
320     }
321 }
322