diff options
4 files changed, 204 insertions, 40 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt index ba80a8deffb7..1a653c316db1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt @@ -34,9 +34,9 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -66,6 +66,7 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @@ -97,7 +98,7 @@ import kotlin.math.pow fun BouncerContent( viewModel: BouncerViewModel, dialogFactory: BouncerDialogFactory, - modifier: Modifier + modifier: Modifier = Modifier, ) { val isFullScreenUserSwitcherEnabled = viewModel.isUserSwitcherVisible val isSideBySideSupported by viewModel.isSideBySideSupported.collectAsState() @@ -142,6 +143,7 @@ private fun StandardLayout( viewModel: BouncerViewModel, dialogFactory: BouncerDialogFactory, modifier: Modifier = Modifier, + layout: BouncerSceneLayout = BouncerSceneLayout.STANDARD, outputOnly: Boolean = false, ) { val foldPosture: FoldPosture by foldPosture() @@ -161,6 +163,7 @@ private fun StandardLayout( FoldSplittable( viewModel = viewModel, dialogFactory = dialogFactory, + layout = layout, outputOnly = outputOnly, isSplit = false, ) @@ -170,6 +173,7 @@ private fun StandardLayout( FoldSplittable( viewModel = viewModel, dialogFactory = dialogFactory, + layout = layout, outputOnly = outputOnly, isSplit = true, ) @@ -193,6 +197,7 @@ private fun StandardLayout( private fun SceneScope.FoldSplittable( viewModel: BouncerViewModel, dialogFactory: BouncerDialogFactory, + layout: BouncerSceneLayout, outputOnly: Boolean, isSplit: Boolean, modifier: Modifier = Modifier, @@ -210,13 +215,21 @@ private fun SceneScope.FoldSplittable( // Content above the fold, when split on a foldable device in a "table top" posture: Box( modifier = - Modifier.element(SceneElements.AboveFold).fillMaxWidth().thenIf(isSplit) { - Modifier.weight(splitRatio) - }, + Modifier.element(SceneElements.AboveFold) + .fillMaxWidth() + .then( + if (isSplit) { + Modifier.weight(splitRatio) + } else if (outputOnly) { + Modifier.fillMaxHeight() + } else { + Modifier + } + ), ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth().padding(top = 92.dp), + modifier = Modifier.fillMaxWidth().padding(top = layout.topPadding), ) { Crossfade( targetState = message, @@ -230,11 +243,23 @@ private fun SceneScope.FoldSplittable( ) } - Spacer(Modifier.heightIn(min = 21.dp, max = 48.dp)) + if (!outputOnly) { + Spacer(Modifier.height(layout.spacingBetweenMessageAndEnteredInput)) + + UserInputArea( + viewModel = viewModel, + visibility = UserInputAreaVisibility.OUTPUT_ONLY, + layout = layout, + ) + } + } + if (outputOnly) { UserInputArea( viewModel = viewModel, visibility = UserInputAreaVisibility.OUTPUT_ONLY, + layout = layout, + modifier = Modifier.align(Alignment.Center), ) } } @@ -242,25 +267,32 @@ private fun SceneScope.FoldSplittable( // Content below the fold, when split on a foldable device in a "table top" posture: Box( modifier = - Modifier.element(SceneElements.BelowFold).fillMaxWidth().thenIf(isSplit) { - Modifier.weight(1 - splitRatio) - }, + Modifier.element(SceneElements.BelowFold) + .fillMaxWidth() + .weight( + if (isSplit) { + 1 - splitRatio + } else { + 1f + } + ), ) { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxSize() ) { if (!outputOnly) { Box(Modifier.weight(1f)) { UserInputArea( viewModel = viewModel, visibility = UserInputAreaVisibility.INPUT_ONLY, - modifier = Modifier.align(Alignment.Center), + layout = layout, + modifier = Modifier.align(Alignment.BottomCenter), ) } } - Spacer(Modifier.heightIn(min = 21.dp, max = 48.dp)) + Spacer(Modifier.height(48.dp)) val actionButtonModifier = Modifier.height(56.dp) @@ -275,7 +307,7 @@ private fun SceneScope.FoldSplittable( } } - Spacer(Modifier.height(48.dp)) + Spacer(Modifier.height(layout.bottomPadding)) } } @@ -311,6 +343,7 @@ private fun SceneScope.FoldSplittable( private fun UserInputArea( viewModel: BouncerViewModel, visibility: UserInputAreaVisibility, + layout: BouncerSceneLayout, modifier: Modifier = Modifier, ) { val authMethodViewModel: AuthMethodBouncerViewModel? by @@ -327,6 +360,7 @@ private fun UserInputArea( UserInputAreaVisibility.INPUT_ONLY -> PinPad( viewModel = nonNullViewModel, + layout = layout, modifier = modifier, ) } @@ -341,7 +375,8 @@ private fun UserInputArea( if (visibility == UserInputAreaVisibility.INPUT_ONLY) { PatternBouncer( viewModel = nonNullViewModel, - modifier = modifier.aspectRatio(1f, matchHeightConstraintsFirst = false) + layout = layout, + modifier = modifier.aspectRatio(1f, matchHeightConstraintsFirst = false), ) } else -> Unit @@ -449,7 +484,7 @@ private fun UserSwitcher( } /** - * Renders the dropdown menu that displays the actual users and/or user actions that can be + * Renders the dropdowm menu that displays the actual users and/or user actions that can be * selected. */ @Composable @@ -519,6 +554,7 @@ private fun SplitLayout( StandardLayout( viewModel = viewModel, dialogFactory = dialogFactory, + layout = BouncerSceneLayout.SPLIT, outputOnly = true, modifier = startContentModifier, ) @@ -527,10 +563,12 @@ private fun SplitLayout( UserInputArea( viewModel = viewModel, visibility = UserInputAreaVisibility.INPUT_ONLY, + layout = BouncerSceneLayout.SPLIT, modifier = endContentModifier, ) }, - modifier = modifier + layout = BouncerSceneLayout.SPLIT, + modifier = modifier, ) } @@ -542,6 +580,7 @@ private fun SplitLayout( private fun SwappableLayout( startContent: @Composable (Modifier) -> Unit, endContent: @Composable (Modifier) -> Unit, + layout: BouncerSceneLayout, modifier: Modifier = Modifier, ) { val layoutDirection = LocalLayoutDirection.current @@ -597,7 +636,7 @@ private fun SwappableLayout( alpha = animatedAlpha(animatedOffset) } ) { - endContent(Modifier.widthIn(max = 400.dp).align(Alignment.BottomCenter)) + endContent(Modifier.align(layout.swappableEndContentAlignment).widthIn(max = 400.dp)) } } } @@ -635,9 +674,11 @@ private fun SideBySideLayout( StandardLayout( viewModel = viewModel, dialogFactory = dialogFactory, + layout = BouncerSceneLayout.SIDE_BY_SIDE, modifier = endContentModifier, ) }, + layout = BouncerSceneLayout.SIDE_BY_SIDE, modifier = modifier, ) } @@ -663,6 +704,7 @@ private fun StackedLayout( StandardLayout( viewModel = viewModel, dialogFactory = dialogFactory, + layout = BouncerSceneLayout.STACKED, modifier = Modifier.fillMaxWidth().weight(1f), ) } @@ -732,3 +774,48 @@ private object SceneElements { private val SceneTransitions = transitions { from(SceneKeys.ContiguousSceneKey, to = SceneKeys.SplitSceneKey) { spec = tween() } } + +/** Whether a more compact size should be used for various spacing dimensions. */ +internal val BouncerSceneLayout.isUseCompactSize: Boolean + get() = + when (this) { + BouncerSceneLayout.SIDE_BY_SIDE -> true + BouncerSceneLayout.SPLIT -> true + else -> false + } + +/** Amount of space to place between the message and the entered input UI elements, in dips. */ +private val BouncerSceneLayout.spacingBetweenMessageAndEnteredInput: Dp + get() = + when { + this == BouncerSceneLayout.STACKED -> 24.dp + isUseCompactSize -> 96.dp + else -> 128.dp + } + +/** Amount of space to place above the topmost UI element, in dips. */ +private val BouncerSceneLayout.topPadding: Dp + get() = + if (this == BouncerSceneLayout.SPLIT) { + 40.dp + } else { + 92.dp + } + +/** Amount of space to place below the bottommost UI element, in dips. */ +private val BouncerSceneLayout.bottomPadding: Dp + get() = + if (this == BouncerSceneLayout.SPLIT) { + 40.dp + } else { + 48.dp + } + +/** The in-a-box alignment for the content on the "end" side of a swappable layout. */ +private val BouncerSceneLayout.swappableEndContentAlignment: Alignment + get() = + if (this == BouncerSceneLayout.SPLIT) { + Alignment.Center + } else { + Alignment.BottomCenter + } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt index eb0688914b9d..cee9fa625c87 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt @@ -18,8 +18,6 @@ package com.android.systemui.bouncer.ui.composable import android.view.ViewTreeObserver import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.LocalTextStyle @@ -123,8 +121,6 @@ internal fun PasswordBouncer( ) }, ) - - Spacer(Modifier.height(100.dp)) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt index ff1cbd6b04c3..a4b195508b3d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt @@ -48,6 +48,7 @@ import androidx.compose.ui.unit.dp import com.android.compose.animation.Easings import com.android.compose.modifiers.thenIf import com.android.internal.R +import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel import kotlin.math.min @@ -64,6 +65,7 @@ import kotlinx.coroutines.launch @Composable internal fun PatternBouncer( viewModel: PatternBouncerViewModel, + layout: BouncerSceneLayout, modifier: Modifier = Modifier, ) { DisposableEffect(Unit) { @@ -190,6 +192,8 @@ internal fun PatternBouncer( // This is the position of the input pointer. var inputPosition: Offset? by remember { mutableStateOf(null) } var gridCoordinates: LayoutCoordinates? by remember { mutableStateOf(null) } + var offset: Offset by remember { mutableStateOf(Offset.Zero) } + var scale: Float by remember { mutableStateOf(1f) } Canvas( modifier @@ -224,21 +228,42 @@ internal fun PatternBouncer( }, ) { change, _ -> inputPosition = change.position - viewModel.onDrag( - xPx = change.position.x, - yPx = change.position.y, - containerSizePx = size.width, - ) + change.position.minus(offset).div(scale).let { + viewModel.onDrag( + xPx = it.x, + yPx = it.y, + containerSizePx = size.width, + ) + } } } } ) { gridCoordinates?.let { nonNullCoordinates -> val containerSize = nonNullCoordinates.size + if (containerSize.width <= 0 || containerSize.height <= 0) { + return@let + } + val horizontalSpacing = containerSize.width.toFloat() / colCount val verticalSpacing = containerSize.height.toFloat() / rowCount val spacing = min(horizontalSpacing, verticalSpacing) - val verticalOffset = containerSize.height - spacing * rowCount + val horizontalOffset = + offset( + availableSize = containerSize.width, + spacingPerDot = spacing, + dotCount = colCount, + isCentered = true, + ) + val verticalOffset = + offset( + availableSize = containerSize.height, + spacingPerDot = spacing, + dotCount = rowCount, + isCentered = layout.isCenteredVertically, + ) + offset = Offset(horizontalOffset, verticalOffset) + scale = (colCount * spacing) / containerSize.width if (isAnimationEnabled) { // Draw lines between dots. @@ -248,8 +273,9 @@ internal fun PatternBouncer( val lineFadeOutAnimationProgress = lineFadeOutAnimatables[previousDot]!!.value val startLerp = 1 - lineFadeOutAnimationProgress - val from = pixelOffset(previousDot, spacing, verticalOffset) - val to = pixelOffset(dot, spacing, verticalOffset) + val from = + pixelOffset(previousDot, spacing, horizontalOffset, verticalOffset) + val to = pixelOffset(dot, spacing, horizontalOffset, verticalOffset) val lerpedFrom = Offset( x = from.x + (to.x - from.x) * startLerp, @@ -270,7 +296,7 @@ internal fun PatternBouncer( // position. inputPosition?.let { lineEnd -> currentDot?.let { dot -> - val from = pixelOffset(dot, spacing, verticalOffset) + val from = pixelOffset(dot, spacing, horizontalOffset, verticalOffset) val lineLength = sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2)) drawLine( @@ -288,7 +314,7 @@ internal fun PatternBouncer( // Draw each dot on the grid. dots.forEach { dot -> drawCircle( - center = pixelOffset(dot, spacing, verticalOffset), + center = pixelOffset(dot, spacing, horizontalOffset, verticalOffset), color = dotColor, radius = dotRadius * (dotScalingAnimatables[dot]?.value ?: 1f), ) @@ -301,10 +327,11 @@ internal fun PatternBouncer( private fun pixelOffset( dot: PatternDotViewModel, spacing: Float, + horizontalOffset: Float, verticalOffset: Float, ): Offset { return Offset( - x = dot.x * spacing + spacing / 2, + x = dot.x * spacing + spacing / 2 + horizontalOffset, y = dot.y * spacing + spacing / 2 + verticalOffset, ) } @@ -371,6 +398,35 @@ private suspend fun showFailureAnimation( } } +/** + * Returns the amount of offset along the axis, in pixels, that should be applied to all dots. + * + * @param availableSize The size of the container, along the axis of interest. + * @param spacingPerDot The amount of pixels that each dot should take (including the area around + * that dot). + * @param dotCount The number of dots along the axis (e.g. if the axis of interest is the + * horizontal/x axis, this is the number of columns in the dot grid). + * @param isCentered Whether the dots should be centered along the axis of interest; if `false`, the + * dots will be pushed towards to end/bottom of the axis. + */ +private fun offset( + availableSize: Int, + spacingPerDot: Float, + dotCount: Int, + isCentered: Boolean = false, +): Float { + val default = availableSize - spacingPerDot * dotCount + return if (isCentered) { + default / 2 + } else { + default + } +} + +/** Whether the UI should be centered vertically. */ +private val BouncerSceneLayout.isCenteredVertically: Boolean + get() = this == BouncerSceneLayout.SPLIT + private const val DOT_DIAMETER_DP = 16 private const val SELECTED_DOT_DIAMETER_DP = 24 private const val SELECTED_DOT_REACTION_ANIMATION_DURATION_MS = 83 diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt index 59617c9022ab..8f5d9f4a1790 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt @@ -52,6 +52,7 @@ import androidx.compose.ui.unit.dp import com.android.compose.animation.Easings import com.android.compose.grid.VerticalGrid import com.android.compose.modifiers.thenIf +import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout import com.android.systemui.bouncer.ui.viewmodel.ActionButtonAppearance import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel import com.android.systemui.common.shared.model.ContentDescription @@ -65,9 +66,11 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch +/** Renders the PIN button pad. */ @Composable fun PinPad( viewModel: PinBouncerViewModel, + layout: BouncerSceneLayout, modifier: Modifier = Modifier, ) { DisposableEffect(Unit) { @@ -92,9 +95,9 @@ fun PinPad( } VerticalGrid( - columns = 3, - verticalSpacing = 12.dp, - horizontalSpacing = 20.dp, + columns = columns, + verticalSpacing = layout.verticalSpacing, + horizontalSpacing = calculateHorizontalSpacingBetweenColumns(layout.gridWidth), modifier = modifier, ) { repeat(9) { index -> @@ -254,7 +257,7 @@ private fun PinPadButton( val cornerRadius: Dp by animateDpAsState( - if (isAnimationEnabled && isPressed) 24.dp else pinButtonSize / 2, + if (isAnimationEnabled && isPressed) 24.dp else pinButtonMaxSize / 2, label = "PinButton round corners", animationSpec = tween(animDurationMillis, easing = animEasing) ) @@ -284,7 +287,7 @@ private fun PinPadButton( contentAlignment = Alignment.Center, modifier = modifier - .sizeIn(maxWidth = pinButtonSize, maxHeight = pinButtonSize) + .sizeIn(maxWidth = pinButtonMaxSize, maxHeight = pinButtonMaxSize) .aspectRatio(1f) .drawBehind { drawRoundRect( @@ -345,10 +348,32 @@ private suspend fun showFailureAnimation( } } -private val pinButtonSize = 84.dp -private val pinButtonErrorShrinkFactor = 67.dp / pinButtonSize +/** Returns the amount of horizontal spacing between columns, in dips. */ +private fun calculateHorizontalSpacingBetweenColumns( + gridWidth: Dp, +): Dp { + return (gridWidth - (pinButtonMaxSize * columns)) / (columns - 1) +} + +/** The width of the grid of PIN pad buttons, in dips. */ +private val BouncerSceneLayout.gridWidth: Dp + get() = if (isUseCompactSize) 292.dp else 300.dp + +/** The spacing between rows of PIN pad buttons, in dips. */ +private val BouncerSceneLayout.verticalSpacing: Dp + get() = if (isUseCompactSize) 8.dp else 12.dp + +/** Number of columns in the PIN pad grid. */ +private const val columns = 3 +/** Maximum size (width and height) of each PIN pad button. */ +private val pinButtonMaxSize = 84.dp +/** Scale factor to apply to buttons when animating the "error" animation on them. */ +private val pinButtonErrorShrinkFactor = 67.dp / pinButtonMaxSize +/** Animation duration of the "shrink" phase of the error animation, on each PIN pad button. */ private const val pinButtonErrorShrinkMs = 50 +/** Amount of time to wait between application of the "error" animation to each row of buttons. */ private const val pinButtonErrorStaggerDelayMs = 33 +/** Animation duration of the "revert" phase of the error animation, on each PIN pad button. */ private const val pinButtonErrorRevertMs = 617 // Pin button motion spec: http://shortn/_9TTIG6SoEa |