Loading packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt +37 −1 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ package com.android.systemui.bouncer.ui.composable import android.app.AlertDialog import android.content.DialogInterface import androidx.compose.animation.Crossfade import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween Loading Loading @@ -54,6 +55,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf Loading @@ -66,6 +68,7 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp Loading @@ -75,6 +78,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.times import com.android.compose.PlatformButton import com.android.compose.animation.Easings import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneScope Loading Loading @@ -665,10 +669,42 @@ private fun ActionArea( modifier: Modifier = Modifier, ) { val actionButton: BouncerActionButtonModel? by viewModel.actionButton.collectAsState() val appearFadeInAnimatable = remember { Animatable(0f) } val appearMoveAnimatable = remember { Animatable(0f) } val appearAnimationInitialOffset = with(LocalDensity.current) { 80.dp.toPx() } actionButton?.let { actionButtonViewModel -> LaunchedEffect(Unit) { appearFadeInAnimatable.animateTo( targetValue = 1f, animationSpec = tween( durationMillis = 450, delayMillis = 133, easing = Easings.LegacyDecelerate, ) ) } LaunchedEffect(Unit) { appearMoveAnimatable.animateTo( targetValue = 1f, animationSpec = tween( durationMillis = 450, delayMillis = 133, easing = Easings.StandardDecelerate, ) ) } Box( modifier = modifier, modifier = modifier.graphicsLayer { // Translate the button up from an initially pushed-down position: translationY = (1 - appearMoveAnimatable.value) * appearAnimationInitialOffset // Fade the button in: alpha = appearFadeInAnimatable.value }, ) { Button( onClick = actionButtonViewModel.onClick, Loading packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt +85 −6 Original line number Diff line number Diff line Loading @@ -52,6 +52,7 @@ import com.android.compose.modifiers.thenIf import com.android.internal.R import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel import kotlin.math.max import kotlin.math.min import kotlin.math.pow import kotlin.math.sqrt Loading @@ -72,15 +73,17 @@ internal fun PatternBouncer( centerDotsVertically: Boolean, modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() val density = LocalDensity.current DisposableEffect(Unit) { onDispose { viewModel.onHidden() } } val colCount = viewModel.columnCount val rowCount = viewModel.rowCount val dotColor = MaterialTheme.colorScheme.secondary val dotRadius = with(LocalDensity.current) { (DOT_DIAMETER_DP / 2).dp.toPx() } val dotRadius = with(density) { (DOT_DIAMETER_DP / 2).dp.toPx() } val lineColor = MaterialTheme.colorScheme.primary val lineStrokeWidth = with(LocalDensity.current) { LINE_STROKE_WIDTH_DP.dp.toPx() } val lineStrokeWidth = with(density) { LINE_STROKE_WIDTH_DP.dp.toPx() } // All dots that should be rendered on the grid. val dots: List<PatternDotViewModel> by viewModel.dots.collectAsState() Loading @@ -101,7 +104,70 @@ internal fun PatternBouncer( integerResource(R.integer.lock_pattern_line_fade_out_duration) val lineFadeOutAnimationDelayMs = integerResource(R.integer.lock_pattern_line_fade_out_delay) val scope = rememberCoroutineScope() val dotAppearFadeInAnimatables = remember(dots) { dots.associateWith { Animatable(0f) } } val dotAppearMoveUpAnimatables = remember(dots) { dots.associateWith { Animatable(0f) } } val dotAppearMaxOffsetPixels = remember(dots) { dots.associateWith { dot -> with(density) { (80 + (20 * dot.y)).dp.toPx() } } } val dotAppearScaleAnimatables = remember(dots) { dots.associateWith { Animatable(0f) } } LaunchedEffect(Unit) { dotAppearFadeInAnimatables.forEach { (dot, animatable) -> scope.launch { // Maps a dot at x and y to an ordinal number to denote the order in which all dots // are visited by the fade-in animation. // // The order is basically starting from the top-left most dot (at 0,0) and ending at // the bottom-right most dot (at 2,2). The visitation order happens // diagonal-by-diagonal. Here's a visual representation of the expected output: // [0][1][3] // [2][4][6] // [5][7][8] // // There's an assumption here that the grid is 3x3. If it's not, this formula needs // to be revisited. check(viewModel.columnCount == 3 && viewModel.rowCount == 3) val staggerOrder = max(0, min(8, 2 * (dot.x + dot.y) + (dot.y - 1))) animatable.animateTo( targetValue = 1f, animationSpec = tween( delayMillis = 33 * staggerOrder, durationMillis = 450, easing = Easings.LegacyDecelerate, ) ) } } dotAppearMoveUpAnimatables.forEach { (dot, animatable) -> scope.launch { animatable.animateTo( targetValue = 1f, animationSpec = tween( delayMillis = 0, durationMillis = 450 + (33 * dot.y), easing = Easings.StandardDecelerate, ) ) } } dotAppearScaleAnimatables.forEach { (dot, animatable) -> scope.launch { animatable.animateTo( targetValue = 1f, animationSpec = tween( delayMillis = 33 * dot.y, durationMillis = 450, easing = Easings.LegacyDecelerate, ) ) } } } val view = LocalView.current // When the current dot is changed, we need to update our animations. Loading Loading @@ -322,10 +388,23 @@ internal fun PatternBouncer( // Draw each dot on the grid. dots.forEach { dot -> val initialOffset = checkNotNull(dotAppearMaxOffsetPixels[dot]) val appearOffset = (1 - checkNotNull(dotAppearMoveUpAnimatables[dot]).value) * initialOffset drawCircle( center = pixelOffset(dot, spacing, horizontalOffset, verticalOffset), color = dotColor, radius = dotRadius * (dotScalingAnimatables[dot]?.value ?: 1f), center = pixelOffset( dot, spacing, horizontalOffset, verticalOffset + appearOffset, ), color = dotColor.copy(alpha = checkNotNull(dotAppearFadeInAnimatables[dot]).value), radius = dotRadius * checkNotNull(dotScalingAnimatables[dot]).value * checkNotNull(dotAppearScaleAnimatables[dot]).value, ) } } Loading Loading
packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt +37 −1 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ package com.android.systemui.bouncer.ui.composable import android.app.AlertDialog import android.content.DialogInterface import androidx.compose.animation.Crossfade import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween Loading Loading @@ -54,6 +55,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf Loading @@ -66,6 +68,7 @@ import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp Loading @@ -75,6 +78,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.times import com.android.compose.PlatformButton import com.android.compose.animation.Easings import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneKey import com.android.compose.animation.scene.SceneScope Loading Loading @@ -665,10 +669,42 @@ private fun ActionArea( modifier: Modifier = Modifier, ) { val actionButton: BouncerActionButtonModel? by viewModel.actionButton.collectAsState() val appearFadeInAnimatable = remember { Animatable(0f) } val appearMoveAnimatable = remember { Animatable(0f) } val appearAnimationInitialOffset = with(LocalDensity.current) { 80.dp.toPx() } actionButton?.let { actionButtonViewModel -> LaunchedEffect(Unit) { appearFadeInAnimatable.animateTo( targetValue = 1f, animationSpec = tween( durationMillis = 450, delayMillis = 133, easing = Easings.LegacyDecelerate, ) ) } LaunchedEffect(Unit) { appearMoveAnimatable.animateTo( targetValue = 1f, animationSpec = tween( durationMillis = 450, delayMillis = 133, easing = Easings.StandardDecelerate, ) ) } Box( modifier = modifier, modifier = modifier.graphicsLayer { // Translate the button up from an initially pushed-down position: translationY = (1 - appearMoveAnimatable.value) * appearAnimationInitialOffset // Fade the button in: alpha = appearFadeInAnimatable.value }, ) { Button( onClick = actionButtonViewModel.onClick, Loading
packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt +85 −6 Original line number Diff line number Diff line Loading @@ -52,6 +52,7 @@ import com.android.compose.modifiers.thenIf import com.android.internal.R import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel import kotlin.math.max import kotlin.math.min import kotlin.math.pow import kotlin.math.sqrt Loading @@ -72,15 +73,17 @@ internal fun PatternBouncer( centerDotsVertically: Boolean, modifier: Modifier = Modifier, ) { val scope = rememberCoroutineScope() val density = LocalDensity.current DisposableEffect(Unit) { onDispose { viewModel.onHidden() } } val colCount = viewModel.columnCount val rowCount = viewModel.rowCount val dotColor = MaterialTheme.colorScheme.secondary val dotRadius = with(LocalDensity.current) { (DOT_DIAMETER_DP / 2).dp.toPx() } val dotRadius = with(density) { (DOT_DIAMETER_DP / 2).dp.toPx() } val lineColor = MaterialTheme.colorScheme.primary val lineStrokeWidth = with(LocalDensity.current) { LINE_STROKE_WIDTH_DP.dp.toPx() } val lineStrokeWidth = with(density) { LINE_STROKE_WIDTH_DP.dp.toPx() } // All dots that should be rendered on the grid. val dots: List<PatternDotViewModel> by viewModel.dots.collectAsState() Loading @@ -101,7 +104,70 @@ internal fun PatternBouncer( integerResource(R.integer.lock_pattern_line_fade_out_duration) val lineFadeOutAnimationDelayMs = integerResource(R.integer.lock_pattern_line_fade_out_delay) val scope = rememberCoroutineScope() val dotAppearFadeInAnimatables = remember(dots) { dots.associateWith { Animatable(0f) } } val dotAppearMoveUpAnimatables = remember(dots) { dots.associateWith { Animatable(0f) } } val dotAppearMaxOffsetPixels = remember(dots) { dots.associateWith { dot -> with(density) { (80 + (20 * dot.y)).dp.toPx() } } } val dotAppearScaleAnimatables = remember(dots) { dots.associateWith { Animatable(0f) } } LaunchedEffect(Unit) { dotAppearFadeInAnimatables.forEach { (dot, animatable) -> scope.launch { // Maps a dot at x and y to an ordinal number to denote the order in which all dots // are visited by the fade-in animation. // // The order is basically starting from the top-left most dot (at 0,0) and ending at // the bottom-right most dot (at 2,2). The visitation order happens // diagonal-by-diagonal. Here's a visual representation of the expected output: // [0][1][3] // [2][4][6] // [5][7][8] // // There's an assumption here that the grid is 3x3. If it's not, this formula needs // to be revisited. check(viewModel.columnCount == 3 && viewModel.rowCount == 3) val staggerOrder = max(0, min(8, 2 * (dot.x + dot.y) + (dot.y - 1))) animatable.animateTo( targetValue = 1f, animationSpec = tween( delayMillis = 33 * staggerOrder, durationMillis = 450, easing = Easings.LegacyDecelerate, ) ) } } dotAppearMoveUpAnimatables.forEach { (dot, animatable) -> scope.launch { animatable.animateTo( targetValue = 1f, animationSpec = tween( delayMillis = 0, durationMillis = 450 + (33 * dot.y), easing = Easings.StandardDecelerate, ) ) } } dotAppearScaleAnimatables.forEach { (dot, animatable) -> scope.launch { animatable.animateTo( targetValue = 1f, animationSpec = tween( delayMillis = 33 * dot.y, durationMillis = 450, easing = Easings.LegacyDecelerate, ) ) } } } val view = LocalView.current // When the current dot is changed, we need to update our animations. Loading Loading @@ -322,10 +388,23 @@ internal fun PatternBouncer( // Draw each dot on the grid. dots.forEach { dot -> val initialOffset = checkNotNull(dotAppearMaxOffsetPixels[dot]) val appearOffset = (1 - checkNotNull(dotAppearMoveUpAnimatables[dot]).value) * initialOffset drawCircle( center = pixelOffset(dot, spacing, horizontalOffset, verticalOffset), color = dotColor, radius = dotRadius * (dotScalingAnimatables[dot]?.value ?: 1f), center = pixelOffset( dot, spacing, horizontalOffset, verticalOffset + appearOffset, ), color = dotColor.copy(alpha = checkNotNull(dotAppearFadeInAnimatables[dot]).value), radius = dotRadius * checkNotNull(dotScalingAnimatables[dot]).value * checkNotNull(dotAppearScaleAnimatables[dot]).value, ) } } Loading