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

Commit 87bafca1 authored by Jordan Demeulenaere's avatar Jordan Demeulenaere Committed by Android (Google) Code Review
Browse files

Merge changes I591dac9b,I79abd2a7 into main

* changes:
  Set the target state during lookahead pass
  Don't cast ApproachMeasureScope as LookaheadScope
parents 87c1e98f 96f26cee
Loading
Loading
Loading
Loading
+33 −26
Original line number Diff line number Diff line
@@ -33,9 +33,9 @@ import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.layout.ApproachLayoutModifierNode
import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LookaheadScope
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
@@ -248,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)

@@ -272,15 +293,7 @@ internal class ElementNode(
            val placeable = measurable.measure(constraints)
            sceneState.lastSize = placeable.size()

            this as LookaheadScope
            return layout(placeable.width, placeable.height) {
                // Update the offset (relative to the SceneTransitionLayout) this element has in
                // this scene when idle.
                coordinates?.let { coords ->
                    sceneState.targetOffset =
                        lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
                }
            }
            return layout(placeable.width, placeable.height) { /* Do not place */ }
        }

        val placeable =
@@ -294,7 +307,6 @@ internal class ElementNode(
                transition,
                sceneState,
                placeable,
                placementScope = this,
            )
        }
    }
@@ -541,8 +553,7 @@ internal fun shouldDrawOrComposeSharedElement(
            transition = transition,
            fromSceneZIndex = layoutImpl.scenes.getValue(fromScene).zIndex,
            toSceneZIndex = layoutImpl.scenes.getValue(toScene).zIndex,
        )
            ?: return false
        ) ?: return false

    return pickedScene == scene || transition.currentOverscrollSpec?.scene == scene
}
@@ -797,23 +808,19 @@ private fun ContentDrawScope.getDrawScale(
}

@OptIn(ExperimentalComposeUiApi::class)
private fun ApproachMeasureScope.place(
private fun Placeable.PlacementScope.place(
    layoutImpl: SceneTransitionLayoutImpl,
    scene: Scene,
    element: Element,
    transition: TransitionState.Transition?,
    sceneState: Element.SceneState,
    placeable: Placeable,
    placementScope: Placeable.PlacementScope,
) {
    this as LookaheadScope

    with(placementScope) {
    with(layoutImpl.lookaheadScope) {
        // Update the offset (relative to the SceneTransitionLayout) this element has in this scene
        // 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)) {
+9 −0
Original line number Diff line number Diff line
@@ -107,6 +107,13 @@ internal class SceneTransitionLayoutImpl(
                    _userActionDistanceScope = it
                }

    /**
     * The [LookaheadScope] of this layout, that can be used to compute offsets relative to the
     * layout.
     */
    internal lateinit var lookaheadScope: LookaheadScope
        private set

    init {
        updateScenes(builder)

@@ -195,6 +202,8 @@ internal class SceneTransitionLayoutImpl(
                .then(LayoutElement(layoutImpl = this))
        ) {
            LookaheadScope {
                lookaheadScope = this

                BackHandler()

                scenesToCompose().fastForEach { scene -> key(scene.key) { scene.Content() } }
+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) }
        }
    }
}