Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +5 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) } } Loading Loading @@ -1217,6 +1216,7 @@ private fun measure( maybePlaceable?.let { placeable -> stateInContent.sizeBeforeInterruption = Element.SizeUnspecified stateInContent.sizeInterruptionDelta = IntSize.Zero stateInContent.lastSize = placeable.size() return placeable } Loading @@ -1239,6 +1239,9 @@ private fun measure( ) }, ) stateInContent.lastSize = interruptedSize return measurable.measure( Constraints.fixed( interruptedSize.width.coerceAtLeast(0), Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +11 −0 Original line number Diff line number Diff line Loading @@ -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). Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt +6 −0 Original line number Diff line number Diff line Loading @@ -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 Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +96 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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() } } Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +5 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) } } Loading Loading @@ -1217,6 +1216,7 @@ private fun measure( maybePlaceable?.let { placeable -> stateInContent.sizeBeforeInterruption = Element.SizeUnspecified stateInContent.sizeInterruptionDelta = IntSize.Zero stateInContent.lastSize = placeable.size() return placeable } Loading @@ -1239,6 +1239,9 @@ private fun measure( ) }, ) stateInContent.lastSize = interruptedSize return measurable.measure( Constraints.fixed( interruptedSize.width.coerceAtLeast(0), Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +11 −0 Original line number Diff line number Diff line Loading @@ -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). Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt +6 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +96 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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() } }