• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * 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(ExperimentalHorologistApi::class, SavedStateHandleSaveableApi::class)
18 
19 package com.google.android.horologist.compose.navscaffold
20 
21 import android.os.Bundle
22 import androidx.compose.foundation.ScrollState
23 import androidx.compose.foundation.gestures.ScrollableState
24 import androidx.compose.foundation.lazy.LazyListState
25 import androidx.compose.runtime.getValue
26 import androidx.compose.runtime.mutableStateOf
27 import androidx.compose.runtime.setValue
28 import androidx.lifecycle.SavedStateHandle
29 import androidx.lifecycle.ViewModel
30 import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi
31 import androidx.lifecycle.viewmodel.compose.saveable
32 import androidx.navigation.NavBackStackEntry
33 import androidx.navigation.NavHostController
34 import androidx.wear.compose.foundation.lazy.ScalingLazyListState
35 import androidx.wear.compose.material.PositionIndicator
36 import androidx.wear.compose.material.Scaffold
37 import androidx.wear.compose.material.TimeText
38 import androidx.wear.compose.material.Vignette
39 import androidx.wear.compose.material.VignettePosition
40 import com.google.android.horologist.annotations.ExperimentalHorologistApi
41 import com.google.android.horologist.compose.layout.ScalingLazyColumnState
42 import com.google.android.horologist.compose.navscaffold.NavScaffoldViewModel.PositionIndicatorMode
43 import com.google.android.horologist.compose.navscaffold.NavScaffoldViewModel.TimeTextMode
44 import com.google.android.horologist.compose.navscaffold.NavScaffoldViewModel.TimeTextMode.ScrollAway
45 import com.google.android.horologist.compose.navscaffold.NavScaffoldViewModel.VignetteMode.Off
46 import com.google.android.horologist.compose.navscaffold.NavScaffoldViewModel.VignetteMode.On
47 import com.google.android.horologist.compose.navscaffold.NavScaffoldViewModel.VignetteMode.WhenScrollable
48 
49 /**
50  * A ViewModel that backs the WearNavScaffold to allow each composable to interact and effect
51  * the [Scaffold] positionIndicator, vignette and timeText.
52  *
53  * A ViewModel is used to allow the same current instance to be shared between the WearNavScaffold
54  * and the composable screen via [NavHostController.currentBackStackEntry].
55  */
56 public open class NavScaffoldViewModel(
57         private val savedStateHandle: SavedStateHandle,
58 ) : ViewModel() {
59     internal var initialIndex: Int? = null
60     internal var initialOffsetPx: Int? = null
61     internal var scrollType by mutableStateOf<ScrollType?>(null)
62 
63     private lateinit var _scrollableState: ScrollableState
64 
65     /**
66      * Returns the scrollable state for this composable or null if the scaffold should
67      * not consider this element to be scrollable.
68      */
69     public val scrollableState: ScrollableState?
70         get() = if (scrollType == null || scrollType == ScrollType.None) {
71             null
72         } else {
73             _scrollableState
74         }
75 
76     /**
77      * The configuration of [Vignette], [WhenScrollable], [Off], [On] and if so whether top and
78      * bottom. Defaults to on for scrollable screens.
79      */
80     public var vignettePosition: VignetteMode by mutableStateOf(WhenScrollable)
81 
82     /**
83      * The configuration of [TimeText], defaults to [TimeTextMode.ScrollAway] which will move the
84      * time text above the screen to avoid overlapping with the content moving up.
85      */
86     public var timeTextMode: TimeTextMode by mutableStateOf(ScrollAway)
87 
88     /**
89      * The configuration of [PositionIndicator].  The default is to show a scroll bar while the
90      * scroll is in progress.
91      */
92     public var positionIndicatorMode: PositionIndicatorMode
93             by mutableStateOf(PositionIndicatorMode.On)
94 
initializeScrollStatenull95     internal fun initializeScrollState(scrollStateBuilder: () -> ScrollState): ScrollState {
96         check(scrollType == null || scrollType == ScrollType.ScrollState)
97 
98         if (scrollType == null) {
99             scrollType = ScrollType.ScrollState
100 
101             _scrollableState = savedStateHandle.saveable(
102                     key = "navScaffold.ScrollState",
103                     saver = ScrollState.Saver,
104             ) {
105                 scrollStateBuilder()
106             }
107         }
108 
109         return _scrollableState as ScrollState
110     }
111 
initializeScalingLazyListStatenull112     internal fun initializeScalingLazyListState(
113             scrollableStateBuilder: () -> ScalingLazyListState,
114     ): ScalingLazyListState {
115         check(scrollType == null || scrollType == ScrollType.ScalingLazyColumn)
116 
117         if (scrollType == null) {
118             scrollType = ScrollType.ScalingLazyColumn
119 
120             _scrollableState = savedStateHandle.saveable(
121                     key = "navScaffold.ScalingLazyListState",
122                     saver = ScalingLazyListState.Saver,
123             ) {
124                 scrollableStateBuilder().also {
125                     initialIndex = it.centerItemIndex
126                     initialOffsetPx = it.centerItemScrollOffset
127                 }
128             }
129         }
130 
131         return _scrollableState as ScalingLazyListState
132     }
133 
initializeScalingLazyListStatenull134     internal fun initializeScalingLazyListState(
135             columnState: ScalingLazyColumnState,
136     ) {
137         check(scrollType == null || scrollType == ScrollType.ScalingLazyColumn)
138 
139         if (scrollType == null) {
140             scrollType = ScrollType.ScalingLazyColumn
141 
142             initialIndex = columnState.initialScrollPosition.index
143             initialOffsetPx = columnState.initialScrollPosition.offsetPx
144 
145             _scrollableState = savedStateHandle.saveable(
146                     key = "navScaffold.ScalingLazyListState",
147                     saver = ScalingLazyListState.Saver,
148             ) {
149                 columnState.state
150             }
151         }
152 
153         columnState.state = _scrollableState as ScalingLazyListState
154     }
155 
initializeLazyListnull156     internal fun initializeLazyList(
157             scrollableStateBuilder: () -> LazyListState,
158     ): LazyListState {
159         check(scrollType == null || scrollType == ScrollType.LazyList)
160 
161         if (scrollType == null) {
162             scrollType = ScrollType.LazyList
163 
164             _scrollableState = savedStateHandle.saveable(
165                     key = "navScaffold.LazyListState",
166                     saver = LazyListState.Saver,
167             ) {
168                 scrollableStateBuilder()
169             }
170         }
171 
172         return _scrollableState as LazyListState
173     }
174 
175     internal enum class ScrollType {
176         None, ScalingLazyColumn, ScrollState, LazyList
177     }
178 
179     /**
180      * The configuration of [TimeText], defaults to [ScrollAway] which will move the time text above the
181      * screen to avoid overlapping with the content moving up.
182      */
183     public enum class TimeTextMode {
184         On, Off, ScrollAway
185     }
186 
187     /**
188      * The configuration of [PositionIndicator].  The default is to show a scroll bar while the
189      * scroll is in progress.
190      */
191     public enum class PositionIndicatorMode {
192         On, Off
193     }
194 
195     /**
196      * The configuration of [Vignette], [WhenScrollable], [Off], [On] and if so whether top and
197      * bottom. Defaults to on for scrollable screens.
198      */
199     public sealed interface VignetteMode {
200         public object WhenScrollable : VignetteMode
201         public object Off : VignetteMode
202         public data class On(val position: VignettePosition) : VignetteMode
203     }
204 
timeTextScrollableStatenull205     internal fun timeTextScrollableState(): ScrollableState? {
206         return when (timeTextMode) {
207             ScrollAway -> {
208                 when (this.scrollType) {
209                     ScrollType.ScrollState -> {
210                         this.scrollableState as ScrollState
211                     }
212 
213                     ScrollType.ScalingLazyColumn -> {
214                         val scalingLazyListState =
215                                 this.scrollableState as ScalingLazyListState
216 
217                         ScalingLazyColumnScrollableState(scalingLazyListState, initialIndex
218                                 ?: 1, initialOffsetPx ?: 0)
219                     }
220 
221                     ScrollType.LazyList -> {
222                         this.scrollableState as LazyListState
223                     }
224 
225                     else -> {
226                         ScrollState(0)
227                     }
228                 }
229             }
230 
231             TimeTextMode.On -> {
232                 ScrollState(0)
233             }
234 
235             else -> {
236                 null
237             }
238         }
239     }
240 }
241 
242 internal class ScalingLazyColumnScrollableState(
243         val scalingLazyListState: ScalingLazyListState,
244         val initialIndex: Int,
245         val initialOffsetPx: Int,
246 ) : ScrollableState by scalingLazyListState
247 
248 /**
249  * The context items provided to a navigation composable.
250  *
251  * The [viewModel] can be used to customise the scaffold behaviour.
252  */
253 public data class ScaffoldContext<T : ScrollableState>(
254         val backStackEntry: NavBackStackEntry,
255         val scrollableState: T,
256         val viewModel: NavScaffoldViewModel,
257 ) {
258     var timeTextMode: TimeTextMode by viewModel::timeTextMode
259 
260     var positionIndicatorMode: PositionIndicatorMode by viewModel::positionIndicatorMode
261 
262     val arguments: Bundle?
263         get() = backStackEntry.arguments
264 }
265 
266 public data class NonScrollableScaffoldContext(
267         val backStackEntry: NavBackStackEntry,
268         val viewModel: NavScaffoldViewModel,
269 ) {
270     var timeTextMode: TimeTextMode by viewModel::timeTextMode
271 
272     var positionIndicatorMode: PositionIndicatorMode by viewModel::positionIndicatorMode
273 
274     val arguments: Bundle?
275         get() = backStackEntry.arguments
276 }
277 
278 /**
279  * The context items provided to a navigation composable.
280  *
281  * The [viewModel] can be used to customise the scaffold behaviour.
282  */
283 public data class ScrollableScaffoldContext(
284         val backStackEntry: NavBackStackEntry,
285         val columnState: ScalingLazyColumnState,
286         val viewModel: NavScaffoldViewModel,
287 ) {
288     val scrollableState: ScalingLazyListState
289         get() = columnState.state
290 
291     var timeTextMode: TimeTextMode by viewModel::timeTextMode
292 
293     var positionIndicatorMode: PositionIndicatorMode by viewModel::positionIndicatorMode
294 
295     val arguments: Bundle?
296         get() = backStackEntry.arguments
297 }
298