[flexiglass] Pin bouncer UX polish. am: 2515b7efaf am: c4ea06e7c3

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23284679

Change-Id: I7fb2312b2c874c102348f0c83e18f13907ec4be1
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Mike Schneider
2023-05-26 01:34:29 +00:00
committed by Automerger Merge Worker
2 changed files with 133 additions and 15 deletions

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.compose.animation
import androidx.compose.animation.core.Easing
import androidx.core.animation.Interpolator
import com.android.app.animation.InterpolatorsAndroidX
/**
* Compose-compatible definition of Android motion eases, see
* https://carbon.googleplex.com/android-motion/pages/easing
*/
object Easings {
/** The standard interpolator that should be used on every normal animation */
val StandardEasing = fromInterpolator(InterpolatorsAndroidX.STANDARD)
/**
* The standard accelerating interpolator that should be used on every regular movement of
* content that is disappearing e.g. when moving off screen.
*/
val StandardAccelerateEasing = fromInterpolator(InterpolatorsAndroidX.STANDARD_ACCELERATE)
/**
* The standard decelerating interpolator that should be used on every regular movement of
* content that is appearing e.g. when coming from off screen.
*/
val StandardDecelerateEasing = fromInterpolator(InterpolatorsAndroidX.STANDARD_DECELERATE)
/** The default emphasized interpolator. Used for hero / emphasized movement of content. */
val EmphasizedEasing = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED)
/**
* The accelerated emphasized interpolator. Used for hero / emphasized movement of content that
* is disappearing e.g. when moving off screen.
*/
val EmphasizedAccelerateEasing = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED_ACCELERATE)
/**
* The decelerating emphasized interpolator. Used for hero / emphasized movement of content that
* is appearing e.g. when coming from off screen
*/
val EmphasizedDecelerateEasing = fromInterpolator(InterpolatorsAndroidX.EMPHASIZED_DECELERATE)
/** The linear interpolator. */
val LinearEasing = fromInterpolator(InterpolatorsAndroidX.LINEAR)
private fun fromInterpolator(source: Interpolator) = Easing { x -> source.getInterpolation(x) }
}

View File

