summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt63
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt85
2 files changed, 133 insertions, 15 deletions
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt
new file mode 100644
index 000000000000..8fe1f48dcaee
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/Easings.kt
@@ -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) }
+}
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 cbd7b8806a77..2ca78b1ad195 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
@@ -18,11 +18,15 @@
package com.android.systemui.bouncer.ui.composable
+import android.view.HapticFeedbackConstants
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateColorAsState
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.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
@@ -48,6 +52,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.graphics.Color
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 com.android.compose.animation.Easings
import com.android.compose.grid.VerticalGrid
import com.android.systemui.R
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.compose.modifiers.thenIf
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
internal fun PinBouncer(
@@ -128,7 +140,7 @@ internal fun PinBouncer(
onClicked = { viewModel.onBackspaceButtonClicked() },
onLongPressed = { viewModel.onBackspaceButtonLongPressed() },
isEnabled = isInputEnabled,
- isHighlighted = true,
+ isIconButton = true,
) { contentColor ->
PinIcon(
Icon.Resource(
@@ -149,8 +161,8 @@ internal fun PinBouncer(
PinButton(
onClicked = { viewModel.onAuthenticateButtonClicked() },
- isHighlighted = true,
isEnabled = isInputEnabled,
+ isIconButton = true,
) { contentColor ->
PinIcon(
Icon.Resource(
@@ -196,39 +208,65 @@ private fun PinButton(
isEnabled: Boolean,
modifier: Modifier = Modifier,
onLongPressed: (() -> Unit)? = null,
- isHighlighted: Boolean = false,
+ isIconButton: Boolean = false,
content: @Composable (contentColor: Color) -> Unit,
) {
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
animateDpAsState(
- if (isPressed) 24.dp else PinButtonSize / 2,
+ if (isPressed) 24.dp else pinButtonSize / 2,
label = "PinButton round corners",
+ animationSpec = tween(animDurationMillis, easing = animEasing)
)
+ val colorAnimationSpec: AnimationSpec<Color> = tween(animDurationMillis, easing = animEasing)
val containerColor: Color by
animateColorAsState(
when {
- isPressed -> MaterialTheme.colorScheme.primaryContainer
- isHighlighted -> MaterialTheme.colorScheme.secondaryContainer
- else -> MaterialTheme.colorScheme.surface
+ isPressed -> MaterialTheme.colorScheme.primary
+ isIconButton -> MaterialTheme.colorScheme.secondaryContainer
+ else -> MaterialTheme.colorScheme.surfaceVariant
},
label = "Pin button container color",
+ animationSpec = colorAnimationSpec
)
val contentColor: Color by
animateColorAsState(
when {
- isPressed -> MaterialTheme.colorScheme.onPrimaryContainer
- isHighlighted -> MaterialTheme.colorScheme.onSecondaryContainer
- else -> MaterialTheme.colorScheme.onSurface
+ isPressed -> MaterialTheme.colorScheme.onPrimary
+ isIconButton -> MaterialTheme.colorScheme.onSecondaryContainer
+ else -> MaterialTheme.colorScheme.onSurfaceVariant
},
label = "Pin button container color",
+ animationSpec = colorAnimationSpec
)
+ val scope = rememberCoroutineScope()
+
Box(
contentAlignment = Alignment.Center,
modifier =
modifier
- .size(PinButtonSize)
+ .size(pinButtonSize)
.drawBehind {
drawRoundRect(
color = containerColor,
@@ -239,9 +277,15 @@ private fun PinButton(
Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = {
- isPressed = true
- tryAwaitRelease()
- isPressed = false
+ scope.launch {
+ isPressed = true
+ val minDuration = async {
+ delay(pinButtonPressedDuration + pinButtonHoldTime)
+ }
+ tryAwaitRelease()
+ minDuration.await()
+ isPressed = false
+ }
},
onTap = { onClicked() },
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