summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt40
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt23
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt33
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt25
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) }
+ }
+ }
}