Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +13 −5 Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -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 Loading @@ -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 Loading @@ -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) } } } Loading @@ -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 { Loading Loading @@ -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 Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt +11 −0 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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)" } } packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt +8 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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), Loading @@ -491,6 +496,7 @@ internal class SceneGestureHandler( } internal class SwipeTransition( val key: TransitionKey?, val _fromScene: Scene, val _toScene: Scene, /** Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +33 −19 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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, * ) * ) * ) { ... } Loading @@ -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%. * Loading @@ -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 Loading @@ -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 { Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +18 −10 Original line number Diff line number Diff line Loading @@ -92,6 +92,7 @@ sealed interface MutableSceneTransitionLayoutState : SceneTransitionLayoutState fun setTargetScene( targetScene: SceneKey, coroutineScope: CoroutineScope, transitionKey: TransitionKey? = null, ): TransitionState.Transition? } Loading Loading @@ -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 Loading Loading @@ -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, ) } } } Loading @@ -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 Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +13 −5 Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -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 Loading @@ -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 Loading @@ -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) } } } Loading @@ -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 { Loading Loading @@ -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 Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt +11 −0 Original line number Diff line number Diff line Loading @@ -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 { Loading Loading @@ -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)" } }
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt +8 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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), Loading @@ -491,6 +496,7 @@ internal class SceneGestureHandler( } internal class SwipeTransition( val key: TransitionKey?, val _fromScene: Scene, val _toScene: Scene, /** Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +33 −19 Original line number Diff line number Diff line Loading @@ -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( Loading @@ -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, * ) * ) * ) { ... } Loading @@ -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%. * Loading @@ -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 Loading @@ -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 { Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +18 −10 Original line number Diff line number Diff line Loading @@ -92,6 +92,7 @@ sealed interface MutableSceneTransitionLayoutState : SceneTransitionLayoutState fun setTargetScene( targetScene: SceneKey, coroutineScope: CoroutineScope, transitionKey: TransitionKey? = null, ): TransitionState.Transition? } Loading Loading @@ -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 Loading Loading @@ -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, ) } } } Loading @@ -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