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

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

Merge "Make it possible to change the swipe animation spec" into main

parents 0fd55d7e 18ecc210
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?>(