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

Commit c8139fc6 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere Committed by Android (Google) Code Review
Browse files

Merge "Make it possible to cancel swipes (1/2)" into main

parents 7ca9a1a0 c5645ba3
Loading
Loading
Loading
Loading
+18 −4
Original line number Diff line number Diff line
@@ -348,6 +348,8 @@ internal class SceneGestureHandler(
            // Compute the destination scene (and therefore offset) to settle in.
            val offset = swipeTransition.dragOffset
            val distance = swipeTransition.distance
            var targetScene: Scene
            var targetOffset: Float
            if (
                shouldCommitSwipe(
                    offset,
@@ -356,12 +358,24 @@ internal class SceneGestureHandler(
                    wasCommitted = swipeTransition._currentScene == toScene,
                )
            ) {
                // Animate to the next scene
                animateTo(targetScene = toScene, targetOffset = distance)
                targetScene = toScene
                targetOffset = distance
            } else {
                // Animate to the initial scene
                animateTo(targetScene = fromScene, targetOffset = 0f)
                targetScene = fromScene
                targetOffset = 0f
            }

            if (
                targetScene != swipeTransition._currentScene &&
                    !layoutState.canChangeScene(targetScene.key)
            ) {
                // We wanted to change to a new scene but we are not allowed to, so we animate back
                // to the current scene.
                targetScene = swipeTransition._currentScene
                targetOffset = if (targetScene == fromScene) 0f else distance
            }

            animateTo(targetScene = targetScene, targetOffset = targetOffset)
        } else {
            // We are doing an overscroll animation between scenes. In this case, we can also start
            // from the idle position.
+6 −1
Original line number Diff line number Diff line
@@ -232,7 +232,12 @@ internal class SceneTransitionLayoutImpl(
                scene(state.transitionState.currentScene).userActions[Back]?.let { result ->
                    // TODO(b/290184746): Handle predictive back and use result.distance if
                    // specified.
                    BackHandler { with(state) { coroutineScope.onChangeScene(result.toScene) } }
                    BackHandler {
                        val targetScene = result.toScene
                        if (state.canChangeScene(targetScene)) {
                            with(state) { coroutineScope.onChangeScene(targetScene) }
                        }
                    }
                }

                Box {
+47 −5
Original line number Diff line number Diff line
@@ -101,13 +101,30 @@ sealed interface MutableSceneTransitionLayoutState : SceneTransitionLayoutState
    ): TransitionState.Transition?
}

/** Return a [MutableSceneTransitionLayoutState] initially idle at [initialScene]. */
/**
 * Return a [MutableSceneTransitionLayoutState] initially idle at [initialScene].
 *
 * @param initialScene the initial scene to which this state is initialized.
 * @param transitions the [SceneTransitions] used when this state is transitioning between scenes.
 * @param canChangeScene whether we can transition to the given scene. This is called when the user
 *   commits a transition to a new scene because of a [UserAction]. If [canChangeScene] returns
 *   `true`, then the gesture will be committed and we will animate to the other scene. Otherwise,
 *   the gesture will be cancelled and we will animate back to the current scene.
 * @param stateLinks the [StateLink] connecting this [SceneTransitionLayoutState] to other
 *   [SceneTransitionLayoutState]s.
 */
fun MutableSceneTransitionLayoutState(
    initialScene: SceneKey,
    transitions: SceneTransitions = SceneTransitions.Empty,
    canChangeScene: (SceneKey) -> Boolean = { true },
    stateLinks: List<StateLink> = emptyList(),
): MutableSceneTransitionLayoutState {
    return MutableSceneTransitionLayoutStateImpl(initialScene, transitions, stateLinks)
    return MutableSceneTransitionLayoutStateImpl(
        initialScene,
        transitions,
        canChangeScene,
        stateLinks,
    )
}

/**
@@ -120,18 +137,32 @@ fun MutableSceneTransitionLayoutState(
 *   This is called when the user commits a transition to a new scene because of a [UserAction], for
 *   instance by triggering back navigation or by swiping to a new scene.
 * @param transitions the definition of the transitions used to animate a change of scene.
 * @param canChangeScene whether we can transition to the given scene. This is called when the user
 *   commits a transition to a new scene because of a [UserAction]. If [canChangeScene] returns
 *   `true`, then [onChangeScene] will be called right afterwards with the same [SceneKey]. If it
 *   returns `false`, the user action will be cancelled and we will animate back to the current
 *   scene.
 * @param stateLinks the [StateLink] connecting this [SceneTransitionLayoutState] to other
 *   [SceneTransitionLayoutState]s.
 */
@Composable
fun updateSceneTransitionLayoutState(
    currentScene: SceneKey,
    onChangeScene: (SceneKey) -> Unit,
    transitions: SceneTransitions = SceneTransitions.Empty,
    canChangeScene: (SceneKey) -> Boolean = { true },
    stateLinks: List<StateLink> = emptyList(),
): SceneTransitionLayoutState {
    return remember {
            HoistedSceneTransitionLayoutScene(currentScene, transitions, onChangeScene, stateLinks)
            HoistedSceneTransitionLayoutScene(
                currentScene,
                transitions,
                onChangeScene,
                canChangeScene,
                stateLinks,
            )
        }
        .apply { update(currentScene, onChangeScene, transitions, stateLinks) }
        .apply { update(currentScene, onChangeScene, canChangeScene, transitions, stateLinks) }
}

@Stable
@@ -208,6 +239,9 @@ internal abstract class BaseSceneTransitionLayoutState(

    private val activeTransitionLinks = mutableMapOf<StateLink, LinkedTransition>()

    /** Whether we can transition to the given [scene]. */
    internal abstract fun canChangeScene(scene: SceneKey): Boolean

    /**
     * Called when the [current scene][TransitionState.currentScene] should be changed to [scene].
     *
@@ -334,21 +368,26 @@ internal class HoistedSceneTransitionLayoutScene(
    initialScene: SceneKey,
    override var transitions: SceneTransitions,
    private var changeScene: (SceneKey) -> Unit,
    private var canChangeScene: (SceneKey) -> Boolean,
    stateLinks: List<StateLink> = emptyList(),
) : BaseSceneTransitionLayoutState(initialScene, stateLinks) {
    private val targetSceneChannel = Channel<SceneKey>(Channel.CONFLATED)

    override fun CoroutineScope.onChangeScene(scene: SceneKey) = changeScene(scene)
    override fun canChangeScene(scene: SceneKey): Boolean = canChangeScene.invoke(scene)

    override fun CoroutineScope.onChangeScene(scene: SceneKey) = changeScene.invoke(scene)

    @Composable
    fun update(
        currentScene: SceneKey,
        onChangeScene: (SceneKey) -> Unit,
        canChangeScene: (SceneKey) -> Boolean,
        transitions: SceneTransitions,
        stateLinks: List<StateLink>,
    ) {
        SideEffect {
            this.changeScene = onChangeScene
            this.canChangeScene = canChangeScene
            this.transitions = transitions
            this.stateLinks = stateLinks

@@ -374,6 +413,7 @@ internal class HoistedSceneTransitionLayoutScene(
internal class MutableSceneTransitionLayoutStateImpl(
    initialScene: SceneKey,
    override var transitions: SceneTransitions,
    private val canChangeScene: (SceneKey) -> Boolean = { true },
    stateLinks: List<StateLink> = emptyList(),
) : MutableSceneTransitionLayoutState, BaseSceneTransitionLayoutState(initialScene, stateLinks) {
    override fun setTargetScene(
@@ -388,6 +428,8 @@ internal class MutableSceneTransitionLayoutStateImpl(
        )
    }

    override fun canChangeScene(scene: SceneKey): Boolean = canChangeScene.invoke(scene)

    override fun CoroutineScope.onChangeScene(scene: SceneKey) {
        setTargetScene(scene, coroutineScope = this)
    }
+43 −1
Original line number Diff line number Diff line
@@ -51,8 +51,13 @@ class SceneGestureHandlerTest {
    private class TestGestureScope(
        private val testScope: MonotonicClockTestScope,
    ) {
        var canChangeScene: (SceneKey) -> Boolean = { true }
        private val layoutState =
            MutableSceneTransitionLayoutStateImpl(SceneA, EmptyTestTransitions)
            MutableSceneTransitionLayoutStateImpl(
                SceneA,
                EmptyTestTransitions,
                canChangeScene = { canChangeScene(it) },
            )

        val mutableUserActionsA = mutableMapOf(Swipe.Up to SceneB, Swipe.Down to SceneC)
        val mutableUserActionsB = mutableMapOf(Swipe.Up to SceneC, Swipe.Down to SceneA)
@@ -890,4 +895,41 @@ class SceneGestureHandlerTest {
        )
        assertThat(transitionState).isNotSameInstanceAs(firstTransition)
    }

    @Test
    fun blockTransition() = runGestureTest {
        assertIdle(SceneA)

        // Swipe up to scene B.
        onDragStarted(overSlop = up(0.1f))
        assertTransition(currentScene = SceneA, fromScene = SceneA, toScene = SceneB)

        // Block the transition when the user release their finger.
        canChangeScene = { false }
        onDragStopped(velocity = -velocityThreshold)
        advanceUntilIdle()
        assertIdle(SceneA)
    }

    @Test
    fun blockInterceptedTransition() = runGestureTest {
        assertIdle(SceneA)

        // Swipe up to B.
        onDragStarted(overSlop = up(0.1f))
        assertTransition(currentScene = SceneA, fromScene = SceneA, toScene = SceneB)
        onDragStopped(velocity = -velocityThreshold)
        assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB)

        // Intercept the transition and swipe down back to scene A.
        assertThat(sceneGestureHandler.shouldImmediatelyIntercept(startedPosition = null)).isTrue()
        onDragStartedImmediately()

        // Block the transition when the user release their finger.
        canChangeScene = { false }
        onDragStopped(velocity = velocityThreshold)

        advanceUntilIdle()
        assertIdle(SceneB)
    }
}