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

Commit 1faa0111 authored by Omar Miatello's avatar Omar Miatello
Browse files

STL ElementStateScope exposes lastSize()

This method returns an observable *last known size* of an Element.

## Important 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.

This change has been discussed in ag/32426737

Test: atest ElementTest
Bug: 404526497
Flag: com.android.systemui.scene_container
Change-Id: I0a5821d557f38300a75a83d4013f385893b15c85
parent f7801c7f
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

@@ -410,7 +410,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) }
    }

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

@@ -1236,6 +1236,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()
    }
}