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

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

Merge "STL ElementStateScope exposes lastSize()" into main

parents d3ba32cd 1faa0111
Loading
Loading
Loading
Loading
+5 −2
Original line number Diff line number Diff line
@@ -107,7 +107,7 @@ internal class Element(val key: ElementKey) {

        /** The last state this element had in this content. */
        var lastOffset = Offset.Unspecified
        var lastSize = SizeUnspecified
        var lastSize by mutableStateOf(SizeUnspecified)
        var lastScale = Scale.Unspecified
        var lastAlpha = AlphaUnspecified

@@ -413,7 +413,6 @@ internal class ElementNode(

        val placeable =
            measure(layoutImpl, element, transition, stateInContent, measurable, constraints)
        stateInContent.lastSize = placeable.size()
        return layout(placeable.width, placeable.height) { place(elementState, placeable) }
    }

@@ -1217,6 +1216,7 @@ private fun measure(
    maybePlaceable?.let { placeable ->
        stateInContent.sizeBeforeInterruption = Element.SizeUnspecified
        stateInContent.sizeInterruptionDelta = IntSize.Zero
        stateInContent.lastSize = placeable.size()
        return placeable
    }

@@ -1239,6 +1239,9 @@ private fun measure(
                )
            },
        )

    stateInContent.lastSize = interruptedSize

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

    /**
     * Return the *last known size* of [this] element in the given [content], i.e. the size of the
     * element, or `null` if the element is not composed and measured in that content (yet).
     *
     * Note: Usually updated **after** the measurement pass and after processing children. However,
     * if the target size is known **in advance** (like during transitions involving transformations
     * or shared elements), the update happens **before** measurement pass. This earlier update
     * allows children to potentially use this predetermined size during their own measurement.
     */
    fun ElementKey.lastSize(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.lastSize(content: ContentKey): IntSize? {
        return layoutImpl.elements[this]?.stateByContent?.get(content)?.lastSize.takeIf {
            it != Element.SizeUnspecified
        }
    }

    override fun ElementKey.targetOffset(content: ContentKey): Offset? {
        return layoutImpl.elements[this]?.stateByContent?.get(content)?.targetOffset.takeIf {
            it != Offset.Unspecified
+96 −0
Original line number Diff line number Diff line
@@ -51,6 +51,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEqualTo
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
@@ -2312,4 +2313,99 @@ class ElementTest {

        assertThat(compositions).isEqualTo(1)
    }

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

        lateinit var lastFooHeight: () -> Dp?
        var firstFooHeightBeforeMeasuringChild: Dp? = null

        val scope =
            rule.setContentAndCreateMainScope {
                val density = LocalDensity.current
                SceneTransitionLayoutForTesting(state) {
                    scene(SceneA) {
                        SideEffect {
                            lastFooHeight = {
                                with(density) { TestElements.Foo.lastSize(SceneA)?.height?.toDp() }
                            }
                        }
                        Box(Modifier.element(TestElements.Foo).size(200.dp)) {
                            Box(
                                Modifier.approachLayout(
                                    isMeasurementApproachInProgress = { false },
                                    approachMeasure = { measurable, constraints ->
                                        if (firstFooHeightBeforeMeasuringChild == null) {
                                            firstFooHeightBeforeMeasuringChild = lastFooHeight()
                                        }

                                        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 })

        fun assertDp(actual: Dp?, expected: Dp, subject: String) {
            assertThat(actual).isNotNull()
            actual!!.assertIsEqualTo(expected, subject, tolerance = 0.5.dp)
        }

        // Idle state: Scene A.
        assertThat(state.isTransitioning()).isFalse()
        assertDp(actual = lastFooHeight(), expected = 200.dp, subject = "lastFooHeight")

        // Start transition: Scene A -> Scene B (progress 0%).
        firstFooHeightBeforeMeasuringChild = null
        scope.launch { state.startTransition(transition) }
        rule.waitForIdle()
        assertThat(state.isTransitioning()).isTrue()
        assertDp(
            actual = firstFooHeightBeforeMeasuringChild,
            expected = 200.dp,
            subject = "firstFooHeightBeforeMeasuringChild",
        )
        assertDp(actual = lastFooHeight(), expected = 200.dp, subject = "lastFooHeight")

        // progress 50%: height is going from 200dp to 100dp, so 150dp is expected now.
        firstFooHeightBeforeMeasuringChild = null
        progress = 0.5f
        rule.waitForIdle()
        assertDp(
            actual = firstFooHeightBeforeMeasuringChild,
            expected = 150.dp,
            subject = "firstFooHeightBeforeMeasuringChild",
        )
        assertDp(actual = lastFooHeight(), expected = 150.dp, subject = "lastFooHeight")

        firstFooHeightBeforeMeasuringChild = null
        progress = 1f
        rule.waitForIdle()
        assertDp(
            actual = firstFooHeightBeforeMeasuringChild,
            expected = 100.dp,
            subject = "firstFooHeightBeforeMeasuringChild",
        )
        assertDp(actual = lastFooHeight(), expected = 100.dp, subject = "lastFooHeight")

        firstFooHeightBeforeMeasuringChild = null
        transition.finish()
        rule.waitForIdle()
        assertThat(state.isTransitioning()).isFalse()
        assertThat(firstFooHeightBeforeMeasuringChild).isNull()
        // null because SceneA does not exist anymore.
        assertThat(lastFooHeight()).isNull()
    }
}