@@ -18,11 +18,15 @@
package com.android.systemui.bouncer.ui.composable package com.android.systemui.bouncer.ui.composable
import android.view.HapticFeedbackConstants
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleIn
@@ -48,6 +52,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -55,8 +60,10 @@ import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.android.compose.animation.Easings
import com.android.compose.grid.VerticalGrid import com.android.compose.grid.VerticalGrid
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
@@ -65,6 +72,11 @@ import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon import com.android.systemui.common.ui.compose.Icon
import com.android.systemui.compose.modifiers.thenIf import com.android.systemui.compose.modifiers.thenIf
import kotlin.math.max import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable @Composable
internal fun PinBouncer( internal fun PinBouncer(
@@ -128,7 +140,7 @@ internal fun PinBouncer(
onClicked = { viewModel.onBackspaceButtonClicked() }, onClicked = { viewModel.onBackspaceButtonClicked() },
onLongPressed = { viewModel.onBackspaceButtonLongPressed() }, onLongPressed = { viewModel.onBackspaceButtonLongPressed() },
isEnabled = isInputEnabled, isEnabled = isInputEnabled,
isHighlighted = true, isIconButton = true,
) { contentColor -> ) { contentColor ->
PinIcon( PinIcon(
Icon.Resource( Icon.Resource(
@@ -149,8 +161,8 @@ internal fun PinBouncer(
PinButton( PinButton(
onClicked = { viewModel.onAuthenticateButtonClicked() }, onClicked = { viewModel.onAuthenticateButtonClicked() },
isHighlighted = true,
isEnabled = isInputEnabled, isEnabled = isInputEnabled,
isIconButton = true,
) { contentColor -> ) { contentColor ->
PinIcon( PinIcon(
Icon.Resource( Icon.Resource(
@@ -196,39 +208,65 @@ private fun PinButton(
isEnabled: Boolean, isEnabled: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onLongPressed: (() -> Unit)? = null, onLongPressed: (() -> Unit)? = null,
isHighlighted: Boolean = false, isIconButton: Boolean = false,
content: @Composable (contentColor: Color) -> Unit, content: @Composable (contentColor: Color) -> Unit,
) { ) {
var isPressed: Boolean by remember { mutableStateOf(false) } var isPressed: Boolean by remember { mutableStateOf(false) }
val view = LocalView.current
LaunchedEffect(isPressed) {
if (isPressed) {
view.performHapticFeedback(
HapticFeedbackConstants.VIRTUAL_KEY,
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING,
)
}
}
// Pin button animation specification is asymmetric: fast animation to the pressed state, and a
// slow animation upon release. Note that isPressed is guaranteed to be true for at least the
// press animation duration (see below in detectTapGestures).
val animEasing = if (isPressed) pinButtonPressedEasing else pinButtonReleasedEasing
val animDurationMillis =
(if (isPressed) pinButtonPressedDuration else pinButtonReleasedDuration).toInt(
DurationUnit.MILLISECONDS
)
val cornerRadius: Dp by val cornerRadius: Dp by
animateDpAsState( animateDpAsState(
if (isPressed) 24.dp else PinButtonSize / 2, if (isPressed) 24.dp else pinButtonSize / 2,
label = "PinButton round corners", label = "PinButton round corners",
animationSpec = tween(animDurationMillis, easing = animEasing)
) )
val colorAnimationSpec: AnimationSpec<Color> = tween(animDurationMillis, easing = animEasing)
val containerColor: Color by val containerColor: Color by
animateColorAsState( animateColorAsState(
when { when {
isPressed -> MaterialTheme.colorScheme.primaryContainer isPressed -> MaterialTheme.colorScheme.primary
isHighlighted -> MaterialTheme.colorScheme.secondaryContainer isIconButton -> MaterialTheme.colorScheme.secondaryContainer
else -> MaterialTheme.colorScheme.surface else -> MaterialTheme.colorScheme.surfaceVariant
}, },
label = "Pin button container color", label = "Pin button container color",
animationSpec = colorAnimationSpec
) )
val contentColor: Color by val contentColor: Color by
animateColorAsState( animateColorAsState(
when { when {
isPressed -> MaterialTheme.colorScheme.onPrimaryContainer isPressed -> MaterialTheme.colorScheme.onPrimary
isHighlighted -> MaterialTheme.colorScheme.onSecondaryContainer isIconButton -> MaterialTheme.colorScheme.onSecondaryContainer
else -> MaterialTheme.colorScheme.onSurface else -> MaterialTheme.colorScheme.onSurfaceVariant
}, },
label = "Pin button container color", label = "Pin button container color",
animationSpec = colorAnimationSpec
) )
val scope = rememberCoroutineScope()
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = modifier =
modifier modifier
.size(PinButtonSize) .size(pinButtonSize)
.drawBehind { .drawBehind {
drawRoundRect( drawRoundRect(
color = containerColor, color = containerColor,
@@ -239,9 +277,15 @@ private fun PinButton(
Modifier.pointerInput(Unit) { Modifier.pointerInput(Unit) {
detectTapGestures( detectTapGestures(
onPress = { onPress = {
isPressed = true scope.launch {
tryAwaitRelease() isPressed = true
isPressed = false val minDuration = async {
delay(pinButtonPressedDuration + pinButtonHoldTime)
}
tryAwaitRelease()
minDuration.await()
isPressed = false
}
}, },
onTap = { onClicked() }, onTap = { onClicked() },
onLongPress = onLongPressed?.let { { onLongPressed() } }, onLongPress = onLongPressed?.let { { onLongPressed() } },
@@ -253,4 +297,15 @@ private fun PinButton(
} }
} }
private val PinButtonSize = 84.dp private fun showFailureAnimation() {
// TODO(b/282730134): implement.
}
private val pinButtonSize = 84.dp
// Pin button motion spec: http://shortn/_9TTIG6SoEa
private val pinButtonPressedDuration = 100.milliseconds
private val pinButtonPressedEasing = LinearEasing
private val pinButtonHoldTime = 33.milliseconds
private val pinButtonReleasedDuration = 420.milliseconds
private val pinButtonReleasedEasing = Easings.StandardEasing