Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +33 −7 Original line number Diff line number Diff line Loading @@ -693,8 +693,8 @@ private fun prepareInterruption( val fromState = updateStateInContent(transition.fromContent) val toState = updateStateInContent(transition.toContent) reconcileStates(element, previousTransition) reconcileStates(element, transition) val previousUniqueState = reconcileStates(element, previousTransition, previousState = null) reconcileStates(element, transition, previousState = previousUniqueState) // Remove the interruption values to all contents but the content(s) where the element will be // placed, to make sure that interruption deltas are computed only right after this interruption Loading @@ -721,12 +721,32 @@ private fun prepareInterruption( /** * Reconcile the state of [element] in the formContent and toContent of [transition] so that the * values before interruption have their expected values, taking shared transitions into account. * * @return the unique state this element had during [transition], `null` if it had multiple * different states (i.e. the shared animation was disabled). */ private fun reconcileStates(element: Element, transition: TransitionState.Transition) { val fromContentState = element.stateByContent[transition.fromContent] ?: return val toContentState = element.stateByContent[transition.toContent] ?: return private fun reconcileStates( element: Element, transition: TransitionState.Transition, previousState: Element.State?, ): Element.State? { fun reconcileWithPreviousState(state: Element.State) { if (previousState != null && state.offsetBeforeInterruption == Offset.Unspecified) { state.updateValuesBeforeInterruption(previousState) } } val fromContentState = element.stateByContent[transition.fromContent] val toContentState = element.stateByContent[transition.toContent] if (fromContentState == null || toContentState == null) { return (fromContentState ?: toContentState) ?.also { reconcileWithPreviousState(it) } ?.takeIf { it.offsetBeforeInterruption != Offset.Unspecified } } if (!isSharedElementEnabled(element.key, transition)) { return return null } if ( Loading @@ -735,13 +755,19 @@ private fun reconcileStates(element: Element, transition: TransitionState.Transi ) { // Element is shared and placed in fromContent only. toContentState.updateValuesBeforeInterruption(fromContentState) } else if ( return fromContentState } if ( toContentState.offsetBeforeInterruption != Offset.Unspecified && fromContentState.offsetBeforeInterruption == Offset.Unspecified ) { // Element is shared and placed in toContent only. fromContentState.updateValuesBeforeInterruption(toContentState) return toContentState } return null } private fun Element.State.selfUpdateValuesBeforeInterruption() { Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +54 −0 Original line number Diff line number Diff line Loading @@ -2638,4 +2638,58 @@ class ElementTest { assertWithMessage("Frame $i didn't replace Foo").that(numberOfPlacements).isEqualTo(0) } } @Test fun interruption_considerPreviousUniqueState() { @Composable fun SceneScope.Foo(modifier: Modifier = Modifier) { Box(modifier.element(TestElements.Foo).size(50.dp)) } val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } val scope = rule.setContentAndCreateMainScope { SceneTransitionLayout(state) { scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } } scene(SceneB) { Box(Modifier.fillMaxSize()) } scene(SceneC) { Box(Modifier.fillMaxSize()) { Foo(Modifier.offset(x = 100.dp, y = 100.dp)) } } } } // During A => B, Foo disappears and stays in its original position. scope.launch { state.startTransition(transition(SceneA, SceneB)) } rule .onNode(isElement(TestElements.Foo)) .assertSizeIsEqualTo(50.dp) .assertPositionInRootIsEqualTo(0.dp, 0.dp) // Interrupt A => B by B => C. var interruptionProgress by mutableFloatStateOf(1f) scope.launch { state.startTransition( transition(SceneB, SceneC, interruptionProgress = { interruptionProgress }) ) } // During B => C, Foo appears again. It is still at (0, 0) when the interruption progress is // 100%, and converges to its position (100, 100) in C. rule .onNode(isElement(TestElements.Foo)) .assertSizeIsEqualTo(50.dp) .assertPositionInRootIsEqualTo(0.dp, 0.dp) interruptionProgress = 0.5f rule .onNode(isElement(TestElements.Foo)) .assertSizeIsEqualTo(50.dp) .assertPositionInRootIsEqualTo(50.dp, 50.dp) interruptionProgress = 0f rule .onNode(isElement(TestElements.Foo)) .assertSizeIsEqualTo(50.dp) .assertPositionInRootIsEqualTo(100.dp, 100.dp) } } Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +33 −7 Original line number Diff line number Diff line Loading @@ -693,8 +693,8 @@ private fun prepareInterruption( val fromState = updateStateInContent(transition.fromContent) val toState = updateStateInContent(transition.toContent) reconcileStates(element, previousTransition) reconcileStates(element, transition) val previousUniqueState = reconcileStates(element, previousTransition, previousState = null) reconcileStates(element, transition, previousState = previousUniqueState) // Remove the interruption values to all contents but the content(s) where the element will be // placed, to make sure that interruption deltas are computed only right after this interruption Loading @@ -721,12 +721,32 @@ private fun prepareInterruption( /** * Reconcile the state of [element] in the formContent and toContent of [transition] so that the * values before interruption have their expected values, taking shared transitions into account. * * @return the unique state this element had during [transition], `null` if it had multiple * different states (i.e. the shared animation was disabled). */ private fun reconcileStates(element: Element, transition: TransitionState.Transition) { val fromContentState = element.stateByContent[transition.fromContent] ?: return val toContentState = element.stateByContent[transition.toContent] ?: return private fun reconcileStates( element: Element, transition: TransitionState.Transition, previousState: Element.State?, ): Element.State? { fun reconcileWithPreviousState(state: Element.State) { if (previousState != null && state.offsetBeforeInterruption == Offset.Unspecified) { state.updateValuesBeforeInterruption(previousState) } } val fromContentState = element.stateByContent[transition.fromContent] val toContentState = element.stateByContent[transition.toContent] if (fromContentState == null || toContentState == null) { return (fromContentState ?: toContentState) ?.also { reconcileWithPreviousState(it) } ?.takeIf { it.offsetBeforeInterruption != Offset.Unspecified } } if (!isSharedElementEnabled(element.key, transition)) { return return null } if ( Loading @@ -735,13 +755,19 @@ private fun reconcileStates(element: Element, transition: TransitionState.Transi ) { // Element is shared and placed in fromContent only. toContentState.updateValuesBeforeInterruption(fromContentState) } else if ( return fromContentState } if ( toContentState.offsetBeforeInterruption != Offset.Unspecified && fromContentState.offsetBeforeInterruption == Offset.Unspecified ) { // Element is shared and placed in toContent only. fromContentState.updateValuesBeforeInterruption(toContentState) return toContentState } return null } private fun Element.State.selfUpdateValuesBeforeInterruption() { Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +54 −0 Original line number Diff line number Diff line Loading @@ -2638,4 +2638,58 @@ class ElementTest { assertWithMessage("Frame $i didn't replace Foo").that(numberOfPlacements).isEqualTo(0) } } @Test fun interruption_considerPreviousUniqueState() { @Composable fun SceneScope.Foo(modifier: Modifier = Modifier) { Box(modifier.element(TestElements.Foo).size(50.dp)) } val state = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) } val scope = rule.setContentAndCreateMainScope { SceneTransitionLayout(state) { scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo() } } scene(SceneB) { Box(Modifier.fillMaxSize()) } scene(SceneC) { Box(Modifier.fillMaxSize()) { Foo(Modifier.offset(x = 100.dp, y = 100.dp)) } } } } // During A => B, Foo disappears and stays in its original position. scope.launch { state.startTransition(transition(SceneA, SceneB)) } rule .onNode(isElement(TestElements.Foo)) .assertSizeIsEqualTo(50.dp) .assertPositionInRootIsEqualTo(0.dp, 0.dp) // Interrupt A => B by B => C. var interruptionProgress by mutableFloatStateOf(1f) scope.launch { state.startTransition( transition(SceneB, SceneC, interruptionProgress = { interruptionProgress }) ) } // During B => C, Foo appears again. It is still at (0, 0) when the interruption progress is // 100%, and converges to its position (100, 100) in C. rule .onNode(isElement(TestElements.Foo)) .assertSizeIsEqualTo(50.dp) .assertPositionInRootIsEqualTo(0.dp, 0.dp) interruptionProgress = 0.5f rule .onNode(isElement(TestElements.Foo)) .assertSizeIsEqualTo(50.dp) .assertPositionInRootIsEqualTo(50.dp, 50.dp) interruptionProgress = 0f rule .onNode(isElement(TestElements.Foo)) .assertSizeIsEqualTo(50.dp) .assertPositionInRootIsEqualTo(100.dp, 100.dp) } }