Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cd120883 authored by Matt Pietal's avatar Matt Pietal
Browse files

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
parent d2fe4b9e
Loading
Loading
Loading
Loading
+0 −38
Original line number Diff line number Diff line
@@ -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<KeyguardState> =
        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<TransitionStep>,
        params: AnimationParams,
        totalDuration: Duration,
    ): Flow<Float> {
        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 }
    }
}
+0 −25
Original line number Diff line number Diff line
/*
 * 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,
)
+106 −0
Original line number Diff line number Diff line
/*
 * 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<TransitionStep>,
) {
    /**
     * 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<Float> {
        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()
    }
}
+28 −32
Original line number Diff line number Diff line
@@ -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<Float> {
        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<Float> = flowForAnimation(DREAM_OVERLAY_ALPHA).map { 1f - it }
    val dreamOverlayAlpha: Flow<Float> =
        transitionAnimation.createFlow(
            duration = 250.milliseconds,
            onStep = { 1f - it },
        )

    /** Lockscreen views y-translation */
    fun lockscreenTranslationY(translatePx: Int): Flow<Float> {
        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<Float> = flowForAnimation(LOCKSCREEN_ALPHA)

    private fun flowForAnimation(params: AnimationParams): Flow<Float> {
        return interactor.transitionStepAnimation(
            interactor.dreamingToLockscreenTransition,
            params,
            totalDuration = TO_LOCKSCREEN_DURATION
    val lockscreenAlpha: Flow<Float> =
        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)
    }
}
+18 −27
Original line number Diff line number Diff line
@@ -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<Float> {
        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<Float> = flowForAnimation(LOCKSCREEN_ALPHA).map { 1f - it }

    private fun flowForAnimation(params: AnimationParams): Flow<Float> {
        return interactor.transitionStepAnimation(
            interactor.goneToDreamingTransition,
            params,
            totalDuration = TO_DREAMING_DURATION
    val lockscreenAlpha: Flow<Float> =
        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)
    }
}
Loading