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

Commit 2975d22e authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Take the progress velocity into account during triggered transitions

Bug: 290930950
Test: InterruptionHandlerTest
Flag: N/A
Change-Id: I068aeb037c2ba06867d38c8c87e8cc91872b7206
parent 73791a8b
Loading
Loading
Loading
Loading
+11 −7
Original line number Diff line number Diff line
@@ -71,13 +71,13 @@ internal fun CoroutineScope.animateToScene(
                } else {
                    // The transition is in progress: start the canned animation at the same
                    // progress as it was in.
                    // TODO(b/290184746): Also take the current velocity into account.
                    animate(
                        layoutState,
                        target,
                        transitionKey,
                        isInitiatedByUserInput,
                        startProgress = progress,
                        initialProgress = progress,
                        initialVelocity = transitionState.progressVelocity,
                    )
                }
            } else if (transitionState.fromScene == target) {
@@ -92,13 +92,13 @@ internal fun CoroutineScope.animateToScene(
                    layoutState.finishTransition(transitionState, target)
                    null
                } else {
                    // TODO(b/290184746): Also take the current velocity into account.
                    animate(
                        layoutState,
                        target,
                        transitionKey,
                        isInitiatedByUserInput,
                        startProgress = progress,
                        initialProgress = progress,
                        initialVelocity = transitionState.progressVelocity,
                        reversed = true,
                    )
                }
@@ -148,7 +148,8 @@ private fun CoroutineScope.animate(
    targetScene: SceneKey,
    transitionKey: TransitionKey?,
    isInitiatedByUserInput: Boolean,
    startProgress: Float = 0f,
    initialProgress: Float = 0f,
    initialVelocity: Float = 0f,
    reversed: Boolean = false,
    fromScene: SceneKey = layoutState.transitionState.currentScene,
    chain: Boolean = true,
@@ -184,13 +185,13 @@ private fun CoroutineScope.animate(
    val visibilityThreshold =
        (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
    val animatable =
        Animatable(startProgress, visibilityThreshold = visibilityThreshold).also {
        Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also {
            transition.animatable = it
        }

    // Animate the progress to its target value.
    transition.job =
        launch { animatable.animateTo(targetProgress, animationSpec) }
        launch { animatable.animateTo(targetProgress, animationSpec, initialVelocity) }
            .apply {
                invokeOnCompletion {
                    // Settle the state to Idle(target). Note that this will do nothing if this
@@ -225,6 +226,9 @@ private class OneOffTransition(
    override val progress: Float
        get() = animatable.value

    override val progressVelocity: Float
        get() = animatable.velocity

    override fun finish(): Job = job
}

+12 −0
Original line number Diff line number Diff line
@@ -579,6 +579,18 @@ private class SwipeTransition(
            return offset / distance
        }

    override val progressVelocity: Float
        get() {
            val animatable = offsetAnimation?.animatable ?: return 0f
            val distance = distance()
            if (distance == DistanceUnspecified) {
                return 0f
            }

            val velocityInDistanceUnit = animatable.velocity
            return velocityInDistanceUnit / distance.absoluteValue
        }

    override val isInitiatedByUserInput = true

    override var bouncingScene: SceneKey? = null
+3 −0
Original line number Diff line number Diff line
@@ -227,6 +227,9 @@ sealed interface TransitionState {
         */
        abstract val progress: Float

        /** The current velocity of [progress], in progress units. */
        abstract val progressVelocity: Float

        /** Whether the transition was triggered by user input rather than being programmatic. */
        abstract val isInitiatedByUserInput: Boolean

+3 −0
Original line number Diff line number Diff line
@@ -45,5 +45,8 @@ internal class LinkedTransition(
    override val progress: Float
        get() = originalTransition.progress

    override val progressVelocity: Float
        get() = originalTransition.progressVelocity

    override fun finish(): Job = originalTransition.finish()
}
+55 −0
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import com.android.compose.animation.scene.TestScenes.SceneC
import com.android.compose.test.runMonotonicClockTest
import com.google.common.truth.Correspondence
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.launch
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -142,6 +143,60 @@ class InterruptionHandlerTest {
            .inOrder()
    }

    @Test
    fun animateToFromScene() = runMonotonicClockTest {
        val state = MutableSceneTransitionLayoutStateImpl(SceneA, transitions {})

        // Fake a transition from A to B that has a non 0 velocity.
        val progressVelocity = 1f
        val aToB =
            transition(
                from = SceneA,
                to = SceneB,
                current = { SceneB },
                // Progress must be > visibility threshold otherwise we will directly snap to A.
                progress = { 0.5f },
                progressVelocity = { progressVelocity },
                onFinish = { launch {} },
            )
        state.startTransition(aToB, transitionKey = null)

        // Animate back to A. The previous transition is reversed, i.e. it has the same (from, to)
        // pair, and its velocity is used when animating the progress back to 0.
        val bToA = checkNotNull(state.setTargetScene(SceneA, coroutineScope = this))
        testScheduler.runCurrent()
        assertThat(bToA.fromScene).isEqualTo(SceneA)
        assertThat(bToA.toScene).isEqualTo(SceneB)
        assertThat(bToA.currentScene).isEqualTo(SceneA)
        assertThat(bToA.progressVelocity).isEqualTo(progressVelocity)
    }

    @Test
    fun animateToToScene() = runMonotonicClockTest {
        val state = MutableSceneTransitionLayoutStateImpl(SceneA, transitions {})

        // Fake a transition from A to B with current scene = A that has a non 0 velocity.
        val progressVelocity = -1f
        val aToB =
            transition(
                from = SceneA,
                to = SceneB,
                current = { SceneA },
                progressVelocity = { progressVelocity },
                onFinish = { launch {} },
            )
        state.startTransition(aToB, transitionKey = null)

        // Animate to B. The previous transition is reversed, i.e. it has the same (from, to) pair,
        // and its velocity is used when animating the progress to 1.
        val bToA = checkNotNull(state.setTargetScene(SceneB, coroutineScope = this))
        testScheduler.runCurrent()
        assertThat(bToA.fromScene).isEqualTo(SceneA)
        assertThat(bToA.toScene).isEqualTo(SceneB)
        assertThat(bToA.currentScene).isEqualTo(SceneB)
        assertThat(bToA.progressVelocity).isEqualTo(progressVelocity)
    }

    companion object {
        val FromToCurrentTriple =
            Correspondence.transforming(
Loading