Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +28 −3 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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( Loading Loading @@ -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, Loading @@ -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) } } Loading Loading @@ -1183,7 +1203,7 @@ private fun interruptedAlpha( ) } private fun measure( private fun approachMeasure( layoutImpl: SceneTransitionLayoutImpl, element: Element, transition: TransitionState.Transition?, Loading Loading @@ -1214,6 +1234,7 @@ private fun measure( maybePlaceable?.let { placeable -> stateInContent.sizeBeforeInterruption = Element.SizeUnspecified stateInContent.sizeInterruptionDelta = IntSize.Zero stateInContent.approachSize = Element.SizeUnspecified return placeable } Loading @@ -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), Loading packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +7 −0 Original line number Diff line number Diff line Loading @@ -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). 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.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 Loading packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +72 −0 Original line number Diff line number Diff line Loading @@ -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) } } Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +28 −3 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading Loading @@ -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( Loading Loading @@ -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, Loading @@ -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) } } Loading Loading @@ -1183,7 +1203,7 @@ private fun interruptedAlpha( ) } private fun measure( private fun approachMeasure( layoutImpl: SceneTransitionLayoutImpl, element: Element, transition: TransitionState.Transition?, Loading Loading @@ -1214,6 +1234,7 @@ private fun measure( maybePlaceable?.let { placeable -> stateInContent.sizeBeforeInterruption = Element.SizeUnspecified stateInContent.sizeInterruptionDelta = IntSize.Zero stateInContent.approachSize = Element.SizeUnspecified return placeable } Loading @@ -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), Loading
packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +7 −0 Original line number Diff line number Diff line Loading @@ -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). 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.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 Loading
packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +72 −0 Original line number Diff line number Diff line Loading @@ -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) } }