Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d01a63cb authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[flexiglass] UX polish for bouncer scene: PIN and Pattern

Really brings the UI into spec, especially regarding sizing and spacing
of various elements.

Does so for PIN and Pattern, a little for password - but that requires
more work.

Fix: 300677757
Bug: 282730134
Bug: 281871687
Test: screenshot tests in companion vendor CL
Test: manually verified inner, outer, landscape/portrait,
unfolded/half-foded on foldable hardware
Flag: ACONFIG com.android.systemui.scene_container DEVELOPMENT

Change-Id: I1aa5e3e63d888bad0e63c07425f2a2a2f723f761
parent c06da33a
Loading
Loading
Loading
Loading
+105 −18
Original line number Original line Diff line number Diff line
@@ -34,9 +34,9 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
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.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.dp
@@ -97,7 +98,7 @@ import kotlin.math.pow
fun BouncerContent(
fun BouncerContent(
    viewModel: BouncerViewModel,
    viewModel: BouncerViewModel,
    dialogFactory: BouncerDialogFactory,
    dialogFactory: BouncerDialogFactory,
    modifier: Modifier
    modifier: Modifier = Modifier,
) {
) {
    val isFullScreenUserSwitcherEnabled = viewModel.isUserSwitcherVisible
    val isFullScreenUserSwitcherEnabled = viewModel.isUserSwitcherVisible
    val isSideBySideSupported by viewModel.isSideBySideSupported.collectAsState()
    val isSideBySideSupported by viewModel.isSideBySideSupported.collectAsState()
@@ -142,6 +143,7 @@ private fun StandardLayout(
    viewModel: BouncerViewModel,
    viewModel: BouncerViewModel,
    dialogFactory: BouncerDialogFactory,
    dialogFactory: BouncerDialogFactory,
    modifier: Modifier = Modifier,
    modifier: Modifier = Modifier,
    layout: BouncerSceneLayout = BouncerSceneLayout.STANDARD,
    outputOnly: Boolean = false,
    outputOnly: Boolean = false,
) {
) {
    val foldPosture: FoldPosture by foldPosture()
    val foldPosture: FoldPosture by foldPosture()
@@ -161,6 +163,7 @@ private fun StandardLayout(
            FoldSplittable(
            FoldSplittable(
                viewModel = viewModel,
                viewModel = viewModel,
                dialogFactory = dialogFactory,
                dialogFactory = dialogFactory,
                layout = layout,
                outputOnly = outputOnly,
                outputOnly = outputOnly,
                isSplit = false,
                isSplit = false,
            )
            )
@@ -170,6 +173,7 @@ private fun StandardLayout(
            FoldSplittable(
            FoldSplittable(
                viewModel = viewModel,
                viewModel = viewModel,
                dialogFactory = dialogFactory,
                dialogFactory = dialogFactory,
                layout = layout,
                outputOnly = outputOnly,
                outputOnly = outputOnly,
                isSplit = true,
                isSplit = true,
            )
            )
@@ -193,6 +197,7 @@ private fun StandardLayout(
private fun SceneScope.FoldSplittable(
private fun SceneScope.FoldSplittable(
    viewModel: BouncerViewModel,
    viewModel: BouncerViewModel,
    dialogFactory: BouncerDialogFactory,
    dialogFactory: BouncerDialogFactory,
    layout: BouncerSceneLayout,
    outputOnly: Boolean,
    outputOnly: Boolean,
    isSplit: Boolean,
    isSplit: Boolean,
    modifier: Modifier = Modifier,
    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:
        // Content above the fold, when split on a foldable device in a "table top" posture:
        Box(
        Box(
            modifier =
            modifier =
                Modifier.element(SceneElements.AboveFold).fillMaxWidth().thenIf(isSplit) {
                Modifier.element(SceneElements.AboveFold)
                    .fillMaxWidth()
                    .then(
                        if (isSplit) {
                            Modifier.weight(splitRatio)
                            Modifier.weight(splitRatio)
                },
                        } else if (outputOnly) {
                            Modifier.fillMaxHeight()
                        } else {
                            Modifier
                        }
                    ),
        ) {
        ) {
            Column(
            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier.fillMaxWidth().padding(top = 92.dp),
                modifier = Modifier.fillMaxWidth().padding(top = layout.topPadding),
            ) {
            ) {
                Crossfade(
                Crossfade(
                    targetState = message,
                    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(
                    UserInputArea(
                        viewModel = viewModel,
                        viewModel = viewModel,
                        visibility = UserInputAreaVisibility.OUTPUT_ONLY,
                        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:
        // Content below the fold, when split on a foldable device in a "table top" posture:
        Box(
        Box(
            modifier =
            modifier =
                Modifier.element(SceneElements.BelowFold).fillMaxWidth().thenIf(isSplit) {
                Modifier.element(SceneElements.BelowFold)
                    Modifier.weight(1 - splitRatio)
                    .fillMaxWidth()
                },
                    .weight(
                        if (isSplit) {
                            1 - splitRatio
                        } else {
                            1f
                        }
                    ),
        ) {
        ) {
            Column(
            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier.fillMaxWidth(),
                modifier = Modifier.fillMaxSize()
            ) {
            ) {
                if (!outputOnly) {
                if (!outputOnly) {
                    Box(Modifier.weight(1f)) {
                    Box(Modifier.weight(1f)) {
                        UserInputArea(
                        UserInputArea(
                            viewModel = viewModel,
                            viewModel = viewModel,
                            visibility = UserInputAreaVisibility.INPUT_ONLY,
                            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)
                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(
private fun UserInputArea(
    viewModel: BouncerViewModel,
    viewModel: BouncerViewModel,
    visibility: UserInputAreaVisibility,
    visibility: UserInputAreaVisibility,
    layout: BouncerSceneLayout,
    modifier: Modifier = Modifier,
    modifier: Modifier = Modifier,
) {
) {
    val authMethodViewModel: AuthMethodBouncerViewModel? by
    val authMethodViewModel: AuthMethodBouncerViewModel? by
@@ -327,6 +360,7 @@ private fun UserInputArea(
                UserInputAreaVisibility.INPUT_ONLY ->
                UserInputAreaVisibility.INPUT_ONLY ->
                    PinPad(
                    PinPad(
                        viewModel = nonNullViewModel,
                        viewModel = nonNullViewModel,
                        layout = layout,
                        modifier = modifier,
                        modifier = modifier,
                    )
                    )
            }
            }
@@ -341,7 +375,8 @@ private fun UserInputArea(
            if (visibility == UserInputAreaVisibility.INPUT_ONLY) {
            if (visibility == UserInputAreaVisibility.INPUT_ONLY) {
                PatternBouncer(
                PatternBouncer(
                    viewModel = nonNullViewModel,
                    viewModel = nonNullViewModel,
                    modifier = modifier.aspectRatio(1f, matchHeightConstraintsFirst = false)
                    layout = layout,
                    modifier = modifier.aspectRatio(1f, matchHeightConstraintsFirst = false),
                )
                )
            }
            }
        else -> Unit
        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.
 * selected.
 */
 */
@Composable
@Composable
@@ -519,6 +554,7 @@ private fun SplitLayout(
            StandardLayout(
            StandardLayout(
                viewModel = viewModel,
                viewModel = viewModel,
                dialogFactory = dialogFactory,
                dialogFactory = dialogFactory,
                layout = BouncerSceneLayout.SPLIT,
                outputOnly = true,
                outputOnly = true,
                modifier = startContentModifier,
                modifier = startContentModifier,
            )
            )
@@ -527,10 +563,12 @@ private fun SplitLayout(
            UserInputArea(
            UserInputArea(
                viewModel = viewModel,
                viewModel = viewModel,
                visibility = UserInputAreaVisibility.INPUT_ONLY,
                visibility = UserInputAreaVisibility.INPUT_ONLY,
                layout = BouncerSceneLayout.SPLIT,
                modifier = endContentModifier,
                modifier = endContentModifier,
            )
            )
        },
        },
        modifier = modifier
        layout = BouncerSceneLayout.SPLIT,
        modifier = modifier,
    )
    )
}
}


@@ -542,6 +580,7 @@ private fun SplitLayout(
private fun SwappableLayout(
private fun SwappableLayout(
    startContent: @Composable (Modifier) -> Unit,
    startContent: @Composable (Modifier) -> Unit,
    endContent: @Composable (Modifier) -> Unit,
    endContent: @Composable (Modifier) -> Unit,
    layout: BouncerSceneLayout,
    modifier: Modifier = Modifier,
    modifier: Modifier = Modifier,
) {
) {
    val layoutDirection = LocalLayoutDirection.current
    val layoutDirection = LocalLayoutDirection.current
@@ -597,7 +636,7 @@ private fun SwappableLayout(
                    alpha = animatedAlpha(animatedOffset)
                    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(
            StandardLayout(
                viewModel = viewModel,
                viewModel = viewModel,
                dialogFactory = dialogFactory,
                dialogFactory = dialogFactory,
                layout = BouncerSceneLayout.SIDE_BY_SIDE,
                modifier = endContentModifier,
                modifier = endContentModifier,
            )
            )
        },
        },
        layout = BouncerSceneLayout.SIDE_BY_SIDE,
        modifier = modifier,
        modifier = modifier,
    )
    )
}
}
@@ -663,6 +704,7 @@ private fun StackedLayout(
        StandardLayout(
        StandardLayout(
            viewModel = viewModel,
            viewModel = viewModel,
            dialogFactory = dialogFactory,
            dialogFactory = dialogFactory,
            layout = BouncerSceneLayout.STACKED,
            modifier = Modifier.fillMaxWidth().weight(1f),
            modifier = Modifier.fillMaxWidth().weight(1f),
        )
        )
    }
    }
@@ -732,3 +774,48 @@ private object SceneElements {
private val SceneTransitions = transitions {
private val SceneTransitions = transitions {
    from(SceneKeys.ContiguousSceneKey, to = SceneKeys.SplitSceneKey) { spec = tween() }
    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
        }
+0 −4
Original line number Original line Diff line number Diff line
@@ -18,8 +18,6 @@ package com.android.systemui.bouncer.ui.composable


import android.view.ViewTreeObserver
import android.view.ViewTreeObserver
import androidx.compose.foundation.layout.Column
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.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.LocalTextStyle
@@ -123,8 +121,6 @@ internal fun PasswordBouncer(
                        )
                        )
                    },
                    },
        )
        )

        Spacer(Modifier.height(100.dp))
    }
    }
}
}


+67 −11
Original line number Original line Diff line number Diff line
@@ -48,6 +48,7 @@ import androidx.compose.ui.unit.dp
import com.android.compose.animation.Easings
import com.android.compose.animation.Easings
import com.android.compose.modifiers.thenIf
import com.android.compose.modifiers.thenIf
import com.android.internal.R
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.PatternBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
import kotlin.math.min
import kotlin.math.min
@@ -64,6 +65,7 @@ import kotlinx.coroutines.launch
@Composable
@Composable
internal fun PatternBouncer(
internal fun PatternBouncer(
    viewModel: PatternBouncerViewModel,
    viewModel: PatternBouncerViewModel,
    layout: BouncerSceneLayout,
    modifier: Modifier = Modifier,
    modifier: Modifier = Modifier,
) {
) {
    DisposableEffect(Unit) {
    DisposableEffect(Unit) {
@@ -190,6 +192,8 @@ internal fun PatternBouncer(
    // This is the position of the input pointer.
    // This is the position of the input pointer.
    var inputPosition: Offset? by remember { mutableStateOf(null) }
    var inputPosition: Offset? by remember { mutableStateOf(null) }
    var gridCoordinates: LayoutCoordinates? 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(
    Canvas(
        modifier
        modifier
@@ -224,21 +228,42 @@ internal fun PatternBouncer(
                            },
                            },
                        ) { change, _ ->
                        ) { change, _ ->
                            inputPosition = change.position
                            inputPosition = change.position
                            change.position.minus(offset).div(scale).let {
                                viewModel.onDrag(
                                viewModel.onDrag(
                                xPx = change.position.x,
                                    xPx = it.x,
                                yPx = change.position.y,
                                    yPx = it.y,
                                    containerSizePx = size.width,
                                    containerSizePx = size.width,
                                )
                                )
                            }
                            }
                        }
                        }
                    }
                    }
            }
    ) {
    ) {
        gridCoordinates?.let { nonNullCoordinates ->
        gridCoordinates?.let { nonNullCoordinates ->
            val containerSize = nonNullCoordinates.size
            val containerSize = nonNullCoordinates.size
            if (containerSize.width <= 0 || containerSize.height <= 0) {
                return@let
            }

            val horizontalSpacing = containerSize.width.toFloat() / colCount
            val horizontalSpacing = containerSize.width.toFloat() / colCount
            val verticalSpacing = containerSize.height.toFloat() / rowCount
            val verticalSpacing = containerSize.height.toFloat() / rowCount
            val spacing = min(horizontalSpacing, verticalSpacing)
            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) {
            if (isAnimationEnabled) {
                // Draw lines between dots.
                // Draw lines between dots.
@@ -248,8 +273,9 @@ internal fun PatternBouncer(
                        val lineFadeOutAnimationProgress =
                        val lineFadeOutAnimationProgress =
                            lineFadeOutAnimatables[previousDot]!!.value
                            lineFadeOutAnimatables[previousDot]!!.value
                        val startLerp = 1 - lineFadeOutAnimationProgress
                        val startLerp = 1 - lineFadeOutAnimationProgress
                        val from = pixelOffset(previousDot, spacing, verticalOffset)
                        val from =
                        val to = pixelOffset(dot, spacing, verticalOffset)
                            pixelOffset(previousDot, spacing, horizontalOffset, verticalOffset)
                        val to = pixelOffset(dot, spacing, horizontalOffset, verticalOffset)
                        val lerpedFrom =
                        val lerpedFrom =
                            Offset(
                            Offset(
                                x = from.x + (to.x - from.x) * startLerp,
                                x = from.x + (to.x - from.x) * startLerp,
@@ -270,7 +296,7 @@ internal fun PatternBouncer(
                // position.
                // position.
                inputPosition?.let { lineEnd ->
                inputPosition?.let { lineEnd ->
                    currentDot?.let { dot ->
                    currentDot?.let { dot ->
                        val from = pixelOffset(dot, spacing, verticalOffset)
                        val from = pixelOffset(dot, spacing, horizontalOffset, verticalOffset)
                        val lineLength =
                        val lineLength =
                            sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2))
                            sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2))
                        drawLine(
                        drawLine(
@@ -288,7 +314,7 @@ internal fun PatternBouncer(
            // Draw each dot on the grid.
            // Draw each dot on the grid.
            dots.forEach { dot ->
            dots.forEach { dot ->
                drawCircle(
                drawCircle(
                    center = pixelOffset(dot, spacing, verticalOffset),
                    center = pixelOffset(dot, spacing, horizontalOffset, verticalOffset),
                    color = dotColor,
                    color = dotColor,
                    radius = dotRadius * (dotScalingAnimatables[dot]?.value ?: 1f),
                    radius = dotRadius * (dotScalingAnimatables[dot]?.value ?: 1f),
                )
                )
@@ -301,10 +327,11 @@ internal fun PatternBouncer(
private fun pixelOffset(
private fun pixelOffset(
    dot: PatternDotViewModel,
    dot: PatternDotViewModel,
    spacing: Float,
    spacing: Float,
    horizontalOffset: Float,
    verticalOffset: Float,
    verticalOffset: Float,
): Offset {
): Offset {
    return Offset(
    return Offset(
        x = dot.x * spacing + spacing / 2,
        x = dot.x * spacing + spacing / 2 + horizontalOffset,
        y = dot.y * spacing + spacing / 2 + verticalOffset,
        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 DOT_DIAMETER_DP = 16
private const val SELECTED_DOT_DIAMETER_DP = 24
private const val SELECTED_DOT_DIAMETER_DP = 24
private const val SELECTED_DOT_REACTION_ANIMATION_DURATION_MS = 83
private const val SELECTED_DOT_REACTION_ANIMATION_DURATION_MS = 83
+32 −7
Original line number Original line Diff line number Diff line
@@ -52,6 +52,7 @@ import androidx.compose.ui.unit.dp
import com.android.compose.animation.Easings
import com.android.compose.animation.Easings
import com.android.compose.grid.VerticalGrid
import com.android.compose.grid.VerticalGrid
import com.android.compose.modifiers.thenIf
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.ActionButtonAppearance
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.ContentDescription
@@ -65,9 +66,11 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch


/** Renders the PIN button pad. */
@Composable
@Composable
fun PinPad(
fun PinPad(
    viewModel: PinBouncerViewModel,
    viewModel: PinBouncerViewModel,
    layout: BouncerSceneLayout,
    modifier: Modifier = Modifier,
    modifier: Modifier = Modifier,
) {
) {
    DisposableEffect(Unit) {
    DisposableEffect(Unit) {
@@ -92,9 +95,9 @@ fun PinPad(
    }
    }


    VerticalGrid(
    VerticalGrid(
        columns = 3,
        columns = columns,
        verticalSpacing = 12.dp,
        verticalSpacing = layout.verticalSpacing,
        horizontalSpacing = 20.dp,
        horizontalSpacing = calculateHorizontalSpacingBetweenColumns(layout.gridWidth),
        modifier = modifier,
        modifier = modifier,
    ) {
    ) {
        repeat(9) { index ->
        repeat(9) { index ->
@@ -254,7 +257,7 @@ private fun PinPadButton(


    val cornerRadius: Dp by
    val cornerRadius: Dp by
        animateDpAsState(
        animateDpAsState(
            if (isAnimationEnabled && isPressed) 24.dp else pinButtonSize / 2,
            if (isAnimationEnabled && isPressed) 24.dp else pinButtonMaxSize / 2,
            label = "PinButton round corners",
            label = "PinButton round corners",
            animationSpec = tween(animDurationMillis, easing = animEasing)
            animationSpec = tween(animDurationMillis, easing = animEasing)
        )
        )
@@ -284,7 +287,7 @@ private fun PinPadButton(
        contentAlignment = Alignment.Center,
        contentAlignment = Alignment.Center,
        modifier =
        modifier =
            modifier
            modifier
                .sizeIn(maxWidth = pinButtonSize, maxHeight = pinButtonSize)
                .sizeIn(maxWidth = pinButtonMaxSize, maxHeight = pinButtonMaxSize)
                .aspectRatio(1f)
                .aspectRatio(1f)
                .drawBehind {
                .drawBehind {
                    drawRoundRect(
                    drawRoundRect(
@@ -345,10 +348,32 @@ private suspend fun showFailureAnimation(
    }
    }
}
}


private val pinButtonSize = 84.dp
/** Returns the amount of horizontal spacing between columns, in dips. */
private val pinButtonErrorShrinkFactor = 67.dp / pinButtonSize
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
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
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
private const val pinButtonErrorRevertMs = 617


// Pin button motion spec: http://shortn/_9TTIG6SoEa
// Pin button motion spec: http://shortn/_9TTIG6SoEa