diff options
4 files changed, 95 insertions, 26 deletions
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 f1f84dca6b8e..11d384196b5d 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 @@ -35,6 +35,7 @@ import androidx.compose.ui.layout.ApproachMeasureScope import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.Placeable import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.node.ModifierNodeElement @@ -247,13 +248,34 @@ internal class ElementNode( } @ExperimentalComposeUiApi + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints + ): MeasureResult { + check(isLookingAhead) + + return measurable.measure(constraints).run { + // Update the size this element has in this scene when idle. + sceneState.targetSize = size() + + layout(width, height) { + // Update the offset (relative to the SceneTransitionLayout) this element has in + // this scene when idle. + coordinates?.let { coords -> + with(layoutImpl.lookaheadScope) { + sceneState.targetOffset = + lookaheadScopeCoordinates.localLookaheadPositionOf(coords) + } + } + place(0, 0) + } + } + } + override fun ApproachMeasureScope.approachMeasure( measurable: Measurable, constraints: Constraints, ): MeasureResult { - // Update the size this element has in this scene when idle. - sceneState.targetSize = lookaheadSize - val transitions = currentTransitions val transition = elementTransition(element, transitions) @@ -271,16 +293,7 @@ internal class ElementNode( val placeable = measurable.measure(constraints) sceneState.lastSize = placeable.size() - return layout(placeable.width, placeable.height) { - // Update the offset (relative to the SceneTransitionLayout) this element has in - // this scene when idle. - coordinates?.let { coords -> - with(layoutImpl.lookaheadScope) { - sceneState.targetOffset = - lookaheadScopeCoordinates.localLookaheadPositionOf(coords) - } - } - } + return layout(placeable.width, placeable.height) { /* Do not place */ } } val placeable = @@ -808,7 +821,6 @@ private fun Placeable.PlacementScope.place( // when idle. val coords = coordinates ?: error("Element ${element.key} does not have any coordinates") val targetOffsetInScene = lookaheadScopeCoordinates.localLookaheadPositionOf(coords) - sceneState.targetOffset = targetOffsetInScene // No need to place the element in this scene if we don't want to draw it anyways. if (!shouldPlaceElement(layoutImpl, scene, element, transition)) { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt index 124ec290f42a..b54afae70a55 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt @@ -41,15 +41,20 @@ internal class AnchoredSize( value: IntSize, ): IntSize { fun anchorSizeIn(scene: SceneKey): IntSize { - val size = layoutImpl.elements[anchor]?.sceneStates?.get(scene)?.targetSize - return if (size != null && size != Element.SizeUnspecified) { - IntSize( - width = if (anchorWidth) size.width else value.width, - height = if (anchorHeight) size.height else value.height, - ) - } else { - value - } + val size = + layoutImpl.elements[anchor]?.sceneStates?.get(scene)?.targetSize?.takeIf { + it != Element.SizeUnspecified + } + ?: throwMissingAnchorException( + transformation = "AnchoredSize", + anchor = anchor, + scene = scene, + ) + + return IntSize( + width = if (anchorWidth) size.width else value.width, + height = if (anchorHeight) size.height else value.height, + ) } // This simple implementation assumes that the size of [element] is the same as the size of diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt index 7aa702b0bbd2..2bab4f88ffdd 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt @@ -39,7 +39,15 @@ internal class AnchoredTranslate( transition: TransitionState.Transition, value: Offset, ): Offset { - val anchor = layoutImpl.elements[anchor] ?: return value + fun throwException(scene: SceneKey?): Nothing { + throwMissingAnchorException( + transformation = "AnchoredTranslate", + anchor = anchor, + scene = scene, + ) + } + + val anchor = layoutImpl.elements[anchor] ?: throwException(scene = null) fun anchorOffsetIn(scene: SceneKey): Offset? { return anchor.sceneStates[scene]?.targetOffset?.takeIf { it.isSpecified } } @@ -47,8 +55,10 @@ internal class AnchoredTranslate( // [element] will move the same amount as [anchor] does. // TODO(b/290184746): Also support anchors that are not shared but translated because of // other transformations, like an edge translation. - val anchorFromOffset = anchorOffsetIn(transition.fromScene) ?: return value - val anchorToOffset = anchorOffsetIn(transition.toScene) ?: return value + val anchorFromOffset = + anchorOffsetIn(transition.fromScene) ?: throwException(transition.fromScene) + val anchorToOffset = + anchorOffsetIn(transition.toScene) ?: throwException(transition.toScene) val offset = anchorToOffset - anchorFromOffset return if (scene.key == transition.toScene) { @@ -64,3 +74,20 @@ internal class AnchoredTranslate( } } } + +internal fun throwMissingAnchorException( + transformation: String, + anchor: ElementKey, + scene: SceneKey?, +): Nothing { + error( + """ + Anchor ${anchor.debugName} does not have a target state in scene ${scene?.debugName}. + This either means that it was not composed at all during the transition or that it was + composed too late, for instance during layout/subcomposition. To avoid flickers in + $transformation, you should make sure that the composition and layout of anchor is *not* + deferred, for instance by moving it out of lazy layouts. + """ + .trimIndent() + ) +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt index d1205e727cf9..46075c3b3f9f 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.animation.scene.TestElements import com.android.compose.animation.scene.testTransition +import com.android.compose.animation.scene.transition import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -83,4 +84,28 @@ class AnchoredTranslateTest { after { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) } } } + + @Test + fun anchorPlacedAfterAnchoredElement() { + rule.testTransition( + fromSceneContent = { Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo)) }, + toSceneContent = { + Box(Modifier.offset(20.dp, 40.dp).element(TestElements.Bar)) + Box(Modifier.offset(30.dp, 10.dp).element(TestElements.Foo)) + }, + transition = { + spec = tween(16 * 4, easing = LinearEasing) + anchoredTranslate(TestElements.Bar, TestElements.Foo) + }, + ) { + // No exception is thrown even if Bar is placed before the anchor in toScene. + before { onElement(TestElements.Bar).assertDoesNotExist() } + at(0) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(0.dp, 80.dp) } + at(16) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(5.dp, 70.dp) } + at(32) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(10.dp, 60.dp) } + at(48) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(15.dp, 50.dp) } + at(64) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) } + after { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) } + } + } } |