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:Suppress("ObjectLiteralToLambda")
18 @file:OptIn(ExperimentalHorologistApi::class, ExperimentalWearFoundationApi::class)
19
20 package com.google.android.horologist.compose.layout
21
22 import androidx.compose.foundation.gestures.FlingBehavior
23 import androidx.compose.foundation.gestures.ScrollableDefaults
24 import androidx.compose.foundation.layout.Arrangement
25 import androidx.compose.foundation.layout.PaddingValues
26 import androidx.compose.runtime.Composable
27 import androidx.compose.runtime.saveable.rememberSaveable
28 import androidx.compose.ui.Alignment
29 import androidx.compose.ui.Modifier
30 import androidx.compose.ui.unit.dp
31 import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
32 import androidx.wear.compose.foundation.lazy.AutoCenteringParams
33 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
34 import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType
35 import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
36 import androidx.wear.compose.foundation.lazy.ScalingLazyListState
37 import androidx.wear.compose.foundation.lazy.ScalingParams
38 import androidx.wear.compose.foundation.rememberActiveFocusRequester
39 import com.google.android.horologist.annotations.ExperimentalHorologistApi
40 import com.google.android.horologist.compose.layout.ScalingLazyColumnState.RotaryMode
41 import com.google.android.horologist.compose.rotaryinput.rememberDisabledHaptic
42 import com.google.android.horologist.compose.rotaryinput.rememberRotaryHapticHandler
43 import com.google.android.horologist.compose.rotaryinput.rotaryWithScroll
44 import com.google.android.horologist.compose.rotaryinput.rotaryWithSnap
45 import com.google.android.horologist.compose.rotaryinput.toRotaryScrollAdapter
46 import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults as WearScalingLazyColumnDefaults
47
48 /**
49 * A Config and State object wrapping up all configuration for a [ScalingLazyColumn].
50 * This allows defaults such as [ScalingLazyColumnDefaults.belowTimeText].
51 */
52 @ExperimentalHorologistApi
53 public class ScalingLazyColumnState(
54 public val initialScrollPosition: ScrollPosition = ScrollPosition(1, 0),
55 public val autoCentering: AutoCenteringParams? = AutoCenteringParams(
56 initialScrollPosition.index,
57 initialScrollPosition.offsetPx,
58 ),
59 public val anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter,
60 public val contentPadding: PaddingValues = PaddingValues(horizontal = 10.dp),
61 public val rotaryMode: RotaryMode = RotaryMode.Scroll,
62 public val reverseLayout: Boolean = false,
63 public val verticalArrangement: Arrangement.Vertical =
64 Arrangement.spacedBy(
65 space = 4.dp,
66 alignment = if (!reverseLayout) Alignment.Top else Alignment.Bottom,
67 ),
68 public val horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
69 public val flingBehavior: FlingBehavior? = null,
70 public val userScrollEnabled: Boolean = true,
71 public val scalingParams: ScalingParams = WearScalingLazyColumnDefaults.scalingParams(),
72 public val hapticsEnabled: Boolean = true,
73 ) {
74 private var _state: ScalingLazyListState? = null
75 public var state: ScalingLazyListState
76 get() {
77 if (_state == null) {
78 _state = ScalingLazyListState(
79 initialScrollPosition.index,
80 initialScrollPosition.offsetPx,
81 )
82 }
83 return _state!!
84 }
85 set(value) {
86 _state = value
87 }
88
89 public sealed interface RotaryMode {
90 public object Snap : RotaryMode
91 public object Scroll : RotaryMode
92
93 @Deprecated(
94 "Use RotaryMode.Scroll instead",
95 replaceWith = ReplaceWith("RotaryMode.Scroll"),
96 )
97 public object Fling : RotaryMode
98 }
99
100 public data class ScrollPosition(
101 val index: Int,
102 val offsetPx: Int,
103 )
104
105 public fun interface Factory {
106 @Composable
createnull107 public fun create(): ScalingLazyColumnState
108 }
109 }
110
111 @Composable
112 public fun rememberColumnState(
113 factory: ScalingLazyColumnState.Factory = ScalingLazyColumnDefaults.belowTimeText(),
114 ): ScalingLazyColumnState {
115 val columnState = factory.create()
116
117 columnState.state = rememberSaveable(saver = ScalingLazyListState.Saver) {
118 columnState.state
119 }
120
121 return columnState
122 }
123
124 @ExperimentalHorologistApi
125 @Composable
ScalingLazyColumnnull126 public fun ScalingLazyColumn(
127 columnState: ScalingLazyColumnState,
128 modifier: Modifier = Modifier,
129 content: ScalingLazyListScope.() -> Unit,
130 ) {
131 val focusRequester = rememberActiveFocusRequester()
132
133 val rotaryHaptics = if (columnState.hapticsEnabled) {
134 rememberRotaryHapticHandler(columnState.state)
135 } else {
136 rememberDisabledHaptic()
137 }
138 val modifierWithRotary = when (columnState.rotaryMode) {
139 RotaryMode.Snap -> modifier.rotaryWithSnap(
140 focusRequester = focusRequester,
141 rotaryScrollAdapter = columnState.state.toRotaryScrollAdapter(),
142 reverseDirection = columnState.reverseLayout,
143 rotaryHaptics = rotaryHaptics,
144 )
145
146 else -> modifier.rotaryWithScroll(
147 focusRequester = focusRequester,
148 scrollableState = columnState.state,
149 reverseDirection = columnState.reverseLayout,
150 rotaryHaptics = rotaryHaptics,
151 )
152 }
153
154 ScalingLazyColumn(
155 modifier = modifierWithRotary,
156 state = columnState.state,
157 contentPadding = columnState.contentPadding,
158 reverseLayout = columnState.reverseLayout,
159 verticalArrangement = columnState.verticalArrangement,
160 horizontalAlignment = columnState.horizontalAlignment,
161 flingBehavior = columnState.flingBehavior ?: ScrollableDefaults.flingBehavior(),
162 userScrollEnabled = columnState.userScrollEnabled,
163 scalingParams = columnState.scalingParams,
164 anchorType = columnState.anchorType,
165 autoCentering = columnState.autoCentering,
166 content = content,
167 )
168 }
169