diff options
author | 2024-01-25 14:40:42 +0100 | |
---|---|---|
committer | 2024-01-26 14:43:08 +0100 | |
commit | 18ecc210b772b0b8037f7ee2e8703c1364073a78 (patch) | |
tree | 3095e6dd8503db730d78f57e0b2336e750397476 | |
parent | e5c48c9025cfec2e727caf522df29c91cfdf74fc (diff) |
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
5 files changed, 111 insertions, 19 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt index 8305428aa9c8..58c3be244725 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt @@ -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 diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt index 3a55567d69bb..ac423b70fd50 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt @@ -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>>() diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index a764a52723af..04e093721824 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -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 @@ -31,6 +32,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 * specific scenes, use [from] instead. @@ -64,12 +71,20 @@ 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]. * * For instance, the following will fade `Foo` during the first half of the transition then it diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index b96f9bebb08b..df186a15181e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -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 diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt index ef729921f4cd..140bacaec738 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt @@ -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?>( |