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

Commit 9b5fb2d8 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "STL ElementStateScope exposes approachSize()" into main

parents 850e122f e944f379
Loading
Loading
Loading
Loading
+28 −3
Original line number Diff line number Diff line
@@ -52,6 +52,7 @@ import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastForEachReversed
import androidx.compose.ui.util.lerp
import com.android.compose.animation.scene.Element.Companion.SizeUnspecified
import com.android.compose.animation.scene.content.Content
import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
@@ -105,6 +106,13 @@ internal class Element(val key: ElementKey) {
        var targetSize by mutableStateOf(SizeUnspecified)
        var targetOffset by mutableStateOf(Offset.Unspecified)

        /**
         * The *approach* state of this element in this content, i.e. the intermediate layout state
         * during transitions, used for smooth animation. Note: These values are computed before
         * measuring the children.
         */
        var approachSize by mutableStateOf(SizeUnspecified)

        /** The last state this element had in this content. */
        var lastOffset = Offset.Unspecified
        var lastSize = SizeUnspecified
@@ -340,7 +348,11 @@ internal class ElementNode(
    override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
        // TODO(b/324191441): Investigate whether making this check more complex (checking if this
        // element is shared or transformed) would lead to better performance.
        return isAnyStateTransitioning()
        val isTransitioning = isAnyStateTransitioning()
        if (!isTransitioning) {
            stateInContent.approachSize = SizeUnspecified
        }
        return isTransitioning
    }

    override fun Placeable.PlacementScope.isPlacementApproachInProgress(
@@ -392,6 +404,7 @@ internal class ElementNode(
            // sharedElement isn't part of either but the element is still rendered as part of
            // the underlying scene that is currently not being transitioned.
            val currentState = currentTransitionStates.last().last()
            stateInContent.approachSize = Element.SizeUnspecified
            val shouldPlaceInThisContent =
                elementContentWhenIdle(
                    layoutImpl,
@@ -409,7 +422,14 @@ internal class ElementNode(
        val transition = elementState as? TransitionState.Transition

        val placeable =
            measure(layoutImpl, element, transition, stateInContent, measurable, constraints)
            approachMeasure(
                layoutImpl = layoutImpl,
                element = element,
                transition = transition,
                stateInContent = stateInContent,
                measurable = measurable,
                constraints = constraints,
            )
        stateInContent.lastSize = placeable.size()
        return layout(placeable.width, placeable.height) { place(elementState, placeable) }
    }
@@ -1183,7 +1203,7 @@ private fun interruptedAlpha(
    )
}

private fun measure(
private fun approachMeasure(
    layoutImpl: SceneTransitionLayoutImpl,
    element: Element,
    transition: TransitionState.Transition?,
@@ -1214,6 +1234,7 @@ private fun measure(
    maybePlaceable?.let { placeable ->
        stateInContent.sizeBeforeInterruption = Element.SizeUnspecified
        stateInContent.sizeInterruptionDelta = IntSize.Zero
        stateInContent.approachSize = Element.SizeUnspecified
        return placeable
    }

@@ -1236,6 +1257,10 @@ private fun measure(
                )
            },
        )

    // Important: Set approachSize before child measurement. Could be used for their calculations.
    stateInContent.approachSize = interruptedSize

    return measurable.measure(
        Constraints.fixed(
            interruptedSize.width.coerceAtLeast(0),
+7 −0
Original line number Diff line number Diff line
@@ -159,6 +159,13 @@ interface ElementStateScope {
     */
    fun ElementKey.targetSize(content: ContentKey): IntSize?

    /**
     * Return the *approaching* size of [this] element in the given [content], i.e. thethe size the
     * element when is transitioning, or `null` if the element is not composed and measured in that
     * content (yet).
     */
    fun ElementKey.approachSize(content: ContentKey): IntSize?

    /**
     * Return the *target* offset of [this] element in the given [content], i.e. the size of the
     * element when idle, or `null` if the element is not composed and placed in that content (yet).
+6 −0
Original line number Diff line number Diff line
@@ -31,6 +31,12 @@ internal class ElementStateScopeImpl(private val layoutImpl: SceneTransitionLayo
        }
    }

    override fun ElementKey.approachSize(content: ContentKey): IntSize? {
        return layoutImpl.elements[this]?.stateByContent?.get(content)?.approachSize.takeIf {
            it != Element.SizeUnspecified
        }
    }

    override fun ElementKey.targetOffset(content: ContentKey): Offset? {
        return layoutImpl.elements[this]?.stateByContent?.get(content)?.targetOffset.takeIf {
            it != Offset.Unspecified
+72 −0
Original line number Diff line number Diff line
@@ -2312,4 +2312,76 @@ class ElementTest {

        assertThat(compositions).isEqualTo(1)
    }

    @Test
    fun measureElementApproachSizeBeforeChildren() {
        val state =
            rule.runOnUiThread {
                MutableSceneTransitionLayoutStateForTests(SceneA, SceneTransitions.Empty)
            }

        lateinit var fooHeight: () -> Dp?
        val fooHeightPreChildMeasure = mutableListOf<Dp?>()

        val scope =
            rule.setContentAndCreateMainScope {
                val density = LocalDensity.current
                SceneTransitionLayoutForTesting(state) {
                    scene(SceneA) {
                        fooHeight = {
                            with(density) { TestElements.Foo.approachSize(SceneA)?.height?.toDp() }
                        }
                        Box(Modifier.element(TestElements.Foo).size(200.dp)) {
                            Box(
                                Modifier.approachLayout(
                                    isMeasurementApproachInProgress = { false },
                                    approachMeasure = { measurable, constraints ->
                                        fooHeightPreChildMeasure += fooHeight()
                                        measurable.measure(constraints).run {
                                            layout(width, height) {}
                                        }
                                    },
                                )
                            )
                        }
                    }
                    scene(SceneB) { Box(Modifier.element(TestElements.Foo).size(100.dp)) }
                }
            }

        var progress by mutableFloatStateOf(0f)
        val transition = transition(from = SceneA, to = SceneB, progress = { progress })
        var countApproachPass = fooHeightPreChildMeasure.size

        // Idle state: Scene A.
        assertThat(state.isTransitioning()).isFalse()
        assertThat(fooHeight()).isNull()

        // Start transition: Scene A -> Scene B (progress 0%).
        scope.launch { state.startTransition(transition) }
        rule.waitForIdle()
        assertThat(state.isTransitioning()).isTrue()
        assertThat(fooHeightPreChildMeasure[countApproachPass]?.value).isWithin(.5f).of(200f)
        assertThat(fooHeight()).isNotNull()
        countApproachPass = fooHeightPreChildMeasure.size

        // progress 50%: height is going from 200dp to 100dp, so 150dp is expected now.
        progress = 0.5f
        rule.waitForIdle()
        assertThat(fooHeightPreChildMeasure[countApproachPass]?.value).isWithin(.5f).of(150f)
        assertThat(fooHeight()).isNotNull()
        countApproachPass = fooHeightPreChildMeasure.size

        progress = 1f
        rule.waitForIdle()
        assertThat(fooHeightPreChildMeasure[countApproachPass]?.value).isWithin(.5f).of(100f)
        assertThat(fooHeight()).isNotNull()
        countApproachPass = fooHeightPreChildMeasure.size

        transition.finish()
        rule.waitForIdle()
        assertThat(state.isTransitioning()).isFalse()
        assertThat(fooHeight()).isNull()
        assertThat(fooHeightPreChildMeasure.size).isEqualTo(countApproachPass)
    }
}