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

Commit 20f85cba authored by omarmt's avatar omarmt
Browse files

Improvements for stopping animations in SceneGestureHandler

The following improvements have been made:
- on onDragStarted(): When a new transition begins, the current
animation is stopped and the current drag offset is immediately obtained.
- New log: If the transition is a generic TransitionState.Transition, a
log is added, since it is not fully supported at the moment.
- on onDragStopped(): An animation has been added when the animation is
stopped when the state is TransitionState.Transition.
- on animateOffset(): animateOffset no longer waits for the coroutine to
 complete to reset the isAnimatingOffset state.

Test: atest SceneGestureHandlerTest
Bug: 291025415
Change-Id: I3d04ea2a2c822a2b17b377046d43a31757dcbd19
parent 08a4f97f
Loading
Loading
Loading
Loading
+50 −34
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.compose.animation.scene

import android.util.Log
import androidx.annotation.VisibleForTesting
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
@@ -130,10 +131,12 @@ class SceneGestureHandler(
    internal val currentScene: Scene
        get() = layoutImpl.scene(transitionState.currentScene)

    internal val isDrivingTransition
    @VisibleForTesting
    val isDrivingTransition
        get() = transitionState == swipeTransition

    internal var isAnimatingOffset
    @VisibleForTesting
    var isAnimatingOffset
        get() = swipeTransition.isAnimatingOffset
        private set(value) {
            swipeTransition.isAnimatingOffset = value
@@ -157,17 +160,21 @@ class SceneGestureHandler(
    internal fun onDragStarted() {
        if (isDrivingTransition) {
            // This [transition] was already driving the animation: simply take over it.
            if (isAnimatingOffset) {
                // Stop animating and start from where the current offset. Setting the animation job
                // to `null` will effectively cancel the animation.
            // Stop animating and start from where the current offset.
            swipeTransition.stopOffsetAnimation()
                swipeTransition.dragOffset = swipeTransition.offsetAnimatable.value
            }

            return
        }

        val transition = transitionState
        if (transition is TransitionState.Transition) {
            // TODO(b/290184746): Better handle interruptions here if state != idle.
            Log.w(
                TAG,
                "start from TransitionState.Transition is not fully supported: from" +
                    " ${transition.fromScene} to ${transition.toScene} " +
                    "(progress ${transition.progress})"
            )
        }

        val fromScene = currentScene

@@ -409,8 +416,7 @@ class SceneGestureHandler(
        targetScene: SceneKey,
    ) {
        swipeTransition.startOffsetAnimation {
            coroutineScope
                .launch {
            coroutineScope.launch {
                if (!isAnimatingOffset) {
                    swipeTransition.offsetAnimatable.snapTo(swipeTransition.dragOffset)
                }
@@ -426,15 +432,16 @@ class SceneGestureHandler(
                    initialVelocity = initialVelocity,
                )

                    // Now that the animation is done, the state should be idle. Note that if the
                    // state was changed since this animation started, some external code changed it
                    // and we shouldn't do anything here. Note also that this job will be cancelled
                    // in the case where the user intercepts this swipe.
                isAnimatingOffset = false

                // Now that the animation is done, the state should be idle. Note that if the state
                // was changed since this animation started, some external code changed it and we
                // shouldn't do anything here. Note also that this job will be cancelled in the case
                // where the user intercepts this swipe.
                if (isDrivingTransition) {
                    transitionState = TransitionState.Idle(targetScene)
                }
            }
                .also { it.invokeOnCompletion { isAnimatingOffset = false } }
        }
    }

@@ -490,6 +497,11 @@ class SceneGestureHandler(
        /** Stops any ongoing offset animation. */
        fun stopOffsetAnimation() {
            offsetAnimationJob?.cancel()

            if (isAnimatingOffset) {
                isAnimatingOffset = false
                dragOffset = offsetAnimatable.value
            }
        }

        /** The absolute distance between [fromScene] and [toScene]. */
@@ -503,6 +515,10 @@ class SceneGestureHandler(
        val distance: Float
            get() = _distance
    }

    companion object {
        private const val TAG = "SceneGestureHandler"
    }
}

private class SceneDraggableHandler(
+32 −1
Original line number Diff line number Diff line
@@ -47,7 +47,7 @@ class SceneGestureHandlerTest {
            scene(SceneC) { Text("SceneC") }
        }

        private val sceneGestureHandler =
        val sceneGestureHandler =
            SceneGestureHandler(
                layoutImpl =
                    SceneTransitionLayoutImpl(
@@ -81,6 +81,10 @@ class SceneGestureHandlerTest {
            coroutineScope.testScheduler.advanceUntilIdle()
        }

        fun runCurrent() {
            coroutineScope.testScheduler.runCurrent()
        }

        fun assertScene(currentScene: SceneKey, isIdle: Boolean) {
            val idleMsg = if (isIdle) "MUST" else "MUST NOT"
            assertWithMessage("transitionState $idleMsg be Idle")
@@ -164,6 +168,33 @@ class SceneGestureHandlerTest {
        assertScene(currentScene = SceneA, isIdle = true)
    }

    @Test
    fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest {
        draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
        assertScene(currentScene = SceneA, isIdle = false)

        draggable.onDelta(pixels = deltaInPixels10)
        assertScene(currentScene = SceneA, isIdle = false)

        draggable.onDragStopped(
            coroutineScope = coroutineScope,
            velocity = velocityThreshold,
        )

        // The stop animation is not started yet
        assertThat(sceneGestureHandler.isAnimatingOffset).isFalse()

        runCurrent()

        assertThat(sceneGestureHandler.isAnimatingOffset).isTrue()
        assertThat(sceneGestureHandler.isDrivingTransition).isTrue()
        assertScene(currentScene = SceneC, isIdle = false)

        // Start a new gesture while the offset is animating
        draggable.onDragStarted(coroutineScope = coroutineScope, startedPosition = Offset.Zero)
        assertThat(sceneGestureHandler.isAnimatingOffset).isFalse()
    }

    @Test
    fun onInitialPreScroll_doNotChangeState() = runGestureTest {
        nestedScroll.onPreScroll(available = offsetY10, source = NestedScrollSource.Drag)