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