Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 96f26cee authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Set the target state during lookahead pass

This CL makes sure that we set the target state of each element during
the lookahead pass rather than during the approach pass. This ensures
that the target state of all elements is set before starting the
approach pass, making some transformations with anchors work
independently of the order of elements in the UI hierarchy.

Bug: 290930950
Test: atest AnchoredTranslateTest
Flag: com.android.systemui.scene_container
Change-Id: I591dac9b30c4a27e0f1f8500f04a7008cf912590
parent c92dbaa3
Loading
Loading
Loading
Loading
+26 −14
Original line number Diff line number Diff line
@@ -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 ApproachMeasureScope.approachMeasure(
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints,
        constraints: Constraints
    ): MeasureResult {
        check(isLookingAhead)

        return measurable.measure(constraints).run {
            // Update the size this element has in this scene when idle.
        sceneState.targetSize = lookaheadSize
            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 {
        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)) {
+14 −9
Original line number Diff line number Diff line
@@ -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(
            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,
            )
            } else {
                value
            }
        }

        // This simple implementation assumes that the size of [element] is the same as the size of
+30 −3
Original line number Diff line number Diff line
@@ -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()
    )
}
+25 −0
Original line number Diff line number Diff line
@@ -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) }
        }
    }
}