Merge changes from topic "shapes" into udc-qpr-dev

* changes:
  Add shapes to PIN bouncer
  Tweak AVDs to make them work with the compose androidx.compose.animation:animation-graphics library.
  Reformat AVDs to allow better understanding of the changes in the follow-up CL
This commit is contained in:
Mike Schneider
2023-07-05 12:15:12 +00:00
committed by Android (Google) Code Review
14 changed files with 1046 additions and 31 deletions

View File

@@ -34,6 +34,7 @@ android_library {
"PlatformComposeCore", "PlatformComposeCore",
"androidx.compose.runtime_runtime", "androidx.compose.runtime_runtime",
"androidx.compose.animation_animation-graphics",
"androidx.compose.material3_material3", "androidx.compose.material3_material3",
"androidx.activity_activity-compose", "androidx.activity_activity-compose",
], ],

View File

@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
@file:OptIn(ExperimentalAnimationApi::class) @file:OptIn(ExperimentalAnimationApi::class, ExperimentalAnimationGraphicsApi::class)
package com.android.systemui.bouncer.ui.composable package com.android.systemui.bouncer.ui.composable
@@ -29,11 +29,14 @@ import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.snap import androidx.compose.animation.core.snap
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.Canvas import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -61,8 +64,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind 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.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Constraints
@@ -70,6 +75,7 @@ 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.animation.Easings
import com.android.compose.grid.VerticalGrid import com.android.compose.grid.VerticalGrid
import com.android.internal.R.id.image
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.bouncer.ui.viewmodel.ActionButtonAppearance import com.android.systemui.bouncer.ui.viewmodel.ActionButtonAppearance
import com.android.systemui.bouncer.ui.viewmodel.EnteredKey import com.android.systemui.bouncer.ui.viewmodel.EnteredKey
@@ -139,7 +145,8 @@ private fun PinInputDisplay(viewModel: PinBouncerViewModel) {
else -> EntryVisibility.Hidden else -> EntryVisibility.Hidden
} }
ObscuredInputEntry(updateTransition(visibility, label = "Pin Entry $entry")) val shape = viewModel.pinShapes.getShape(entry.sequenceNumber)
PinInputEntry(shape, updateTransition(visibility, label = "Pin Entry $entry"))
LaunchedEffect(entry) { LaunchedEffect(entry) {
// Remove entry from visiblePinEntries once the hide transition completed. // Remove entry from visiblePinEntries once the hide transition completed.
@@ -171,15 +178,11 @@ private sealed class EntryVisibility {
} }
@Composable @Composable
private fun ObscuredInputEntry(transition: Transition<EntryVisibility>) { private fun PinInputEntry(shapeResourceId: Int, transition: Transition<EntryVisibility>) {
// spec: http://shortn/_DEhE3Xl2bi // spec: http://shortn/_DEhE3Xl2bi
val shapePadding = 6.dp
val shapeOvershootSize = 22.dp
val dismissStaggerDelayMs = 33 val dismissStaggerDelayMs = 33
val dismissDurationMs = 450 val dismissDurationMs = 450
val expansionDurationMs = 250 val expansionDurationMs = 250
val shapeExpandDurationMs = 83
val shapeRetractDurationMs = 167
val shapeCollapseDurationMs = 200 val shapeCollapseDurationMs = 200
val animatedEntryWidth by val animatedEntryWidth by
@@ -194,19 +197,17 @@ private fun ObscuredInputEntry(transition: Transition<EntryVisibility>) {
}, },
label = "entry space" label = "entry space"
) { state -> ) { state ->
if (state == EntryVisibility.Shown) entryShapeSize + (shapePadding * 2) else 0.dp if (state == EntryVisibility.Shown) entryShapeSize else 0.dp
} }
val animatedShapeSize by val animatedShapeSize by
transition.animateDp( transition.animateDp(
transitionSpec = { transitionSpec = {
when { when {
EntryVisibility.Hidden isTransitioningTo EntryVisibility.Shown -> EntryVisibility.Hidden isTransitioningTo EntryVisibility.Shown -> {
keyframes { // The AVD contains the entry transition.
durationMillis = shapeExpandDurationMs + shapeRetractDurationMs snap()
0.dp at 0 with Easings.Linear }
shapeOvershootSize at shapeExpandDurationMs with Easings.Legacy
}
targetState is EntryVisibility.BulkHidden -> { targetState is EntryVisibility.BulkHidden -> {
val target = targetState as EntryVisibility.BulkHidden val target = targetState as EntryVisibility.BulkHidden
tween( tween(
@@ -220,17 +221,21 @@ private fun ObscuredInputEntry(transition: Transition<EntryVisibility>) {
}, },
label = "shape size" label = "shape size"
) { state -> ) { state ->
when (state) { if (state == EntryVisibility.Shown) entryShapeSize else 0.dp
EntryVisibility.Shown -> entryShapeSize
else -> 0.dp
}
} }
val dotColor = MaterialTheme.colorScheme.onSurfaceVariant val dotColor = MaterialTheme.colorScheme.onSurfaceVariant
Layout( Layout(
content = { content = {
// TODO(b/282730134): add support for dot shapes. val image = AnimatedImageVector.animatedVectorResource(shapeResourceId)
Canvas(Modifier) { drawCircle(dotColor) } var atEnd by remember { mutableStateOf(false) }
Image(
painter = rememberAnimatedVectorPainter(image, atEnd),
contentDescription = null,
contentScale = ContentScale.Crop,
colorFilter = ColorFilter.tint(dotColor),
)
LaunchedEffect(Unit) { atEnd = true }
} }
) { measurables, _ -> ) { measurables, _ ->
val shapeSizePx = animatedShapeSize.roundToPx() val shapeSizePx = animatedShapeSize.roundToPx()
@@ -507,7 +512,7 @@ private suspend fun showFailureAnimation(
} }
} }
private val entryShapeSize = 16.dp private val entryShapeSize = 30.dp
private val pinButtonSize = 84.dp private val pinButtonSize = 84.dp
private val pinButtonErrorShrinkFactor = 67.dp / pinButtonSize private val pinButtonErrorShrinkFactor = 67.dp / pinButtonSize

View File

@@ -1 +1,40 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"><aapt:attr name="android:drawable"><vector android:height="30dp" android:width="30dp" android:viewportHeight="30" android:viewportWidth="30"><group android:name="_R_G"><group android:name="_R_G_L_0_G" android:translateX="28.237000000000002" android:translateY="23.112000000000002"><path android:name="_R_G_L_0_G_D_0_P_0" android:strokeColor="#ffffff" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:strokeAlpha="1" android:pathData=" M-13.24 -12.11 C-11.03,-12.11 -9.24,-10.32 -9.24,-8.11 C-9.24,-5.9 -11.03,-4.11 -13.24,-4.11 C-15.44,-4.11 -17.24,-5.9 -17.24,-8.11 C-17.24,-10.32 -15.44,-12.11 -13.24,-12.11c "/></group></group><group android:name="time_group"/></vector></aapt:attr><target android:name="time_group"><aapt:attr name="android:animation"><set android:ordering="together"><objectAnimator android:propertyName="translateX" android:duration="500" android:startOffset="0" android:valueFrom="0" android:valueTo="1" android:valueType="floatType"/></set></aapt:attr></target></animated-vector> <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:height="30dp"
android:width="30dp"
android:viewportHeight="30"
android:viewportWidth="30">
<group android:name="_R_G">
<group
android:name="_R_G_L_0_G"
android:translateX="28.237000000000002"
android:translateY="23.112000000000002">
<path
android:name="_R_G_L_0_G_D_0_P_0"
android:strokeColor="#ffffff"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeAlpha="1"
android:pathData=" M-13.24 -12.11 C-11.03,-12.11 -9.24,-10.32 -9.24,-8.11 C-9.24,-5.9 -11.03,-4.11 -13.24,-4.11 C-15.44,-4.11 -17.24,-5.9 -17.24,-8.11 C-17.24,-10.32 -15.44,-12.11 -13.24,-12.11c " />
</group>
</group>
<group android:name="time_group" />
</vector>
</aapt:attr>
<target android:name="time_group">
<aapt:attr name="android:animation">
<set android:ordering="together">
<objectAnimator
android:propertyName="translateX"
android:duration="500"
android:startOffset="0"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
</animated-vector>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -61,6 +61,7 @@ constructor(
private val pin: PinBouncerViewModel by lazy { private val pin: PinBouncerViewModel by lazy {
PinBouncerViewModel( PinBouncerViewModel(
applicationContext = applicationContext,
applicationScope = applicationScope, applicationScope = applicationScope,
interactor = interactor, interactor = interactor,
isInputEnabled = isInputEnabled, isInputEnabled = isInputEnabled,

View File

@@ -16,6 +16,8 @@
package com.android.systemui.bouncer.ui.viewmodel package com.android.systemui.bouncer.ui.viewmodel
import android.content.Context
import com.android.keyguard.PinShapeAdapter
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -29,6 +31,7 @@ import kotlinx.coroutines.launch
/** Holds UI state and handles user input for the PIN code bouncer UI. */ /** Holds UI state and handles user input for the PIN code bouncer UI. */
class PinBouncerViewModel( class PinBouncerViewModel(
applicationContext: Context,
private val applicationScope: CoroutineScope, private val applicationScope: CoroutineScope,
private val interactor: BouncerInteractor, private val interactor: BouncerInteractor,
isInputEnabled: StateFlow<Boolean>, isInputEnabled: StateFlow<Boolean>,
@@ -37,6 +40,8 @@ class PinBouncerViewModel(
isInputEnabled = isInputEnabled, isInputEnabled = isInputEnabled,
) { ) {
val pinShapes = PinShapeAdapter(applicationContext)
private val mutablePinEntries = MutableStateFlow<List<EnteredKey>>(emptyList()) private val mutablePinEntries = MutableStateFlow<List<EnteredKey>>(emptyList())
val pinEntries: StateFlow<List<EnteredKey>> = mutablePinEntries val pinEntries: StateFlow<List<EnteredKey>> = mutablePinEntries

View File

@@ -42,6 +42,7 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() {
) )
private val underTest = private val underTest =
PinBouncerViewModel( PinBouncerViewModel(
applicationContext = context,
applicationScope = testScope.backgroundScope, applicationScope = testScope.backgroundScope,
interactor = interactor =
utils.bouncerInteractor( utils.bouncerInteractor(

View File

@@ -67,6 +67,7 @@ class PinBouncerViewModelTest : SysuiTestCase() {
) )
private val underTest = private val underTest =
PinBouncerViewModel( PinBouncerViewModel(
applicationContext = context,
applicationScope = testScope.backgroundScope, applicationScope = testScope.backgroundScope,
interactor = bouncerInteractor, interactor = bouncerInteractor,
isInputEnabled = MutableStateFlow(true).asStateFlow(), isInputEnabled = MutableStateFlow(true).asStateFlow(),