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

Commit 18ecc210 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Make it possible to change the swipe animation spec

This CL adds a way to change the default spring spec used when animating
a transition that started with a user gesture.

Bug: 322316041
Test: TransitionDslTest
Flag: N/A
Change-Id: Ia1310340580d9910d45e93067aee0658559953ba
parent e5c48c90
Loading
Loading
Loading
Loading
+23 −13
Original line number Diff line number Diff line
@@ -20,8 +20,7 @@ package com.android.compose.animation.scene

import android.util.Log
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.SpringSpec
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
@@ -54,7 +53,20 @@ internal class SceneGestureHandler(
        }

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

            // Initialize SwipeTransition.swipeSpec. Note that this must be called right after
            // layoutState.startTransition() is called, because it computes the
            // layoutState.transformationSpec().
            newTransition.swipeSpec =
                layoutState.transformationSpec.swipeSpec ?: layoutState.transitions.defaultSwipeSpec
        } else {
            // We were not driving the transition and we don't force the update, so the spec won't
            // be used and it doesn't matter which one we set here.
            newTransition.swipeSpec = SceneTransitions.DefaultSwipeSpec
        }

        swipeTransition = newTransition
    }

@@ -491,7 +503,7 @@ internal class SceneGestureHandler(
         * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is
         * above or to the left of [toScene].
         */
        val distance: Float
        val distance: Float,
    ) : TransitionState.Transition(_fromScene.key, _toScene.key) {
        var _currentScene by mutableStateOf(_fromScene)
        override val currentScene: SceneKey
@@ -524,6 +536,9 @@ internal class SceneGestureHandler(
        /** Job to check that there is at most one offset animation in progress. */
        private var offsetAnimationJob: Job? = null

        /** The spec to use when animating this transition to either [fromScene] or [toScene]. */
        lateinit var swipeSpec: SpringSpec<Float>

        /** Ends any previous [offsetAnimationJob] and runs the new [job]. */
        private fun startOffsetAnimation(job: () -> Job) {
            cancelOffsetAnimation()
@@ -545,13 +560,6 @@ internal class SceneGestureHandler(
            }
        }

        // TODO(b/290184746): Make this spring spec configurable.
        private val animationSpec =
            spring(
                stiffness = Spring.StiffnessMediumLow,
                visibilityThreshold = OffsetVisibilityThreshold
            )

        fun animateOffset(
            // TODO(b/317063114) The CoroutineScope should be removed.
            coroutineScope: CoroutineScope,
@@ -575,7 +583,7 @@ internal class SceneGestureHandler(

            offsetAnimatable.animateTo(
                targetValue = targetOffset,
                animationSpec = animationSpec,
                animationSpec = swipeSpec,
                initialVelocity = initialVelocity,
            )

@@ -811,4 +819,6 @@ internal class SceneNestedScrollHandler(
 * The number of pixels below which there won't be a visible difference in the transition and from
 * which the animation can stop.
 */
private const val OffsetVisibilityThreshold = 0.5f
// TODO(b/290184746): Have a better default visibility threshold which takes the swipe distance into
// account instead.
internal const val OffsetVisibilityThreshold = 0.5f
+30 −3
Original line number Diff line number Diff line
@@ -17,7 +17,10 @@
package com.android.compose.animation.scene

import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.snap
import androidx.compose.animation.core.spring
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.fastForEach
@@ -36,6 +39,7 @@ import com.android.compose.animation.scene.transformation.Translate
/** The transitions configuration of a [SceneTransitionLayout]. */
class SceneTransitions
internal constructor(
    internal val defaultSwipeSpec: SpringSpec<Float>,
    internal val transitionSpecs: List<TransitionSpecImpl>,
) {
    private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpecImpl>>()
@@ -91,7 +95,13 @@ internal constructor(
        TransitionSpecImpl(from, to, TransformationSpec.EmptyProvider)

    companion object {
        val Empty = SceneTransitions(transitionSpecs = emptyList())
        internal val DefaultSwipeSpec =
            spring(
                stiffness = Spring.StiffnessMediumLow,
                visibilityThreshold = OffsetVisibilityThreshold,
            )

        val Empty = SceneTransitions(DefaultSwipeSpec, transitionSpecs = emptyList())
    }
}

@@ -125,15 +135,30 @@ interface TransitionSpec {
}

interface TransformationSpec {
    /** The [AnimationSpec] used to animate the associated transition progress. */
    /**
     * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when
     * the transition is triggered (i.e. it is not gesture-based).
     */
    val progressSpec: AnimationSpec<Float>

    /**
     * The [SpringSpec] used to animate the associated transition progress when the transition was
     * started by a swipe and is now animating back to a scene because the user lifted their finger.
     *
     * If `null`, then the [SceneTransitions.defaultSwipeSpec] will be used.
     */
    val swipeSpec: SpringSpec<Float>?

    /** The list of [Transformation] applied to elements during this transition. */
    val transformations: List<Transformation>

    companion object {
        internal val Empty =
            TransformationSpecImpl(progressSpec = snap(), transformations = emptyList())
            TransformationSpecImpl(
                progressSpec = snap(),
                swipeSpec = null,
                transformations = emptyList(),
            )
        internal val EmptyProvider = { Empty }
    }
}
@@ -151,6 +176,7 @@ internal class TransitionSpecImpl(
                val reverse = transformationSpec.invoke()
                TransformationSpecImpl(
                    progressSpec = reverse.progressSpec,
                    swipeSpec = reverse.swipeSpec,
                    transformations = reverse.transformations.map { it.reversed() }
                )
            }
@@ -166,6 +192,7 @@ internal class TransitionSpecImpl(
 */
internal class TransformationSpecImpl(
    override val progressSpec: AnimationSpec<Float>,
    override val swipeSpec: SpringSpec<Float>?,
    override val transformations: List<Transformation>,
) : TransformationSpec {
    private val cache = mutableMapOf<ElementKey, MutableMap<SceneKey, ElementTransformations>>()
+17 −2
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.compose.animation.scene

import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.SpringSpec
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -30,6 +31,12 @@ fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {

@TransitionDsl
interface SceneTransitionsBuilder {
    /**
     * The default [AnimationSpec] used when after the user lifts their finger after starting a
     * swipe to transition, to animate back into one of the 2 scenes we are transitioning to.
     */
    var defaultSwipeSpec: SpringSpec<Float>

    /**
     * Define the default animation to be played when transitioning [to] the specified scene, from
     * any scene. For the animation specification to apply only when transitioning between two
@@ -64,11 +71,19 @@ interface SceneTransitionsBuilder {
@TransitionDsl
interface TransitionBuilder : PropertyTransformationBuilder {
    /**
     * The [AnimationSpec] used to animate the progress of this transition from `0` to `1` when
     * performing programmatic (not input pointer tracking) animations.
     * The [AnimationSpec] used to animate the associated transition progress from `0` to `1` when
     * the transition is triggered (i.e. it is not gesture-based).
     */
    var spec: AnimationSpec<Float>

    /**
     * The [SpringSpec] used to animate the associated transition progress when the transition was
     * started by a swipe and is now animating back to a scene because the user lifted their finger.
     *
     * If `null`, then the [SceneTransitionsBuilder.defaultSwipeSpec] will be used.
     */
    var swipeSpec: SpringSpec<Float>?

    /**
     * Define a progress-based range for the transformations inside [builder].
     *
+6 −1
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package com.android.compose.animation.scene
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.animation.core.DurationBasedAnimationSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.VectorConverter
import androidx.compose.animation.core.spring
import androidx.compose.ui.geometry.Offset
@@ -40,10 +41,12 @@ internal fun transitionsImpl(
    builder: SceneTransitionsBuilder.() -> Unit,
): SceneTransitions {
    val impl = SceneTransitionsBuilderImpl().apply(builder)
    return SceneTransitions(impl.transitionSpecs)
    return SceneTransitions(impl.defaultSwipeSpec, impl.transitionSpecs)
}

private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
    override var defaultSwipeSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec

    val transitionSpecs = mutableListOf<TransitionSpecImpl>()

    override fun to(to: SceneKey, builder: TransitionBuilder.() -> Unit): TransitionSpec {
@@ -67,6 +70,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
            val impl = TransitionBuilderImpl().apply(builder)
            return TransformationSpecImpl(
                progressSpec = impl.spec,
                swipeSpec = impl.swipeSpec,
                transformations = impl.transformations,
            )
        }
@@ -80,6 +84,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
internal class TransitionBuilderImpl : TransitionBuilder {
    val transformations = mutableListOf<Transformation>()
    override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
    override var swipeSpec: SpringSpec<Float>? = null

    private var range: TransformationRange? = null
    private var reversed = false
+35 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.compose.animation.scene

import androidx.compose.animation.core.SpringSpec
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.transformation.Transformation
@@ -188,6 +189,40 @@ class TransitionDslTest {
            )
    }

    @Test
    fun springSpec() {
        val defaultSpec = spring<Float>(stiffness = 1f)
        val specFromAToC = spring<Float>(stiffness = 2f)
        val transitions = transitions {
            defaultSwipeSpec = defaultSpec

            from(TestScenes.SceneA, to = TestScenes.SceneB) {
                // Default swipe spec.
            }
            from(TestScenes.SceneA, to = TestScenes.SceneC) { swipeSpec = specFromAToC }
        }

        assertThat(transitions.defaultSwipeSpec).isSameInstanceAs(defaultSpec)

        // A => B does not have a custom spec.
        assertThat(
                transitions
                    .transitionSpec(from = TestScenes.SceneA, to = TestScenes.SceneB)
                    .transformationSpec()
                    .swipeSpec
            )
            .isNull()

        // A => C has a custom swipe spec.
        assertThat(
                transitions
                    .transitionSpec(from = TestScenes.SceneA, to = TestScenes.SceneC)
                    .transformationSpec()
                    .swipeSpec
            )
            .isSameInstanceAs(specFromAToC)
    }

    companion object {
        private val TRANSFORMATION_RANGE =
            Correspondence.transforming<Transformation, TransformationRange?>(