From cd1208833472effc369bd5e6d86ad9c7e38f7b48 Mon Sep 17 00:00:00 2001 From: Matt Pietal Date: Fri, 27 Jan 2023 14:47:00 +0000 Subject: [PATCH 1/2] Transitions - Refactor view model All view models were following the same pattern, so consolidate into KeyguardTransitionAnimationFlow. This also adds support for cancel/finish events. Rework how tests run for this. Explicity test logic for how animation turn into flows with KeyguardTransitionAnimationFlowTest. For the ViewModels, don't retest this logic, simply verify that the output stays in the expected range. Fixes: 266680387 Test: atest frameworks/base/packages/SystemUI/tests/src/com/android/systemui/keyguard/ Change-Id: I254d213026775e34c0852675ea14efb167271915 --- .../KeyguardTransitionInteractor.kt | 38 ---- .../keyguard/shared/model/AnimationParams.kt | 25 --- .../ui/KeyguardTransitionAnimationFlow.kt | 106 +++++++++++ ...DreamingToLockscreenTransitionViewModel.kt | 60 +++--- .../GoneToDreamingTransitionViewModel.kt | 45 ++--- ...LockscreenToDreamingTransitionViewModel.kt | 42 ++--- ...LockscreenToOccludedTransitionViewModel.kt | 49 ++--- ...OccludedToLockscreenTransitionViewModel.kt | 39 ++-- .../ui/KeyguardTransitionAnimationFlowTest.kt | 174 ++++++++++++++++++ ...mingToLockscreenTransitionViewModelTest.kt | 104 +++-------- .../GoneToDreamingTransitionViewModelTest.kt | 52 +----- ...screenToDreamingTransitionViewModelTest.kt | 50 +---- ...screenToOccludedTransitionViewModelTest.kt | 78 ++++---- ...udedToLockscreenTransitionViewModelTest.kt | 67 ++----- 14 files changed, 477 insertions(+), 452 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index ad6dbea7ae433..53c80f65e44fc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -19,7 +19,6 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository -import com.android.systemui.keyguard.shared.model.AnimationParams import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.BOUNCER @@ -31,9 +30,6 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.shared.model.TransitionStep import javax.inject.Inject -import kotlin.math.max -import kotlin.math.min -import kotlin.time.Duration import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map @@ -104,38 +100,4 @@ constructor( /* The last completed [KeyguardState] transition */ val finishedKeyguardState: Flow = finishedKeyguardTransitionStep.map { step -> step.to } - - /** - * Transitions will occur over a [totalDuration] with [TransitionStep]s being emitted in the - * range of [0, 1]. View animations should begin and end within a subset of this range. This - * function maps the [startTime] and [duration] into [0, 1], when this subset is valid. - */ - fun transitionStepAnimation( - flow: Flow, - params: AnimationParams, - totalDuration: Duration, - ): Flow { - val start = (params.startTime / totalDuration).toFloat() - val chunks = (totalDuration / params.duration).toFloat() - var isRunning = false - return flow - .map { step -> - val value = (step.value - start) * chunks - if (step.transitionState == STARTED) { - // When starting, make sure to always emit. If a transition is started from the - // middle, it is possible this animation is being skipped but we need to inform - // the ViewModels of the last update - isRunning = true - max(0f, min(1f, value)) - } else if (isRunning && value >= 1f) { - // Always send a final value of 1. Because of rounding, [value] may never be - // exactly 1. - isRunning = false - 1f - } else { - value - } - } - .filter { value -> value >= 0f && value <= 1f } - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt deleted file mode 100644 index 67733e9052683..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/AnimationParams.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2022 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.systemui.keyguard.shared.model - -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds - -/** Animation parameters */ -data class AnimationParams( - val startTime: Duration = 0.milliseconds, - val duration: Duration, -) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt new file mode 100644 index 0000000000000..ca1e27c9d19c9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt @@ -0,0 +1,106 @@ +/* + * 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.systemui.keyguard.ui + +import android.view.animation.Interpolator +import com.android.systemui.animation.Interpolators.LINEAR +import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED +import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING +import com.android.systemui.keyguard.shared.model.TransitionState.STARTED +import com.android.systemui.keyguard.shared.model.TransitionStep +import kotlin.math.max +import kotlin.math.min +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map + +/** + * For the given transition params, construct a flow using [createFlow] for the specified portion of + * the overall transition. + */ +class KeyguardTransitionAnimationFlow( + private val transitionDuration: Duration, + private val transitionFlow: Flow, +) { + /** + * Transitions will occur over a [transitionDuration] with [TransitionStep]s being emitted in + * the range of [0, 1]. View animations should begin and end within a subset of this range. This + * function maps the [startTime] and [duration] into [0, 1], when this subset is valid. + */ + fun createFlow( + duration: Duration, + onStep: (Float) -> Float, + startTime: Duration = 0.milliseconds, + onCancel: (() -> Float)? = null, + onFinish: (() -> Float)? = null, + interpolator: Interpolator = LINEAR, + ): Flow { + if (!duration.isPositive()) { + throw IllegalArgumentException("duration must be a positive number: $duration") + } + if ((startTime + duration).compareTo(transitionDuration) > 0) { + throw IllegalArgumentException( + "startTime($startTime) + duration($duration) must be" + + " <= transitionDuration($transitionDuration)" + ) + } + + val start = (startTime / transitionDuration).toFloat() + val chunks = (transitionDuration / duration).toFloat() + var isComplete = true + + fun stepToValue(step: TransitionStep): Float? { + val value = (step.value - start) * chunks + return when (step.transitionState) { + // When starting, make sure to always emit. If a transition is started from the + // middle, it is possible this animation is being skipped but we need to inform + // the ViewModels of the last update + STARTED -> { + isComplete = false + max(0f, min(1f, value)) + } + // Always send a final value of 1. Because of rounding, [value] may never be + // exactly 1. + RUNNING -> + if (isComplete) { + null + } else if (value >= 1f) { + isComplete = true + 1f + } else if (value >= 0f) { + value + } else { + null + } + else -> null + }?.let { onStep(interpolator.getInterpolation(it)) } + } + + return transitionFlow + .map { step -> + when (step.transitionState) { + STARTED -> stepToValue(step) + RUNNING -> stepToValue(step) + CANCELED -> onCancel?.invoke() + FINISHED -> onFinish?.invoke() + } + } + .filterNotNull() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt index 6627865ecc79c..8d6545a49a8a3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModel.kt @@ -21,15 +21,10 @@ import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.AnimationParams -import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED -import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge /** * Breaks down DREAMING->LOCKSCREEN transition into discrete steps for corresponding views to @@ -41,39 +36,46 @@ class DreamingToLockscreenTransitionViewModel constructor( private val interactor: KeyguardTransitionInteractor, ) { + private val transitionAnimation = + KeyguardTransitionAnimationFlow( + transitionDuration = TO_LOCKSCREEN_DURATION, + transitionFlow = interactor.dreamingToLockscreenTransition, + ) /** Dream overlay y-translation on exit */ fun dreamOverlayTranslationY(translatePx: Int): Flow { - return flowForAnimation(DREAM_OVERLAY_TRANSLATION_Y).map { value -> - EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx - } + return transitionAnimation.createFlow( + duration = 600.milliseconds, + onStep = { it * translatePx }, + interpolator = EMPHASIZED_ACCELERATE, + ) } /** Dream overlay views alpha - fade out */ - val dreamOverlayAlpha: Flow = flowForAnimation(DREAM_OVERLAY_ALPHA).map { 1f - it } + val dreamOverlayAlpha: Flow = + transitionAnimation.createFlow( + duration = 250.milliseconds, + onStep = { 1f - it }, + ) /** Lockscreen views y-translation */ fun lockscreenTranslationY(translatePx: Int): Flow { - return merge( - flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> - -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx) - }, - // On end, reset the translation to 0 - interactor.dreamingToLockscreenTransition - .filter { it.transitionState == FINISHED || it.transitionState == CANCELED } - .map { 0f } + return transitionAnimation.createFlow( + duration = TO_LOCKSCREEN_DURATION, + onStep = { value -> -translatePx + value * translatePx }, + // Reset on cancel or finish + onFinish = { 0f }, + onCancel = { 0f }, + interpolator = EMPHASIZED_DECELERATE, ) } /** Lockscreen views alpha */ - val lockscreenAlpha: Flow = flowForAnimation(LOCKSCREEN_ALPHA) - - private fun flowForAnimation(params: AnimationParams): Flow { - return interactor.transitionStepAnimation( - interactor.dreamingToLockscreenTransition, - params, - totalDuration = TO_LOCKSCREEN_DURATION + val lockscreenAlpha: Flow = + transitionAnimation.createFlow( + startTime = 233.milliseconds, + duration = 250.milliseconds, + onStep = { it }, ) - } companion object { /* Length of time before ending the dream activity, in order to start unoccluding */ @@ -81,11 +83,5 @@ constructor( @JvmField val LOCKSCREEN_ANIMATION_DURATION_MS = (TO_LOCKSCREEN_DURATION - DREAM_ANIMATION_DURATION).inWholeMilliseconds - - val DREAM_OVERLAY_TRANSLATION_Y = AnimationParams(duration = 600.milliseconds) - val DREAM_OVERLAY_ALPHA = AnimationParams(duration = 250.milliseconds) - val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION) - val LOCKSCREEN_ALPHA = - AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt index 5a4796096eeb9..f16827d4a54af 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModel.kt @@ -20,15 +20,10 @@ import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.AnimationParams -import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED -import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge /** Breaks down GONE->DREAMING transition into discrete steps for corresponding views to consume. */ @SysUISingleton @@ -38,32 +33,28 @@ constructor( private val interactor: KeyguardTransitionInteractor, ) { + private val transitionAnimation = + KeyguardTransitionAnimationFlow( + transitionDuration = TO_DREAMING_DURATION, + transitionFlow = interactor.goneToDreamingTransition, + ) + /** Lockscreen views y-translation */ fun lockscreenTranslationY(translatePx: Int): Flow { - return merge( - flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> - (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx) - }, - // On end, reset the translation to 0 - interactor.goneToDreamingTransition - .filter { it.transitionState == FINISHED || it.transitionState == CANCELED } - .map { 0f } + return transitionAnimation.createFlow( + duration = 500.milliseconds, + onStep = { it * translatePx }, + // Reset on cancel or finish + onFinish = { 0f }, + onCancel = { 0f }, + interpolator = EMPHASIZED_ACCELERATE, ) } /** Lockscreen views alpha */ - val lockscreenAlpha: Flow = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it } - - private fun flowForAnimation(params: AnimationParams): Flow { - return interactor.transitionStepAnimation( - interactor.goneToDreamingTransition, - params, - totalDuration = TO_DREAMING_DURATION + val lockscreenAlpha: Flow = + transitionAnimation.createFlow( + duration = 250.milliseconds, + onStep = { 1f - it }, ) - } - - companion object { - val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds) - val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds) - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt index e05adbdab5838..bc9dc4f691027 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModel.kt @@ -20,15 +20,10 @@ import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.AnimationParams -import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED -import com.android.systemui.keyguard.shared.model.TransitionState.FINISHED +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge /** * Breaks down LOCKSCREEN->DREAMING transition into discrete steps for corresponding views to @@ -40,35 +35,32 @@ class LockscreenToDreamingTransitionViewModel constructor( private val interactor: KeyguardTransitionInteractor, ) { + private val transitionAnimation = + KeyguardTransitionAnimationFlow( + transitionDuration = TO_DREAMING_DURATION, + transitionFlow = interactor.lockscreenToDreamingTransition, + ) /** Lockscreen views y-translation */ fun lockscreenTranslationY(translatePx: Int): Flow { - return merge( - flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> - (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx) - }, - // On end, reset the translation to 0 - interactor.lockscreenToDreamingTransition - .filter { it.transitionState == FINISHED || it.transitionState == CANCELED } - .map { 0f } + return transitionAnimation.createFlow( + duration = 500.milliseconds, + onStep = { it * translatePx }, + // Reset on cancel or finish + onFinish = { 0f }, + onCancel = { 0f }, + interpolator = EMPHASIZED_ACCELERATE, ) } /** Lockscreen views alpha */ - val lockscreenAlpha: Flow = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it } - - private fun flowForAnimation(params: AnimationParams): Flow { - return interactor.transitionStepAnimation( - interactor.lockscreenToDreamingTransition, - params, - totalDuration = TO_DREAMING_DURATION + val lockscreenAlpha: Flow = + transitionAnimation.createFlow( + duration = 250.milliseconds, + onStep = { 1f - it }, ) - } companion object { @JvmField val DREAMING_ANIMATION_DURATION_MS = TO_DREAMING_DURATION.inWholeMilliseconds - - val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = 500.milliseconds) - val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt index 22d292e928565..a60665a81f0ee 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModel.kt @@ -20,14 +20,10 @@ import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.AnimationParams -import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.merge /** * Breaks down LOCKSCREEN->OCCLUDED transition into discrete steps for corresponding views to @@ -39,33 +35,28 @@ class LockscreenToOccludedTransitionViewModel constructor( private val interactor: KeyguardTransitionInteractor, ) { + private val transitionAnimation = + KeyguardTransitionAnimationFlow( + transitionDuration = TO_OCCLUDED_DURATION, + transitionFlow = interactor.lockscreenToOccludedTransition, + ) + + /** Lockscreen views alpha */ + val lockscreenAlpha: Flow = + transitionAnimation.createFlow( + duration = 250.milliseconds, + onStep = { 1f - it }, + ) /** Lockscreen views y-translation */ fun lockscreenTranslationY(translatePx: Int): Flow { - return merge( - flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> - (EMPHASIZED_ACCELERATE.getInterpolation(value) * translatePx) - }, - // On end, reset the translation to 0 - interactor.lockscreenToOccludedTransition - .filter { step -> step.transitionState == TransitionState.FINISHED } - .map { 0f } + return transitionAnimation.createFlow( + duration = TO_OCCLUDED_DURATION, + onStep = { value -> value * translatePx }, + // Reset on cancel or finish + onFinish = { 0f }, + onCancel = { 0f }, + interpolator = EMPHASIZED_ACCELERATE, ) } - - /** Lockscreen views alpha */ - val lockscreenAlpha: Flow = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it } - - private fun flowForAnimation(params: AnimationParams): Flow { - return interactor.transitionStepAnimation( - interactor.lockscreenToOccludedTransition, - params, - totalDuration = TO_OCCLUDED_DURATION - ) - } - - companion object { - val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_OCCLUDED_DURATION) - val LOCKSCREEN_ALPHA = AnimationParams(duration = 250.milliseconds) - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt index e804562bc85f5..5770f3ee8876f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModel.kt @@ -20,11 +20,10 @@ import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.AnimationParams +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map /** * Breaks down OCCLUDED->LOCKSCREEN transition into discrete steps for corresponding views to @@ -36,28 +35,26 @@ class OccludedToLockscreenTransitionViewModel constructor( private val interactor: KeyguardTransitionInteractor, ) { + private val transitionAnimation = + KeyguardTransitionAnimationFlow( + transitionDuration = TO_LOCKSCREEN_DURATION, + transitionFlow = interactor.occludedToLockscreenTransition, + ) + /** Lockscreen views y-translation */ fun lockscreenTranslationY(translatePx: Int): Flow { - return flowForAnimation(LOCKSCREEN_TRANSLATION_Y).map { value -> - -translatePx + (EMPHASIZED_DECELERATE.getInterpolation(value) * translatePx) - } - } - - /** Lockscreen views alpha */ - val lockscreenAlpha: Flow = flowForAnimation(LOCKSCREEN_ALPHA) - - private fun flowForAnimation(params: AnimationParams): Flow { - return interactor.transitionStepAnimation( - interactor.occludedToLockscreenTransition, - params, - totalDuration = TO_LOCKSCREEN_DURATION + return transitionAnimation.createFlow( + duration = TO_LOCKSCREEN_DURATION, + onStep = { value -> -translatePx + value * translatePx }, + interpolator = EMPHASIZED_DECELERATE, ) } - companion object { - @JvmField val LOCKSCREEN_ANIMATION_DURATION_MS = TO_LOCKSCREEN_DURATION.inWholeMilliseconds - val LOCKSCREEN_TRANSLATION_Y = AnimationParams(duration = TO_LOCKSCREEN_DURATION) - val LOCKSCREEN_ALPHA = - AnimationParams(startTime = 233.milliseconds, duration = 250.milliseconds) - } + /** Lockscreen views alpha */ + val lockscreenAlpha: Flow = + transitionAnimation.createFlow( + startTime = 233.milliseconds, + duration = 250.milliseconds, + onStep = { it }, + ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt new file mode 100644 index 0000000000000..a5b78b74fcdf4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlowTest.kt @@ -0,0 +1,174 @@ +/* + * 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.systemui.keyguard.ui + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class KeyguardTransitionAnimationFlowTest : SysuiTestCase() { + private lateinit var underTest: KeyguardTransitionAnimationFlow + private lateinit var repository: FakeKeyguardTransitionRepository + + @Before + fun setUp() { + repository = FakeKeyguardTransitionRepository() + underTest = + KeyguardTransitionAnimationFlow( + 1000.milliseconds, + repository.transitions, + ) + } + + @Test(expected = IllegalArgumentException::class) + fun zeroDurationThrowsException() = runTest { + val flow = underTest.createFlow(duration = 0.milliseconds, onStep = { it }) + } + + @Test(expected = IllegalArgumentException::class) + fun startTimePlusDurationGreaterThanTransitionDurationThrowsException() = runTest { + val flow = + underTest.createFlow( + startTime = 300.milliseconds, + duration = 800.milliseconds, + onStep = { it } + ) + } + + @Test + fun onFinishRunsWhenSpecified() = runTest { + val flow = + underTest.createFlow( + duration = 100.milliseconds, + onStep = { it }, + onFinish = { 10f }, + ) + var animationValues = collectLastValue(flow) + repository.sendTransitionStep(step(1f, TransitionState.FINISHED)) + assertThat(animationValues()).isEqualTo(10f) + } + + @Test + fun onCancelRunsWhenSpecified() = runTest { + val flow = + underTest.createFlow( + duration = 100.milliseconds, + onStep = { it }, + onCancel = { 100f }, + ) + var animationValues = collectLastValue(flow) + repository.sendTransitionStep(step(0.5f, TransitionState.CANCELED)) + assertThat(animationValues()).isEqualTo(100f) + } + + @Test + fun usesStartTime() = runTest { + val flow = + underTest.createFlow( + startTime = 500.milliseconds, + duration = 500.milliseconds, + onStep = { it }, + ) + var animationValues = collectLastValue(flow) + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + assertThat(animationValues()).isEqualTo(0f) + + // Should not emit a value + repository.sendTransitionStep(step(0.1f, TransitionState.RUNNING)) + + repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING)) + assertFloat(animationValues(), 0f) + repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING)) + assertFloat(animationValues(), 0.2f) + repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING)) + assertFloat(animationValues(), 0.6f) + repository.sendTransitionStep(step(1f, TransitionState.RUNNING)) + assertFloat(animationValues(), 1f) + } + + @Test + fun usesInterpolator() = runTest { + val flow = + underTest.createFlow( + duration = 1000.milliseconds, + interpolator = EMPHASIZED_ACCELERATE, + onStep = { it }, + ) + var animationValues = collectLastValue(flow) + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0f)) + repository.sendTransitionStep(step(0.5f, TransitionState.RUNNING)) + assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.5f)) + repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING)) + assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.6f)) + repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING)) + assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(0.8f)) + repository.sendTransitionStep(step(1f, TransitionState.RUNNING)) + assertFloat(animationValues(), EMPHASIZED_ACCELERATE.getInterpolation(1f)) + } + + @Test + fun usesOnStepToDoubleValue() = runTest { + val flow = + underTest.createFlow( + duration = 1000.milliseconds, + onStep = { it * 2 }, + ) + var animationValues = collectLastValue(flow) + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + assertFloat(animationValues(), 0f) + repository.sendTransitionStep(step(0.3f, TransitionState.RUNNING)) + assertFloat(animationValues(), 0.6f) + repository.sendTransitionStep(step(0.6f, TransitionState.RUNNING)) + assertFloat(animationValues(), 1.2f) + repository.sendTransitionStep(step(0.8f, TransitionState.RUNNING)) + assertFloat(animationValues(), 1.6f) + repository.sendTransitionStep(step(1f, TransitionState.RUNNING)) + assertFloat(animationValues(), 2f) + } + + private fun assertFloat(actual: Float?, expected: Float) { + assertThat(actual!!).isWithin(0.01f).of(expected) + } + + private fun step( + value: Float, + state: TransitionState = TransitionState.RUNNING + ): TransitionStep { + return TransitionStep( + from = KeyguardState.GONE, + to = KeyguardState.DREAMING, + value = value, + transitionState = state, + ownerName = "GoneToDreamingTransitionViewModelTest" + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt index 557166301d64a..06e397d8fb8f1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt @@ -18,19 +18,13 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE -import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.AnimationParams import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_ALPHA -import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.DREAM_OVERLAY_TRANSLATION_Y -import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA -import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -46,6 +40,7 @@ import org.junit.runners.JUnit4 class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { private lateinit var underTest: DreamingToLockscreenTransitionViewModel private lateinit var repository: FakeKeyguardTransitionRepository + private lateinit var transitionAnimation: KeyguardTransitionAnimationFlow @Before fun setUp() { @@ -63,32 +58,18 @@ class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { val job = underTest.dreamOverlayTranslationY(pixels).onEach { values.add(it) }.launchIn(this) + // Should start running here... + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) repository.sendTransitionStep(step(0f)) repository.sendTransitionStep(step(0.3f)) repository.sendTransitionStep(step(0.5f)) + repository.sendTransitionStep(step(0.6f)) + // ...up to here + repository.sendTransitionStep(step(0.8f)) repository.sendTransitionStep(step(1f)) - // Only 3 values should be present, since the dream overlay runs for a small fraction - // of the overall animation time - assertThat(values.size).isEqualTo(3) - assertThat(values[0]) - .isEqualTo( - EMPHASIZED_ACCELERATE.getInterpolation( - animValue(0f, DREAM_OVERLAY_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[1]) - .isEqualTo( - EMPHASIZED_ACCELERATE.getInterpolation( - animValue(0.3f, DREAM_OVERLAY_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[2]) - .isEqualTo( - EMPHASIZED_ACCELERATE.getInterpolation( - animValue(0.5f, DREAM_OVERLAY_TRANSLATION_Y) - ) * pixels - ) + assertThat(values.size).isEqualTo(5) + values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } job.cancel() } @@ -100,16 +81,18 @@ class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { val job = underTest.dreamOverlayAlpha.onEach { values.add(it) }.launchIn(this) + // Should start running here... + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) repository.sendTransitionStep(step(0f)) repository.sendTransitionStep(step(0.1f)) repository.sendTransitionStep(step(0.5f)) + // ...up to here repository.sendTransitionStep(step(1f)) // Only two values should be present, since the dream overlay runs for a small fraction // of the overall animation time - assertThat(values.size).isEqualTo(2) - assertThat(values[0]).isEqualTo(1f - animValue(0f, DREAM_OVERLAY_ALPHA)) - assertThat(values[1]).isEqualTo(1f - animValue(0.1f, DREAM_OVERLAY_ALPHA)) + assertThat(values.size).isEqualTo(4) + values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } job.cancel() } @@ -121,19 +104,15 @@ class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this) + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) repository.sendTransitionStep(step(0f)) repository.sendTransitionStep(step(0.1f)) - // Should start running here... repository.sendTransitionStep(step(0.2f)) repository.sendTransitionStep(step(0.3f)) - // ...up to here repository.sendTransitionStep(step(1f)) - // Only two values should be present, since the dream overlay runs for a small fraction - // of the overall animation time - assertThat(values.size).isEqualTo(2) - assertThat(values[0]).isEqualTo(animValue(0.2f, LOCKSCREEN_ALPHA)) - assertThat(values[1]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA)) + assertThat(values.size).isEqualTo(4) + values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } job.cancel() } @@ -147,58 +126,27 @@ class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() { val job = underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this) + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) repository.sendTransitionStep(step(0f)) repository.sendTransitionStep(step(0.3f)) repository.sendTransitionStep(step(0.5f)) repository.sendTransitionStep(step(1f)) - assertThat(values.size).isEqualTo(4) - assertThat(values[0]) - .isEqualTo( - -pixels + - EMPHASIZED_DECELERATE.getInterpolation( - animValue(0f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[1]) - .isEqualTo( - -pixels + - EMPHASIZED_DECELERATE.getInterpolation( - animValue(0.3f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[2]) - .isEqualTo( - -pixels + - EMPHASIZED_DECELERATE.getInterpolation( - animValue(0.5f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[3]) - .isEqualTo( - -pixels + - EMPHASIZED_DECELERATE.getInterpolation( - animValue(1f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) + assertThat(values.size).isEqualTo(5) + values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) } job.cancel() } - private fun animValue(stepValue: Float, params: AnimationParams): Float { - val totalDuration = TO_LOCKSCREEN_DURATION - val startValue = (params.startTime / totalDuration).toFloat() - - val multiplier = (totalDuration / params.duration).toFloat() - return (stepValue - startValue) * multiplier - } - - private fun step(value: Float): TransitionStep { + private fun step( + value: Float, + state: TransitionState = TransitionState.RUNNING + ): TransitionStep { return TransitionStep( from = KeyguardState.DREAMING, to = KeyguardState.LOCKSCREEN, value = value, - transitionState = TransitionState.RUNNING, + transitionState = state, ownerName = "DreamingToLockscreenTransitionViewModelTest" ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt index 7fa204bb980b2..14c3b504d0ae6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt @@ -18,16 +18,12 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.FromGoneTransitionInteractor.Companion.TO_DREAMING_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.AnimationParams import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_ALPHA -import com.android.systemui.keyguard.ui.viewmodel.GoneToDreamingTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y +import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -59,20 +55,18 @@ class GoneToDreamingTransitionViewModelTest : SysuiTestCase() { val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this) // Should start running here... + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) repository.sendTransitionStep(step(0f)) repository.sendTransitionStep(step(0.1f)) repository.sendTransitionStep(step(0.2f)) - // ...up to here repository.sendTransitionStep(step(0.3f)) + // ...up to here repository.sendTransitionStep(step(1f)) // Only three values should be present, since the dream overlay runs for a small - // fraction - // of the overall animation time - assertThat(values.size).isEqualTo(3) - assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA)) - assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA)) - assertThat(values[2]).isEqualTo(1f - animValue(0.2f, LOCKSCREEN_ALPHA)) + // fraction of the overall animation time + assertThat(values.size).isEqualTo(5) + values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } job.cancel() } @@ -87,45 +81,19 @@ class GoneToDreamingTransitionViewModelTest : SysuiTestCase() { underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this) // Should start running here... + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) repository.sendTransitionStep(step(0f)) repository.sendTransitionStep(step(0.3f)) repository.sendTransitionStep(step(0.5f)) - // ...up to here - repository.sendTransitionStep(step(1f)) // And a final reset event on CANCEL repository.sendTransitionStep(step(0.8f, TransitionState.CANCELED)) - assertThat(values.size).isEqualTo(4) - assertThat(values[0]) - .isEqualTo( - EMPHASIZED_ACCELERATE.getInterpolation( - animValue(0f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[1]) - .isEqualTo( - EMPHASIZED_ACCELERATE.getInterpolation( - animValue(0.3f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[2]) - .isEqualTo( - EMPHASIZED_ACCELERATE.getInterpolation( - animValue(0.5f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[3]).isEqualTo(0f) + assertThat(values.size).isEqualTo(5) + values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } + job.cancel() } - private fun animValue(stepValue: Float, params: AnimationParams): Float { - val totalDuration = TO_DREAMING_DURATION - val startValue = (params.startTime / totalDuration).toFloat() - - val multiplier = (totalDuration / params.duration).toFloat() - return (stepValue - startValue) * multiplier - } - private fun step( value: Float, state: TransitionState = TransitionState.RUNNING diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt index 539fc2c1548ed..ed31dc39dd905 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt @@ -18,16 +18,12 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_DREAMING_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.AnimationParams import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.Companion.LOCKSCREEN_ALPHA -import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y +import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -59,19 +55,18 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this) // Should start running here... + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) repository.sendTransitionStep(step(0f)) repository.sendTransitionStep(step(0.1f)) repository.sendTransitionStep(step(0.2f)) - // ...up to here repository.sendTransitionStep(step(0.3f)) + // ...up to here repository.sendTransitionStep(step(1f)) // Only three values should be present, since the dream overlay runs for a small // fraction of the overall animation time - assertThat(values.size).isEqualTo(3) - assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA)) - assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA)) - assertThat(values[2]).isEqualTo(1f - animValue(0.2f, LOCKSCREEN_ALPHA)) + assertThat(values.size).isEqualTo(5) + values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } job.cancel() } @@ -85,47 +80,22 @@ class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() { val job = underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this) - // Should start running here... + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) repository.sendTransitionStep(step(0f)) repository.sendTransitionStep(step(0.3f)) repository.sendTransitionStep(step(0.5f)) - // ...up to here repository.sendTransitionStep(step(1f)) // And a final reset event on FINISHED repository.sendTransitionStep(step(1f, TransitionState.FINISHED)) - assertThat(values.size).isEqualTo(4) - assertThat(values[0]) - .isEqualTo( - EMPHASIZED_ACCELERATE.getInterpolation( - animValue(0f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[1]) - .isEqualTo( - EMPHASIZED_ACCELERATE.getInterpolation( - animValue(0.3f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[2]) - .isEqualTo( - EMPHASIZED_ACCELERATE.getInterpolation( - animValue(0.5f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[3]).isEqualTo(0f) + assertThat(values.size).isEqualTo(6) + values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } + // Validate finished value + assertThat(values[5]).isEqualTo(0f) job.cancel() } - private fun animValue(stepValue: Float, params: AnimationParams): Float { - val totalDuration = TO_DREAMING_DURATION - val startValue = (params.startTime / totalDuration).toFloat() - - val multiplier = (totalDuration / params.duration).toFloat() - return (stepValue - startValue) * multiplier - } - private fun step( value: Float, state: TransitionState = TransitionState.RUNNING diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt index 759345f51c59a..458b315195004 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt @@ -18,16 +18,12 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor.Companion.TO_OCCLUDED_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.AnimationParams import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel.Companion.LOCKSCREEN_ALPHA -import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y +import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -59,19 +55,18 @@ class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() { val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this) // Should start running here... + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) repository.sendTransitionStep(step(0f)) repository.sendTransitionStep(step(0.1f)) repository.sendTransitionStep(step(0.4f)) - // ...up to here repository.sendTransitionStep(step(0.7f)) + // ...up to here repository.sendTransitionStep(step(1f)) // Only 3 values should be present, since the dream overlay runs for a small fraction // of the overall animation time - assertThat(values.size).isEqualTo(3) - assertThat(values[0]).isEqualTo(1f - animValue(0f, LOCKSCREEN_ALPHA)) - assertThat(values[1]).isEqualTo(1f - animValue(0.1f, LOCKSCREEN_ALPHA)) - assertThat(values[2]).isEqualTo(1f - animValue(0.4f, LOCKSCREEN_ALPHA)) + assertThat(values.size).isEqualTo(5) + values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } job.cancel() } @@ -86,54 +81,51 @@ class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() { underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this) // Should start running here... + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) repository.sendTransitionStep(step(0f)) repository.sendTransitionStep(step(0.3f)) repository.sendTransitionStep(step(0.5f)) repository.sendTransitionStep(step(1f)) // ...up to here - assertThat(values.size).isEqualTo(4) - assertThat(values[0]) - .isEqualTo( - EMPHASIZED_ACCELERATE.getInterpolation( - animValue(0f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[1]) - .isEqualTo( - EMPHASIZED_ACCELERATE.getInterpolation( - animValue(0.3f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[2]) - .isEqualTo( - EMPHASIZED_ACCELERATE.getInterpolation( - animValue(0.5f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[3]) - .isEqualTo( - EMPHASIZED_ACCELERATE.getInterpolation( - animValue(1f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) + assertThat(values.size).isEqualTo(5) + values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } + job.cancel() } - private fun animValue(stepValue: Float, params: AnimationParams): Float { - val totalDuration = TO_OCCLUDED_DURATION - val startValue = (params.startTime / totalDuration).toFloat() + @Test + fun lockscreenTranslationYIsCanceled() = + runTest(UnconfinedTestDispatcher()) { + val values = mutableListOf() - val multiplier = (totalDuration / params.duration).toFloat() - return (stepValue - startValue) * multiplier - } + val pixels = 100 + val job = + underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this) - private fun step(value: Float): TransitionStep { + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + repository.sendTransitionStep(step(0f)) + repository.sendTransitionStep(step(0.3f)) + repository.sendTransitionStep(step(0.3f, TransitionState.CANCELED)) + + assertThat(values.size).isEqualTo(4) + values.forEach { assertThat(it).isIn(Range.closed(0f, 100f)) } + + // Cancel will reset the translation + assertThat(values[3]).isEqualTo(0) + + job.cancel() + } + + private fun step( + value: Float, + state: TransitionState = TransitionState.RUNNING, + ): TransitionStep { return TransitionStep( from = KeyguardState.LOCKSCREEN, to = KeyguardState.OCCLUDED, value = value, - transitionState = TransitionState.RUNNING, + transitionState = state, ownerName = "LockscreenToOccludedTransitionViewModelTest" ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt index 98d292d689e40..a36214ecdb2f7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt @@ -18,16 +18,12 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.FromOccludedTransitionInteractor.Companion.TO_LOCKSCREEN_DURATION import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.AnimationParams import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_ALPHA -import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel.Companion.LOCKSCREEN_TRANSLATION_Y +import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -58,21 +54,19 @@ class OccludedToLockscreenTransitionViewModelTest : SysuiTestCase() { val job = underTest.lockscreenAlpha.onEach { values.add(it) }.launchIn(this) - repository.sendTransitionStep(step(0f)) + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) + repository.sendTransitionStep(step(0.1f)) // Should start running here... repository.sendTransitionStep(step(0.3f)) repository.sendTransitionStep(step(0.4f)) repository.sendTransitionStep(step(0.5f)) - // ...up to here repository.sendTransitionStep(step(0.6f)) + // ...up to here + repository.sendTransitionStep(step(0.8f)) repository.sendTransitionStep(step(1f)) - // Only two values should be present, since the dream overlay runs for a small fraction - // of the overall animation time - assertThat(values.size).isEqualTo(3) - assertThat(values[0]).isEqualTo(animValue(0.3f, LOCKSCREEN_ALPHA)) - assertThat(values[1]).isEqualTo(animValue(0.4f, LOCKSCREEN_ALPHA)) - assertThat(values[2]).isEqualTo(animValue(0.5f, LOCKSCREEN_ALPHA)) + assertThat(values.size).isEqualTo(5) + values.forEach { assertThat(it).isIn(Range.closed(0f, 1f)) } job.cancel() } @@ -86,58 +80,27 @@ class OccludedToLockscreenTransitionViewModelTest : SysuiTestCase() { val job = underTest.lockscreenTranslationY(pixels).onEach { values.add(it) }.launchIn(this) + repository.sendTransitionStep(step(0f, TransitionState.STARTED)) repository.sendTransitionStep(step(0f)) repository.sendTransitionStep(step(0.3f)) repository.sendTransitionStep(step(0.5f)) repository.sendTransitionStep(step(1f)) - assertThat(values.size).isEqualTo(4) - assertThat(values[0]) - .isEqualTo( - -pixels + - EMPHASIZED_DECELERATE.getInterpolation( - animValue(0f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[1]) - .isEqualTo( - -pixels + - EMPHASIZED_DECELERATE.getInterpolation( - animValue(0.3f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[2]) - .isEqualTo( - -pixels + - EMPHASIZED_DECELERATE.getInterpolation( - animValue(0.5f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) - assertThat(values[3]) - .isEqualTo( - -pixels + - EMPHASIZED_DECELERATE.getInterpolation( - animValue(1f, LOCKSCREEN_TRANSLATION_Y) - ) * pixels - ) + assertThat(values.size).isEqualTo(5) + values.forEach { assertThat(it).isIn(Range.closed(-100f, 0f)) } job.cancel() } - private fun animValue(stepValue: Float, params: AnimationParams): Float { - val totalDuration = TO_LOCKSCREEN_DURATION - val startValue = (params.startTime / totalDuration).toFloat() - - val multiplier = (totalDuration / params.duration).toFloat() - return (stepValue - startValue) * multiplier - } - - private fun step(value: Float): TransitionStep { + private fun step( + value: Float, + state: TransitionState = TransitionState.RUNNING + ): TransitionStep { return TransitionStep( from = KeyguardState.OCCLUDED, to = KeyguardState.LOCKSCREEN, value = value, - transitionState = TransitionState.RUNNING, + transitionState = state, ownerName = "OccludedToLockscreenTransitionViewModelTest" ) } From 853661538983ac1fec30b26a3cf83b664e95fd9c Mon Sep 17 00:00:00 2001 From: Matt Pietal Date: Mon, 30 Jan 2023 14:00:56 +0000 Subject: [PATCH 2/2] Transitions - Add DOZING -> GONE Recent changes remove the intermediate LOCKSCREEN state, which would flicker when unlocking directly from DOZE state. The device is no longer told to wake up when using FPS, so support a direct transition between DOZING and GONE. Fixes: 267156993 Test: atest KeyguardTransitionSCenariosTest Change-Id: I72c43c93fed9c7dba43449977f541cb3e19210a7 --- .../FromDozingTransitionInteractor.kt | 24 ++++++++++++ .../KeyguardTransitionScenariosTest.kt | 38 +++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt index ce61f2fec92f7..86f65dde40313 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt @@ -21,6 +21,7 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.Companion.isWakeAndUnlock import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.WakefulnessModel.Companion.isWakingOrStartingToWake @@ -44,6 +45,7 @@ constructor( override fun start() { listenForDozingToLockscreen() + listenForDozingToGone() } private fun listenForDozingToLockscreen() { @@ -68,6 +70,28 @@ constructor( } } + private fun listenForDozingToGone() { + scope.launch { + keyguardInteractor.biometricUnlockState + .sample(keyguardTransitionInteractor.startedKeyguardTransitionStep, ::Pair) + .collect { (biometricUnlockState, lastStartedTransition) -> + if ( + lastStartedTransition.to == KeyguardState.DOZING && + isWakeAndUnlock(biometricUnlockState) + ) { + keyguardTransitionRepository.startTransition( + TransitionInfo( + name, + KeyguardState.DOZING, + KeyguardState.GONE, + getAnimator(), + ) + ) + } + } + } + } + private fun getAnimator(duration: Duration = DEFAULT_DURATION): ValueAnimator { return ValueAnimator().apply { setInterpolator(Interpolators.LINEAR) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index 3a871b4de8bc7..702f376350927 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -24,6 +24,7 @@ import com.android.systemui.animation.Interpolators import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositoryImpl +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.KeyguardState @@ -436,6 +437,43 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { coroutineContext.cancelChildren() } + @Test + fun `DOZING to GONE`() = + testScope.runTest { + // GIVEN a prior transition has run to DOZING + runner.startTransition( + testScope, + TransitionInfo( + ownerName = "", + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DOZING, + animator = + ValueAnimator().apply { + duration = 10 + interpolator = Interpolators.LINEAR + }, + ) + ) + runCurrent() + reset(mockTransitionRepository) + + // WHEN biometrics succeeds with wake and unlock mode + keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) + runCurrent() + + val info = + withArgCaptor { + verify(mockTransitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo("FromDozingTransitionInteractor") + assertThat(info.from).isEqualTo(KeyguardState.DOZING) + assertThat(info.to).isEqualTo(KeyguardState.GONE) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + @Test fun `GONE to DOZING`() = testScope.runTest {