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

Commit aa58dfb9 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Introduce TransitionKey

This CL introduces TransitionKey, which can be used to specify which
transition should be used when transitioning between scenes. It can be
specified both for user gestures through UserActionResult or for
triggered animations using MutableSceneTransitionLayoutState. Note that
there is no API for triggered animations backed by a hoisted
SceneTransitionLayoutState.

Bug: 315921512
Test: SceneTransitionLayoutStateTest
Test: SwipeToSceneTest
Flag: N/A
Change-Id: I2cdc2d78da41c8bb1b3234e09096faa15c3f265c
parent 6b135b01
Loading
Loading
Loading
Loading
+13 −5
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import kotlinx.coroutines.launch
internal fun CoroutineScope.animateToScene(
    layoutState: BaseSceneTransitionLayoutState,
    target: SceneKey,
    transitionKey: TransitionKey?,
): TransitionState.Transition? {
    val transitionState = layoutState.transitionState
    if (transitionState.currentScene == target) {
@@ -45,7 +46,7 @@ internal fun CoroutineScope.animateToScene(
    }

    return when (transitionState) {
        is TransitionState.Idle -> animate(layoutState, target)
        is TransitionState.Idle -> animate(layoutState, target, transitionKey)
        is TransitionState.Transition -> {
            // A transition is currently running: first check whether `transition.toScene` or
            // `transition.fromScene` is the same as our target scene, in which case the transition
@@ -67,7 +68,7 @@ internal fun CoroutineScope.animateToScene(
                    // 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, startProgress = progress)
                    animate(layoutState, target, transitionKey, startProgress = progress)
                }
            } else if (transitionState.fromScene == target) {
                // There is a transition from [target] to another scene: simply animate the same
@@ -82,12 +83,18 @@ internal fun CoroutineScope.animateToScene(
                    null
                } else {
                    // TODO(b/290184746): Also take the current velocity into account.
                    animate(layoutState, target, startProgress = progress, reversed = true)
                    animate(
                        layoutState,
                        target,
                        transitionKey,
                        startProgress = progress,
                        reversed = true,
                    )
                }
            } else {
                // Generic interruption; the current transition is neither from or to [target].
                // TODO(b/290930950): Better handle interruptions here.
                animate(layoutState, target)
                animate(layoutState, target, transitionKey)
            }
        }
    }
@@ -96,6 +103,7 @@ internal fun CoroutineScope.animateToScene(
private fun CoroutineScope.animate(
    layoutState: BaseSceneTransitionLayoutState,
    target: SceneKey,
    transitionKey: TransitionKey?,
    startProgress: Float = 0f,
    reversed: Boolean = false,
): TransitionState.Transition {
@@ -127,7 +135,7 @@ private fun CoroutineScope.animate(
    // Change the current layout state to start this new transition. This will compute the
    // TransformationSpec associated to this transition, which we need to initialize the Animatable
    // that will actually animate it.
    layoutState.startTransition(transition)
    layoutState.startTransition(transition, transitionKey)

    // The transformation now contains the spec that we should use to instantiate the Animatable.
    val animationSpec = layoutState.transformationSpec.progressSpec
+11 −0
Original line number Diff line number Diff line
@@ -55,6 +55,7 @@ class SceneKey(

    // Implementation of [UserActionResult].
    override val toScene: SceneKey = this
    override val transitionKey: TransitionKey? = null
    override val distance: UserActionDistance? = null

    override fun toString(): String {
@@ -104,3 +105,13 @@ class ValueKey(debugName: String, identity: Any = Object()) : Key(debugName, ide
        return "ValueKey(debugName=$debugName)"
    }
}

/**
 * Key for a transition. This can be used to specify which transition spec should be used when
 * starting the transition between two scenes.
 */
class TransitionKey(debugName: String, identity: Any = Object()) : Key(debugName, identity) {
    override fun toString(): String {
        return "TransitionKey(debugName=$debugName)"
    }
}
+8 −2
Original line number Diff line number Diff line
@@ -54,7 +54,7 @@ internal class SceneGestureHandler(

    private fun updateTransition(newTransition: SwipeTransition, force: Boolean = false) {
        if (isDrivingTransition || force) {
            layoutState.startTransition(newTransition)
            layoutState.startTransition(newTransition, newTransition.key)

            // Initialize SwipeTransition.swipeSpec. Note that this must be called right after
            // layoutState.startTransition() is called, because it computes the
@@ -237,7 +237,11 @@ internal class SceneGestureHandler(
                }
        swipeTransition.dragOffset += acceleratedOffset

        if (isNewFromScene || result.toScene != swipeTransition.toScene) {
        if (
            isNewFromScene ||
                result.toScene != swipeTransition.toScene ||
                result.transitionKey != swipeTransition.key
        ) {
            updateTransition(
                SwipeTransition(fromScene, result).apply {
                    this.dragOffset = swipeTransition.dragOffset
@@ -484,6 +488,7 @@ internal class SceneGestureHandler(

    private fun SwipeTransition(fromScene: Scene, result: UserActionResult): SwipeTransition {
        return SwipeTransition(
            result.transitionKey,
            fromScene,
            layoutImpl.scene(result.toScene),
            computeAbsoluteDistance(fromScene, result),
@@ -491,6 +496,7 @@ internal class SceneGestureHandler(
    }

    internal class SwipeTransition(
        val key: TransitionKey?,
        val _fromScene: Scene,
        val _toScene: Scene,
        /**
+33 −19
Original line number Diff line number Diff line
@@ -388,8 +388,9 @@ interface SwipeSourceDetector {
/**
 * The result of performing a [UserAction].
 *
 * Note: [UserActionResult] is implemented by [SceneKey], and you can also use [withDistance] to
 * easily create a [UserActionResult] with a fixed distance:
 * Note: [UserActionResult] is implemented by [SceneKey], so you can also use scene keys directly
 * when defining your [UserActionResult]s.
 *
 * ```
 * SceneTransitionLayout(...) {
 *     scene(
@@ -397,7 +398,7 @@ interface SwipeSourceDetector {
 *         userActions =
 *             mapOf(
 *                 Swipe.Right to Scene.Bar,
 *                 Swipe.Down to Scene.Doe withDistance 100.dp,
 *                 Swipe.Down to Scene.Doe,
 *             )
 *         )
 *     ) { ... }
@@ -408,6 +409,9 @@ interface UserActionResult {
    /** The scene we should be transitioning to during the [UserAction]. */
    val toScene: SceneKey

    /** The key of the transition that should be used. */
    val transitionKey: TransitionKey?

    /**
     * The distance the action takes to animate from 0% to 100%.
     *
@@ -416,6 +420,32 @@ interface UserActionResult {
    val distance: UserActionDistance?
}

/** Create a [UserActionResult] to [toScene] with the given [distance] and [transitionKey]. */
fun UserActionResult(
    toScene: SceneKey,
    distance: UserActionDistance? = null,
    transitionKey: TransitionKey? = null,
): UserActionResult {
    return object : UserActionResult {
        override val toScene: SceneKey = toScene
        override val transitionKey: TransitionKey? = transitionKey
        override val distance: UserActionDistance? = distance
    }
}

/** Create a [UserActionResult] to [toScene] with the given fixed [distance] and [transitionKey]. */
fun UserActionResult(
    toScene: SceneKey,
    distance: Dp,
    transitionKey: TransitionKey? = null,
): UserActionResult {
    return UserActionResult(
        toScene = toScene,
        distance = FixedDistance(distance),
        transitionKey = transitionKey,
    )
}

interface UserActionDistance {
    /**
     * Return the **absolute** distance of the user action given the size of the scene we are
@@ -424,22 +454,6 @@ interface UserActionDistance {
    fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float
}

/**
 * A utility function to make it possible to define user actions with a distance using the syntax
 * `Swipe.Up to Scene.foo withDistance 100.dp`
 */
infix fun Pair<UserAction, SceneKey>.withDistance(
    distance: Dp
): Pair<UserAction, UserActionResult> {
    val scene = second
    val distance = FixedDistance(distance)
    return first to
        object : UserActionResult {
            override val toScene: SceneKey = scene
            override val distance: UserActionDistance = distance
        }
}

/** The user action has a fixed [absoluteDistance]. */
private class FixedDistance(private val distance: Dp) : UserActionDistance {
    override fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float {
+18 −10
Original line number Diff line number Diff line
@@ -92,6 +92,7 @@ sealed interface MutableSceneTransitionLayoutState : SceneTransitionLayoutState
    fun setTargetScene(
        targetScene: SceneKey,
        coroutineScope: CoroutineScope,
        transitionKey: TransitionKey? = null,
    ): TransitionState.Transition?
}

@@ -213,11 +214,14 @@ internal abstract class BaseSceneTransitionLayoutState(initialScene: SceneKey) :
    }

    /** Start a new [transition], instantly interrupting any ongoing transition if there was one. */
    internal fun startTransition(transition: TransitionState.Transition) {
    internal fun startTransition(
        transition: TransitionState.Transition,
        transitionKey: TransitionKey?,
    ) {
        // Compute the [TransformationSpec] when the transition starts.
        transformationSpec =
            transitions
                .transitionSpec(transition.fromScene, transition.toScene)
                .transitionSpec(transition.fromScene, transition.toScene, key = transitionKey)
                .transformationSpec()

        transitionState = transition
@@ -265,7 +269,11 @@ internal class HoistedSceneTransitionLayoutScene(
                // Inspired by AnimateAsState.kt: let's poll the last value to avoid being one frame
                // late.
                val newKey = targetSceneChannel.tryReceive().getOrNull() ?: newKey
                animateToScene(layoutState = this@HoistedSceneTransitionLayoutScene, newKey)
                animateToScene(
                    layoutState = this@HoistedSceneTransitionLayoutScene,
                    target = newKey,
                    transitionKey = null,
                )
            }
        }
    }
@@ -278,15 +286,15 @@ internal class MutableSceneTransitionLayoutStateImpl(
) : MutableSceneTransitionLayoutState, BaseSceneTransitionLayoutState(initialScene) {
    override fun setTargetScene(
        targetScene: SceneKey,
        coroutineScope: CoroutineScope
        coroutineScope: CoroutineScope,
        transitionKey: TransitionKey?,
    ): TransitionState.Transition? {
        return with(this) {
            coroutineScope.animateToScene(
        return coroutineScope.animateToScene(
            layoutState = this@MutableSceneTransitionLayoutStateImpl,
            target = targetScene,
            transitionKey = transitionKey,
        )
    }
    }

    override fun CoroutineScope.onChangeScene(scene: SceneKey) {
        setTargetScene(scene, coroutineScope = this)
Loading