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 
19 package com.android.permissioncontroller.permission.ui.wear.elements.layout
20 
21 import androidx.compose.foundation.layout.Arrangement
22 import androidx.compose.foundation.layout.PaddingValues
23 import androidx.compose.runtime.Composable
24 import androidx.compose.runtime.remember
25 import androidx.compose.ui.Alignment
26 import androidx.compose.ui.platform.LocalConfiguration
27 import androidx.compose.ui.platform.LocalDensity
28 import androidx.compose.ui.unit.Dp
29 import androidx.compose.ui.unit.dp
30 import androidx.compose.ui.unit.times
31 import androidx.compose.ui.util.lerp
32 import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults
33 import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType
34 import androidx.wear.compose.foundation.lazy.ScalingParams
35 import androidx.wear.compose.material.ChipDefaults
36 import com.android.permissioncontroller.permission.ui.wear.elements.layout.ScalingLazyColumnState.RotaryMode
37 import kotlin.math.sqrt
38 
39 // This file's content is copied from ScalingLazyColumnDefaults.kt from Horologist (go/horologist),
40 // remove it once after wear compose supports large screen dialogs.
41 
42 /** Default layouts for ScalingLazyColumnState, based on UX guidance. */
43 object ScalingLazyColumnDefaults {
44 
45     /**
46      * Creates a Responsive layout for ScalingLazyColumn. The first and last items will scroll just
47      * onto screen at full size, assuming rounded corners of a Chip.
48      *
49      * @param firstItemIsFullWidth set to false if the first item is small enough to fit at the top,
50      *   however it may be scaled.
51      * @param additionalPaddingAtBottom additional padding at end of content to avoid problem items
52      *   clipping
53      * @param verticalArrangement the ScalingLazyColumn verticalArrangement.
54      * @param horizontalPaddingPercent the amount of horizontal padding as a percent.
55      * @param rotaryMode the rotary handling, such as Fling or Snap.
56      * @param hapticsEnabled whether haptics are enabled.
57      * @param reverseLayout whether to start at the bottom.
58      * @param userScrollEnabled whether to allow user to scroll.
59      */
60     //    @Deprecated("Replaced by rememberResponsiveColumnState")
61 
responsivenull62     fun responsive(
63         firstItemIsFullWidth: Boolean = true,
64         additionalPaddingAtBottom: Dp = 10.dp,
65         verticalArrangement: Arrangement.Vertical =
66             Arrangement.spacedBy(
67                 space = 4.dp,
68                 alignment = Alignment.Top,
69             ),
70         horizontalPaddingPercent: Float = 0.052f,
71         rotaryMode: RotaryMode? = RotaryMode.Scroll,
72         hapticsEnabled: Boolean = true,
73         reverseLayout: Boolean = false,
74         userScrollEnabled: Boolean = true,
75     ): ScalingLazyColumnState.Factory {
76         return object : ScalingLazyColumnState.Factory {
77             @Composable
78             override fun create(): ScalingLazyColumnState {
79                 val density = LocalDensity.current
80                 val configuration = LocalConfiguration.current
81                 val screenWidthDp = configuration.screenWidthDp.toFloat()
82                 val screenHeightDp = configuration.screenHeightDp.toFloat()
83 
84                 return remember {
85                     val padding = screenWidthDp * horizontalPaddingPercent
86                     val topPaddingDp: Dp =
87                         if (firstItemIsFullWidth && configuration.isScreenRound) {
88                             calculateVerticalOffsetForChip(screenWidthDp, horizontalPaddingPercent)
89                         } else {
90                             32.dp
91                         }
92                     val bottomPaddingDp: Dp =
93                         if (configuration.isScreenRound) {
94                             calculateVerticalOffsetForChip(
95                                 screenWidthDp,
96                                 horizontalPaddingPercent,
97                             ) + additionalPaddingAtBottom
98                         } else {
99                             0.dp
100                         }
101                     val contentPadding =
102                         PaddingValues(
103                             start = padding.dp,
104                             end = padding.dp,
105                             top = topPaddingDp,
106                             bottom = bottomPaddingDp,
107                         )
108 
109                     val scalingParams = responsiveScalingParams(screenWidthDp)
110 
111                     val screenHeightPx = with(density) { screenHeightDp.dp.roundToPx() }
112                     val topPaddingPx = with(density) { topPaddingDp.roundToPx() }
113                     val topScreenOffsetPx = screenHeightPx / 2 - topPaddingPx
114 
115                     val initialScrollPosition =
116                         ScalingLazyColumnState.ScrollPosition(
117                             index = 0,
118                             offsetPx = topScreenOffsetPx,
119                         )
120                     ScalingLazyColumnState(
121                         initialScrollPosition = initialScrollPosition,
122                         autoCentering = null,
123                         anchorType = ScalingLazyListAnchorType.ItemStart,
124                         rotaryMode = rotaryMode,
125                         verticalArrangement = verticalArrangement,
126                         horizontalAlignment = Alignment.CenterHorizontally,
127                         contentPadding = contentPadding,
128                         scalingParams = scalingParams,
129                         hapticsEnabled = hapticsEnabled,
130                         reverseLayout = reverseLayout,
131                         userScrollEnabled = userScrollEnabled,
132                     )
133                 }
134             }
135         }
136     }
137 
calculateVerticalOffsetForChipnull138     internal fun calculateVerticalOffsetForChip(
139         viewportDiameter: Float,
140         horizontalPaddingPercent: Float,
141     ): Dp {
142         val childViewHeight: Float = ChipDefaults.Height.value
143         val childViewWidth: Float = viewportDiameter * (1.0f - (2f * horizontalPaddingPercent))
144         val radius = viewportDiameter / 2f
145         return (radius -
146                 sqrt(
147                     (radius - childViewHeight + childViewWidth * 0.5f) *
148                         (radius - childViewWidth * 0.5f),
149                 ) -
150                 childViewHeight * 0.5f)
151             .dp
152     }
153 
responsiveScalingParamsnull154     fun responsiveScalingParams(screenWidthDp: Float): ScalingParams {
155         val sizeRatio = ((screenWidthDp - 192) / (233 - 192).toFloat()).coerceIn(0f, 1.5f)
156         val presetRatio = 0f
157 
158         val minElementHeight = lerp(0.2f, 0.157f, sizeRatio)
159         val maxElementHeight = lerp(0.6f, 0.472f, sizeRatio).coerceAtLeast(minElementHeight)
160         val minTransitionArea = lerp(0.35f, lerp(0.35f, 0.393f, presetRatio), sizeRatio)
161         val maxTransitionArea = lerp(0.55f, lerp(0.55f, 0.593f, presetRatio), sizeRatio)
162 
163         val scalingParams =
164             ScalingLazyColumnDefaults.scalingParams(
165                 minElementHeight = minElementHeight,
166                 maxElementHeight = maxElementHeight,
167                 minTransitionArea = minTransitionArea,
168                 maxTransitionArea = maxTransitionArea,
169             )
170         return scalingParams
171     }
172 
173     internal val Padding12Pct = 0.1248f
174     internal val Padding16Pct = 0.1664f
175     internal val Padding20Pct = 0.2083f
176     internal val Padding21Pct = 0.2188f
177     internal val Padding31Pct = 0.3646f
178 
179     enum class ItemType(
180         val topPaddingDp: Float,
181         val bottomPaddingDp: Float,
182         val paddingCorrection: Dp = 0.dp,
183     ) {
184         Card(Padding21Pct, Padding31Pct),
185         Chip(Padding21Pct, Padding31Pct),
186         CompactChip(
187             topPaddingDp = Padding12Pct,
188             bottomPaddingDp = Padding20Pct,
189             paddingCorrection = (-8).dp,
190         ),
191         Icon(Padding12Pct, Padding21Pct),
192         MultiButton(Padding21Pct, Padding20Pct),
193         SingleButton(Padding12Pct, Padding20Pct),
194         Text(Padding21Pct, Padding31Pct),
195         Unspecified(0f, 0f),
196     }
197 
198     @Composable
paddingnull199     fun padding(
200         first: ItemType = ItemType.Unspecified,
201         last: ItemType = ItemType.Unspecified,
202         horizontalPercent: Float = 0.052f,
203     ): @Composable () -> PaddingValues {
204         val configuration = LocalConfiguration.current
205         val screenWidthDp = configuration.screenWidthDp.toFloat()
206         val screenHeightDp = configuration.screenHeightDp.toFloat()
207 
208         return {
209             val height = screenHeightDp.dp
210             val horizontalPadding = screenWidthDp.dp * horizontalPercent
211 
212             val topPadding =
213                 if (first != ItemType.Unspecified) {
214                     first.topPaddingDp * height + first.paddingCorrection
215                 } else {
216                     if (configuration.isScreenRound) {
217                         calculateVerticalOffsetForChip(screenWidthDp, horizontalPercent)
218                     } else {
219                         32.dp
220                     }
221                 }
222 
223             val bottomPadding =
224                 if (last != ItemType.Unspecified) {
225                     last.bottomPaddingDp * height + first.paddingCorrection
226                 } else {
227                     if (configuration.isScreenRound) {
228                         calculateVerticalOffsetForChip(
229                             screenWidthDp,
230                             horizontalPercent,
231                         ) + 10.dp
232                     } else {
233                         0.dp
234                     }
235                 }
236 
237             PaddingValues(
238                 top = topPadding,
239                 bottom = bottomPadding,
240                 start = horizontalPadding,
241                 end = horizontalPadding,
242             )
243         }
244     }
245 }
246