diff options
8 files changed, 98 insertions, 11 deletions
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt index c2dd80375d5a..ea740a8d8de4 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt @@ -48,8 +48,15 @@ internal fun CoroutineScope.animateToScene( } return when (transitionState) { - is TransitionState.Idle -> - animate(layoutState, target, transitionKey, isInitiatedByUserInput = false) + is TransitionState.Idle -> { + animate( + layoutState, + target, + transitionKey, + isInitiatedByUserInput = false, + replacedTransition = null, + ) + } is TransitionState.Transition -> { val isInitiatedByUserInput = transitionState.isInitiatedByUserInput @@ -79,6 +86,7 @@ internal fun CoroutineScope.animateToScene( isInitiatedByUserInput, initialProgress = progress, initialVelocity = transitionState.progressVelocity, + replacedTransition = transitionState, ) } } else if (transitionState.fromScene == target) { @@ -101,6 +109,7 @@ internal fun CoroutineScope.animateToScene( initialProgress = progress, initialVelocity = transitionState.progressVelocity, reversed = true, + replacedTransition = transitionState, ) } } else { @@ -137,6 +146,7 @@ internal fun CoroutineScope.animateToScene( isInitiatedByUserInput, fromScene = animateFrom, chain = chain, + replacedTransition = null, ) } } @@ -148,6 +158,7 @@ private fun CoroutineScope.animate( targetScene: SceneKey, transitionKey: TransitionKey?, isInitiatedByUserInput: Boolean, + replacedTransition: TransitionState.Transition?, initialProgress: Float = 0f, initialVelocity: Float = 0f, reversed: Boolean = false, @@ -164,6 +175,7 @@ private fun CoroutineScope.animate( currentScene = targetScene, isInitiatedByUserInput = isInitiatedByUserInput, isUserInputOngoing = false, + replacedTransition = replacedTransition, ) } else { OneOffTransition( @@ -173,6 +185,7 @@ private fun CoroutineScope.animate( currentScene = targetScene, isInitiatedByUserInput = isInitiatedByUserInput, isUserInputOngoing = false, + replacedTransition = replacedTransition, ) } @@ -214,7 +227,8 @@ private class OneOffTransition( override val currentScene: SceneKey, override val isInitiatedByUserInput: Boolean, override val isUserInputOngoing: Boolean, -) : TransitionState.Transition(fromScene, toScene) { + replacedTransition: TransitionState.Transition?, +) : TransitionState.Transition(fromScene, toScene, replacedTransition) { /** * The animatable used to animate this transition. * diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index da968ac2b2dd..e313aa52faea 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -37,7 +37,7 @@ import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Job import kotlinx.coroutines.launch -interface DraggableHandler { +internal interface DraggableHandler { /** * Start a drag in the given [startedPosition], with the given [overSlop] and number of * [pointersDown]. @@ -51,7 +51,7 @@ interface DraggableHandler { * The [DragController] provides control over the transition between two scenes through the [onDrag] * and [onStop] methods. */ -interface DragController { +internal interface DragController { /** Drag the current scene by [delta] pixels. */ fun onDrag(delta: Float) @@ -537,6 +537,7 @@ private fun SwipeTransition( orientation = orientation, isUpOrLeft = isUpOrLeft, requiresFullDistanceSwipe = result.requiresFullDistanceSwipe, + replacedTransition = null, ) } @@ -553,6 +554,7 @@ private fun SwipeTransition(old: SwipeTransition): SwipeTransition { isUpOrLeft = old.isUpOrLeft, lastDistance = old.lastDistance, requiresFullDistanceSwipe = old.requiresFullDistanceSwipe, + replacedTransition = old, ) .apply { _currentScene = old._currentScene @@ -571,9 +573,10 @@ private class SwipeTransition( override val orientation: Orientation, override val isUpOrLeft: Boolean, val requiresFullDistanceSwipe: Boolean, + replacedTransition: SwipeTransition?, var lastDistance: Float = DistanceUnspecified, ) : - TransitionState.Transition(_fromScene.key, _toScene.key), + TransitionState.Transition(_fromScene.key, _toScene.key, replacedTransition), TransitionState.HasOverscrollProperties { var _currentScene by mutableStateOf(_fromScene) override val currentScene: SceneKey diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index d4f1ad10c6b4..69124c1f6dad 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -495,6 +495,10 @@ private fun prepareInterruption( transition: TransitionState.Transition, previousTransition: TransitionState.Transition, ) { + if (transition.replacedTransition == previousTransition) { + return + } + val sceneStates = element.sceneStates fun updatedSceneState(key: SceneKey): Element.SceneState? { return sceneStates[key]?.also { it.selfUpdateValuesBeforeInterruption() } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index a8df6f4ae8b1..5b4fbf036083 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -224,6 +224,9 @@ sealed interface TransitionState { /** The scene this transition is going to. Can't be the same as fromScene */ val toScene: SceneKey, + + /** The transition that `this` transition is replacing, if any. */ + internal val replacedTransition: Transition? = null, ) : TransitionState { /** * The key of this transition. This should usually be null, but it can be specified to use a @@ -279,6 +282,11 @@ sealed interface TransitionState { init { check(fromScene != toScene) + check( + replacedTransition == null || + (replacedTransition.fromScene == fromScene && + replacedTransition.toScene == toScene) + ) } /** @@ -321,6 +329,10 @@ sealed interface TransitionState { return 0f } + if (replacedTransition != null) { + return replacedTransition.interruptionProgress(layoutImpl) + } + fun create(): Animatable<Float, AnimationVector1D> { val animatable = Animatable(1f, visibilityThreshold = ProgressVisibilityThreshold) layoutImpl.coroutineScope.launch { @@ -521,6 +533,10 @@ internal abstract class BaseSceneTransitionLayoutState( check(transitionStates.size == 1) check(transitionStates[0] is TransitionState.Idle) transitionStates = listOf(transition) + } else if (currentState == transition.replacedTransition) { + // Replace the transition. + transitionStates = + transitionStates.subList(0, transitionStates.lastIndex) + transition } else { // Append the new transition. transitionStates = transitionStates + transition diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index c738ad36f2e2..55a342cc13c7 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -1233,4 +1233,17 @@ class DraggableHandlerTest { advanceUntilIdle() assertIdle(SceneB) } + + @Test + fun interceptingTransitionReplacesCurrentTransition() = runGestureTest { + val controller = onDragStarted(overSlop = up(fractionOfScreen = 0.5f)) + val transition = assertThat(layoutState.transitionState).isTransition() + controller.onDragStopped(velocity = 0f) + + // Intercept the transition. + onDragStartedImmediately() + val newTransition = assertThat(layoutState.transitionState).isTransition() + assertThat(newTransition).isNotSameInstanceAs(transition) + assertThat(newTransition.replacedTransition).isSameInstanceAs(transition) + } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 7c20a976d2fc..9646bc2fbca6 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -2088,4 +2088,42 @@ class ElementTest { rule.onNode(isElement(TestElements.Foo, SceneA)).assertPositionInRootIsEqualTo(20.dp, 30.dp) rule.onNode(isElement(TestElements.Foo, SceneB)).assertIsNotDisplayed() } + + @Test + fun replacedTransitionDoesNotTriggerInterruption() = runTest { + val state = rule.runOnIdle { MutableSceneTransitionLayoutStateImpl(SceneA) } + + @Composable + fun SceneScope.Foo(modifier: Modifier = Modifier) { + Box(modifier.element(TestElements.Foo).size(10.dp)) + } + + rule.setContent { + SceneTransitionLayout(state) { + scene(SceneA) { Foo() } + scene(SceneB) { Foo(Modifier.offset(40.dp, 60.dp)) } + } + } + + // Start A => B at 50%. + val aToB1 = + transition(from = SceneA, to = SceneB, progress = { 0.5f }, onFinish = neverFinish()) + rule.runOnUiThread { state.startTransition(aToB1) } + rule.onNode(isElement(TestElements.Foo, SceneA)).assertIsNotDisplayed() + rule.onNode(isElement(TestElements.Foo, SceneB)).assertPositionInRootIsEqualTo(20.dp, 30.dp) + + // Replace A => B by another A => B at 100%. Even with interruption progress at 100%, Foo + // should be at (40dp, 60dp) given that aToB1 was replaced by aToB2. + val aToB2 = + transition( + from = SceneA, + to = SceneB, + progress = { 1f }, + interruptionProgress = { 1f }, + replacedTransition = aToB1, + ) + rule.runOnUiThread { state.startTransition(aToB2) } + rule.onNode(isElement(TestElements.Foo, SceneA)).assertIsNotDisplayed() + rule.onNode(isElement(TestElements.Foo, SceneB)).assertPositionInRootIsEqualTo(40.dp, 60.dp) + } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt index 09d1a827d0c7..3552d3d0bc1b 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt @@ -131,10 +131,6 @@ class InterruptionHandlerTest { assertThat(state.currentTransitions) .comparingElementsUsing(FromToCurrentTriple) .containsExactly( - // Initial transition A to B. This transition will never be consumed by anyone given - // that it has the same (from, to) pair as the next transition. - Triple(SceneA, SceneB, SceneB), - // Initial transition reversed, B back to A. Triple(SceneA, SceneB, SceneA), diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt index 322b0355c2bd..65f4f9e6e393 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt @@ -37,8 +37,11 @@ fun transition( bouncingScene: SceneKey? = null, orientation: Orientation = Orientation.Horizontal, onFinish: ((TransitionState.Transition) -> Job)? = null, + replacedTransition: TransitionState.Transition? = null, ): TransitionState.Transition { - return object : TransitionState.Transition(from, to), TransitionState.HasOverscrollProperties { + return object : + TransitionState.Transition(from, to, replacedTransition), + TransitionState.HasOverscrollProperties { override val currentScene: SceneKey get() = current() |