Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt +23 −13 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 } Loading Loading @@ -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 Loading Loading @@ -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() Loading @@ -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, Loading @@ -575,7 +583,7 @@ internal class SceneGestureHandler( offsetAnimatable.animateTo( targetValue = targetOffset, animationSpec = animationSpec, animationSpec = swipeSpec, initialVelocity = initialVelocity, ) Loading Loading @@ -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 packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt +30 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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>>() Loading Loading @@ -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()) } } Loading Loading @@ -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 } } } Loading @@ -151,6 +176,7 @@ internal class TransitionSpecImpl( val reverse = transformationSpec.invoke() TransformationSpecImpl( progressSpec = reverse.progressSpec, swipeSpec = reverse.swipeSpec, transformations = reverse.transformations.map { it.reversed() } ) } Loading @@ -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>>() Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +17 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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]. * Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +6 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 { Loading @@ -67,6 +70,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { val impl = TransitionBuilderImpl().apply(builder) return TransformationSpecImpl( progressSpec = impl.spec, swipeSpec = impl.swipeSpec, transformations = impl.transformations, ) } Loading @@ -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 Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt +35 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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?>( Loading Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt +23 −13 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 } Loading Loading @@ -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 Loading Loading @@ -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() Loading @@ -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, Loading @@ -575,7 +583,7 @@ internal class SceneGestureHandler( offsetAnimatable.animateTo( targetValue = targetOffset, animationSpec = animationSpec, animationSpec = swipeSpec, initialVelocity = initialVelocity, ) Loading Loading @@ -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
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt +30 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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>>() Loading Loading @@ -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()) } } Loading Loading @@ -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 } } } Loading @@ -151,6 +176,7 @@ internal class TransitionSpecImpl( val reverse = transformationSpec.invoke() TransformationSpecImpl( progressSpec = reverse.progressSpec, swipeSpec = reverse.swipeSpec, transformations = reverse.transformations.map { it.reversed() } ) } Loading @@ -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>>() Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +17 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading Loading @@ -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]. * Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +6 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 { Loading @@ -67,6 +70,7 @@ private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { val impl = TransitionBuilderImpl().apply(builder) return TransformationSpecImpl( progressSpec = impl.spec, swipeSpec = impl.swipeSpec, transformations = impl.transformations, ) } Loading @@ -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 Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/TransitionDslTest.kt +35 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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?>( Loading