1 /*
2 * 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 @file:Suppress("ObjectLiteralToLambda")
18 @file:OptIn(ExperimentalWearFoundationApi::class)
19
20 package com.android.permissioncontroller.permission.ui.wear.elements.layout
21
22 import androidx.compose.foundation.MutatePriority
23 import androidx.compose.foundation.gestures.FlingBehavior
24 import androidx.compose.foundation.gestures.ScrollScope
25 import androidx.compose.foundation.gestures.ScrollableDefaults
26 import androidx.compose.foundation.gestures.ScrollableState
27 import androidx.compose.foundation.layout.Arrangement
28 import androidx.compose.foundation.layout.PaddingValues
29 import androidx.compose.foundation.layout.fillMaxSize
30 import androidx.compose.runtime.Composable
31 import androidx.compose.runtime.saveable.rememberSaveable
32 import androidx.compose.ui.Alignment
33 import androidx.compose.ui.Modifier
34 import androidx.compose.ui.platform.LocalConfiguration
35 import androidx.compose.ui.platform.LocalDensity
36 import androidx.compose.ui.unit.dp
37 import androidx.wear.compose.foundation.ExperimentalWearFoundationApi
38 import androidx.wear.compose.foundation.lazy.AutoCenteringParams
39 import androidx.wear.compose.foundation.lazy.ScalingLazyColumn
40 import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults as WearScalingLazyColumnDefaults
41 import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType
42 import androidx.wear.compose.foundation.lazy.ScalingLazyListScope
43 import androidx.wear.compose.foundation.lazy.ScalingLazyListState
44 import androidx.wear.compose.foundation.lazy.ScalingParams
45 import androidx.wear.compose.foundation.rememberActiveFocusRequester
46 import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnDefaults.responsiveScalingParams
47 import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnState.RotaryMode
48 import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rememberDisabledHaptic
49 import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rememberRotaryHapticHandler
50 import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rotaryWithScroll
51 import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.rotaryWithSnap
52 import com.android.permissioncontroller.permission.ui.wear.elements.rotaryinput.toRotaryScrollAdapter
53
54 // This file is a copy of ScalingLazyColumnState.kt from Horologist (go/horologist),
55 // remove it once after wear compose supports large screen dialogs.
56
57 /**
58 * A Config and State object wrapping up all configuration for a [ScalingLazyColumn]. This allows
59 * defaults such as [ScalingLazyColumnDefaults.responsive].
60 */
61 class ScalingLazyColumnState(
62 val initialScrollPosition: ScrollPosition = ScrollPosition(1, 0),
63 val autoCentering: AutoCenteringParams? =
64 AutoCenteringParams(
65 initialScrollPosition.index,
66 initialScrollPosition.offsetPx,
67 ),
68 val anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter,
69 val contentPadding: PaddingValues = PaddingValues(horizontal = 10.dp),
70 val rotaryMode: RotaryMode? = RotaryMode.Scroll,
71 val reverseLayout: Boolean = false,
72 val verticalArrangement: Arrangement.Vertical =
73 Arrangement.spacedBy(
74 space = 4.dp,
75 alignment = if (!reverseLayout) Alignment.Top else Alignment.Bottom,
76 ),
77 val horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
78 val flingBehavior: FlingBehavior? = null,
79 val userScrollEnabled: Boolean = true,
80 val scalingParams: ScalingParams = WearScalingLazyColumnDefaults.scalingParams(),
81 val hapticsEnabled: Boolean = true,
82 ) : ScrollableState {
83 private var _state: ScalingLazyListState? = null
84 var state: ScalingLazyListState
85 get() {
86 if (_state == null) {
87 _state =
88 ScalingLazyListState(
89 initialScrollPosition.index,
90 initialScrollPosition.offsetPx,
91 )
92 }
93 return _state!!
94 }
95 set(value) {
96 _state = value
97 }
98
99 override val canScrollBackward: Boolean
100 get() = state.canScrollBackward
101
102 override val canScrollForward: Boolean
103 get() = state.canScrollForward
104
105 override val isScrollInProgress: Boolean
106 get() = state.isScrollInProgress
107
dispatchRawDeltanull108 override fun dispatchRawDelta(delta: Float): Float = state.dispatchRawDelta(delta)
109
110 override suspend fun scroll(
111 scrollPriority: MutatePriority,
112 block: suspend ScrollScope.() -> Unit,
113 ) {
114 state.scroll(scrollPriority, block)
115 }
116
117 sealed interface RotaryMode {
118 data object Snap : RotaryMode
119
120 data object Scroll : RotaryMode
121 }
122
123 data class ScrollPosition(
124 val index: Int,
125 val offsetPx: Int,
126 )
127
interfacenull128 fun interface Factory {
129 @Composable fun create(): ScalingLazyColumnState
130 }
131 }
132
133 // @Deprecated("Replaced by rememberResponsiveColumnState")
134 @Composable
rememberColumnStatenull135 fun rememberColumnState(
136 factory: ScalingLazyColumnState.Factory = ScalingLazyColumnDefaults.responsive(),
137 ): ScalingLazyColumnState {
138 val columnState = factory.create()
139
140 columnState.state = rememberSaveable(saver = ScalingLazyListState.Saver) { columnState.state }
141
142 return columnState
143 }
144
145 @Composable
rememberResponsiveColumnStatenull146 fun rememberResponsiveColumnState(
147 contentPadding: @Composable () -> PaddingValues =
148 ScalingLazyColumnDefaults.padding(
149 first = ScalingLazyColumnDefaults.ItemType.Unspecified,
150 last = ScalingLazyColumnDefaults.ItemType.Unspecified,
151 ),
152 verticalArrangement: Arrangement.Vertical =
153 Arrangement.spacedBy(
154 space = 4.dp,
155 alignment = Alignment.Top,
156 ),
157 rotaryMode: RotaryMode? = RotaryMode.Scroll,
158 hapticsEnabled: Boolean = true,
159 reverseLayout: Boolean = false,
160 userScrollEnabled: Boolean = true,
161 ): ScalingLazyColumnState {
162 val density = LocalDensity.current
163 val configuration = LocalConfiguration.current
164 val screenWidthDp = configuration.screenWidthDp.toFloat()
165 val screenHeightDp = configuration.screenHeightDp.toFloat()
166
167 val scalingParams = responsiveScalingParams(screenWidthDp)
168
169 val contentPaddingCalculated = contentPadding()
170
171 val screenHeightPx = with(density) { screenHeightDp.dp.roundToPx() }
172 val topPaddingPx = with(density) { contentPaddingCalculated.calculateTopPadding().roundToPx() }
173 val topScreenOffsetPx = screenHeightPx / 2 - topPaddingPx
174
175 val initialScrollPosition =
176 ScalingLazyColumnState.ScrollPosition(
177 index = 0,
178 offsetPx = topScreenOffsetPx,
179 )
180
181 val columnState =
182 ScalingLazyColumnState(
183 initialScrollPosition = initialScrollPosition,
184 autoCentering = null,
185 anchorType = ScalingLazyListAnchorType.ItemStart,
186 rotaryMode = rotaryMode,
187 verticalArrangement = verticalArrangement,
188 horizontalAlignment = Alignment.CenterHorizontally,
189 contentPadding = contentPaddingCalculated,
190 scalingParams = scalingParams,
191 hapticsEnabled = hapticsEnabled,
192 reverseLayout = reverseLayout,
193 userScrollEnabled = userScrollEnabled,
194 )
195
196 columnState.state = rememberSaveable(saver = ScalingLazyListState.Saver) { columnState.state }
197
198 return columnState
199 }
200
201 @Composable
ScalingLazyColumnnull202 fun ScalingLazyColumn(
203 columnState: ScalingLazyColumnState,
204 modifier: Modifier = Modifier,
205 content: ScalingLazyListScope.() -> Unit,
206 ) {
207 val focusRequester = rememberActiveFocusRequester()
208
209 val rotaryHaptics =
210 if (columnState.hapticsEnabled) {
211 rememberRotaryHapticHandler(columnState.state)
212 } else {
213 rememberDisabledHaptic()
214 }
215
216 val modifierWithRotary =
217 when (columnState.rotaryMode) {
218 RotaryMode.Snap ->
219 modifier.rotaryWithSnap(
220 focusRequester = focusRequester,
221 rotaryScrollAdapter = columnState.state.toRotaryScrollAdapter(),
222 reverseDirection = columnState.reverseLayout,
223 rotaryHaptics = rotaryHaptics,
224 )
225 RotaryMode.Scroll ->
226 modifier.rotaryWithScroll(
227 focusRequester = focusRequester,
228 scrollableState = columnState.state,
229 reverseDirection = columnState.reverseLayout,
230 rotaryHaptics = rotaryHaptics,
231 )
232 else -> modifier
233 }
234
235 ScalingLazyColumn(
236 modifier = modifierWithRotary.fillMaxSize(),
237 state = columnState.state,
238 contentPadding = columnState.contentPadding,
239 reverseLayout = columnState.reverseLayout,
240 verticalArrangement = columnState.verticalArrangement,
241 horizontalAlignment = columnState.horizontalAlignment,
242 flingBehavior = columnState.flingBehavior ?: ScrollableDefaults.flingBehavior(),
243 userScrollEnabled = columnState.userScrollEnabled,
244 scalingParams = columnState.scalingParams,
245 anchorType = columnState.anchorType,
246 autoCentering = columnState.autoCentering,
247 content = content,
248 )
249 }
250