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 Original line 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.ApproachLayoutModifierNode
import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.ApproachMeasureScope
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.ModifierNodeElement
@@ -248,13 +248,34 @@ internal class ElementNode(
    }
    }


    @ExperimentalComposeUiApi
    @ExperimentalComposeUiApi
    override fun ApproachMeasureScope.approachMeasure(
    override fun MeasureScope.measure(
        measurable: Measurable,
        measurable: Measurable,
        constraints: Constraints,
        constraints: Constraints
    ): MeasureResult {
    ): MeasureResult {
        check(isLookingAhead)

        return measurable.measure(constraints).run {
            // Update the size this element has in this scene when idle.
            // 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 transitions = currentTransitions
        val transition = elementTransition(element, transitions)
        val transition = elementTransition(element, transitions)


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


            this as LookaheadScope
            return layout(placeable.width, placeable.height) { /* Do not place */ }
            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)
                }
            }
        }
        }


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


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


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

    with(placementScope) {
        // Update the offset (relative to the SceneTransitionLayout) this element has in this scene
        // Update the offset (relative to the SceneTransitionLayout) this element has in this scene
        // when idle.
        // when idle.
        val coords = coordinates ?: error("Element ${element.key} does not have any coordinates")
        val coords = coordinates ?: error("Element ${element.key} does not have any coordinates")
        val targetOffsetInScene = lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
        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.
        // No need to place the element in this scene if we don't want to draw it anyways.
        if (!shouldPlaceElement(layoutImpl, scene, element, transition)) {
        if (!shouldPlaceElement(layoutImpl, scene, element, transition)) {
+9 −0
Original line number Original line Diff line number Diff line
@@ -107,6 +107,13 @@ internal class SceneTransitionLayoutImpl(
                    _userActionDistanceScope = it
                    _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 {
    init {
        updateScenes(builder)
        updateScenes(builder)


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

                BackHandler()
                BackHandler()


                scenesToCompose().fastForEach { scene -> key(scene.key) { scene.Content() } }
                scenesToCompose().fastForEach { scene -> key(scene.key) { scene.Content() } }
+14 −9
Original line number Original line Diff line number Diff line
@@ -41,15 +41,20 @@ internal class AnchoredSize(
        value: IntSize,
        value: IntSize,
    ): IntSize {
    ): IntSize {
        fun anchorSizeIn(scene: SceneKey): IntSize {
        fun anchorSizeIn(scene: SceneKey): IntSize {
            val size = layoutImpl.elements[anchor]?.sceneStates?.get(scene)?.targetSize
            val size =
            return if (size != null && size != Element.SizeUnspecified) {
                layoutImpl.elements[anchor]?.sceneStates?.get(scene)?.targetSize?.takeIf {
                IntSize(
                    it != Element.SizeUnspecified
                }
                    ?: throwMissingAnchorException(
                        transformation = "AnchoredSize",
                        anchor = anchor,
                        scene = scene,
                    )

            return IntSize(
                width = if (anchorWidth) size.width else value.width,
                width = if (anchorWidth) size.width else value.width,
                height = if (anchorHeight) size.height else value.height,
                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
        // This simple implementation assumes that the size of [element] is the same as the size of
+30 −3
Original line number Original line Diff line number Diff line
@@ -39,7 +39,15 @@ internal class AnchoredTranslate(
        transition: TransitionState.Transition,
        transition: TransitionState.Transition,
        value: Offset,
        value: Offset,
    ): 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? {
        fun anchorOffsetIn(scene: SceneKey): Offset? {
            return anchor.sceneStates[scene]?.targetOffset?.takeIf { it.isSpecified }
            return anchor.sceneStates[scene]?.targetOffset?.takeIf { it.isSpecified }
        }
        }
@@ -47,8 +55,10 @@ internal class AnchoredTranslate(
        // [element] will move the same amount as [anchor] does.
        // [element] will move the same amount as [anchor] does.
        // TODO(b/290184746): Also support anchors that are not shared but translated because of
        // TODO(b/290184746): Also support anchors that are not shared but translated because of
        // other transformations, like an edge translation.
        // other transformations, like an edge translation.
        val anchorFromOffset = anchorOffsetIn(transition.fromScene) ?: return value
        val anchorFromOffset =
        val anchorToOffset = anchorOffsetIn(transition.toScene) ?: return value
            anchorOffsetIn(transition.fromScene) ?: throwException(transition.fromScene)
        val anchorToOffset =
            anchorOffsetIn(transition.toScene) ?: throwException(transition.toScene)
        val offset = anchorToOffset - anchorFromOffset
        val offset = anchorToOffset - anchorFromOffset


        return if (scene.key == transition.toScene) {
        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 Original line Diff line number Diff line
@@ -27,6 +27,7 @@ import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.compose.animation.scene.TestElements
import com.android.compose.animation.scene.TestElements
import com.android.compose.animation.scene.testTransition
import com.android.compose.animation.scene.testTransition
import com.android.compose.animation.scene.transition
import org.junit.Rule
import org.junit.Rule
import org.junit.Test
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runner.RunWith
@@ -83,4 +84,28 @@ class AnchoredTranslateTest {
            after { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) }
            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) }
        }
    }
}
}