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.wear.permission.components.material2.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 com.android.permissioncontroller.wear.permission.components.material2.layout.ScalingLazyColumnDefaults.responsiveScalingParams
46 import com.android.permissioncontroller.wear.permission.components.material2.layout.ScalingLazyColumnState.RotaryMode
47
48 // This file is a copy of ScalingLazyColumnState.kt from Horologist (go/horologist),
49 // remove it once after wear compose supports large screen dialogs.
50
51 /**
52 * A Config and State object wrapping up all configuration for a [ScalingLazyColumn]. This allows
53 * defaults such as [ScalingLazyColumnDefaults.responsive].
54 */
55 class ScalingLazyColumnState(
56 val initialScrollPosition: ScrollPosition = ScrollPosition(1, 0),
57 val autoCentering: AutoCenteringParams? =
58 AutoCenteringParams(initialScrollPosition.index, initialScrollPosition.offsetPx),
59 val anchorType: ScalingLazyListAnchorType = ScalingLazyListAnchorType.ItemCenter,
60 val contentPadding: PaddingValues = PaddingValues(horizontal = 10.dp),
61 val rotaryMode: RotaryMode? = RotaryMode.Scroll,
62 val reverseLayout: Boolean = false,
63 val verticalArrangement: Arrangement.Vertical =
64 Arrangement.spacedBy(
65 space = 4.dp,
66 alignment = if (!reverseLayout) Alignment.Top else Alignment.Bottom,
67 ),
68 val horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
69 val flingBehavior: FlingBehavior? = null,
70 val userScrollEnabled: Boolean = true,
71 val scalingParams: ScalingParams = WearScalingLazyColumnDefaults.scalingParams(),
72 val hapticsEnabled: Boolean = true,
73 ) : ScrollableState {
74 private var _state: ScalingLazyListState? = null
75 var state: ScalingLazyListState
76 get() {
77 if (_state == null) {
78 _state =
79 ScalingLazyListState(
80 initialScrollPosition.index,
81 initialScrollPosition.offsetPx,
82 )
83 }
84 return _state!!
85 }
86 set(value) {
87 _state = value
88 }
89
90 override val canScrollBackward: Boolean
91 get() = state.canScrollBackward
92
93 override val canScrollForward: Boolean
94 get() = state.canScrollForward
95
96 override val isScrollInProgress: Boolean
97 get() = state.isScrollInProgress
98
dispatchRawDeltanull99 override fun dispatchRawDelta(delta: Float): Float = state.dispatchRawDelta(delta)
100
101 override suspend fun scroll(
102 scrollPriority: MutatePriority,
103 block: suspend ScrollScope.() -> Unit,
104 ) {
105 state.scroll(scrollPriority, block)
106 }
107
108 sealed interface RotaryMode {
109 data object Snap : RotaryMode
110
111 data object Scroll : RotaryMode
112 }
113
114 data class ScrollPosition(val index: Int, val offsetPx: Int)
115
interfacenull116 fun interface Factory {
117 @Composable fun create(): ScalingLazyColumnState
118 }
119 }
120
121 // @Deprecated("Replaced by rememberResponsiveColumnState")
122 @Composable
rememberColumnStatenull123 fun rememberColumnState(
124 factory: ScalingLazyColumnState.Factory = ScalingLazyColumnDefaults.responsive()
125 ): ScalingLazyColumnState {
126 val columnState = factory.create()
127
128 columnState.state = rememberSaveable(saver = ScalingLazyListState.Saver) { columnState.state }
129
130 return columnState
131 }
132
133 @Composable
rememberResponsiveColumnStatenull134 fun rememberResponsiveColumnState(
135 contentPadding: @Composable () -> PaddingValues =
136 ScalingLazyColumnDefaults.padding(
137 first = ScalingLazyColumnDefaults.ItemType.Unspecified,
138 last = ScalingLazyColumnDefaults.ItemType.Unspecified,
139 ),
140 verticalArrangement: Arrangement.Vertical =
141 Arrangement.spacedBy(space = 4.dp, alignment = Alignment.Top),
142 rotaryMode: RotaryMode? = RotaryMode.Scroll,
143 hapticsEnabled: Boolean = true,
144 reverseLayout: Boolean = false,
145 userScrollEnabled: Boolean = true,
146 ): ScalingLazyColumnState {
147 val density = LocalDensity.current
148 val configuration = LocalConfiguration.current
149 val screenWidthDp = configuration.screenWidthDp.toFloat()
150 val screenHeightDp = configuration.screenHeightDp.toFloat()
151
152 val scalingParams = responsiveScalingParams(screenWidthDp)
153
154 val contentPaddingCalculated = contentPadding()
155
156 val screenHeightPx = with(density) { screenHeightDp.dp.roundToPx() }
157 val topPaddingPx = with(density) { contentPaddingCalculated.calculateTopPadding().roundToPx() }
158 val topScreenOffsetPx = screenHeightPx / 2 - topPaddingPx
159
160 val initialScrollPosition =
161 ScalingLazyColumnState.ScrollPosition(index = 0, offsetPx = topScreenOffsetPx)
162
163 val columnState =
164 ScalingLazyColumnState(
165 initialScrollPosition = initialScrollPosition,
166 autoCentering = null,
167 anchorType = ScalingLazyListAnchorType.ItemStart,
168 rotaryMode = rotaryMode,
169 verticalArrangement = verticalArrangement,
170 horizontalAlignment = Alignment.CenterHorizontally,
171 contentPadding = contentPaddingCalculated,
172 scalingParams = scalingParams,
173 hapticsEnabled = hapticsEnabled,
174 reverseLayout = reverseLayout,
175 userScrollEnabled = userScrollEnabled,
176 )
177
178 columnState.state = rememberSaveable(saver = ScalingLazyListState.Saver) { columnState.state }
179
180 return columnState
181 }
182
183 @Composable
ScalingLazyColumnnull184 fun ScalingLazyColumn(
185 columnState: ScalingLazyColumnState,
186 modifier: Modifier = Modifier,
187 content: ScalingLazyListScope.() -> Unit,
188 ) {
189 ScalingLazyColumn(
190 modifier = modifier.fillMaxSize(),
191 state = columnState.state,
192 contentPadding = columnState.contentPadding,
193 reverseLayout = columnState.reverseLayout,
194 verticalArrangement = columnState.verticalArrangement,
195 horizontalAlignment = columnState.horizontalAlignment,
196 flingBehavior = columnState.flingBehavior ?: ScrollableDefaults.flingBehavior(),
197 userScrollEnabled = columnState.userScrollEnabled,
198 scalingParams = columnState.scalingParams,
199 anchorType = columnState.anchorType,
200 autoCentering = columnState.autoCentering,
201 content = content,
202 )
203 }
204