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