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