diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt index 51ee357f6a0b1..28bb8072a95d8 100644 --- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt +++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt @@ -119,6 +119,34 @@ class NodesRemeasuredOnceTest { assertThat(remeasurements).isEqualTo(2) } } + + @Test + fun remeasuringChildWithExtraLayer_notPlacedChild() { + val height = mutableStateOf(10) + var remeasurements = 0 + + rule.setContent { + WrapChild(onMeasured = { actualHeight -> + assertThat(actualHeight).isEqualTo(height.value) + remeasurements++ + }) { + NotPlaceChild(height) { + WrapChild { + Child(height) + } + } + } + } + + rule.runOnIdle { + assertThat(remeasurements).isEqualTo(1) + height.value = 20 + } + + rule.runOnIdle { + assertThat(remeasurements).isEqualTo(2) + } + } } @Composable @@ -133,6 +161,16 @@ private fun WrapChild(onMeasured: (Int) -> Unit = {}, content: @Composable () -> } } +@Composable +private fun NotPlaceChild(height: State, content: @Composable () -> Unit) { + Layout(content = content) { measurables, constraints -> + layout(constraints.maxWidth, height.value) { + measurables.first() + .measure(constraints.copy(minHeight = 0, maxHeight = Constraints.Infinity)) + } + } +} + @Composable private fun Child(height: State) { Layout { _, constraints -> diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt index 1bc1644b508fa..775bdd3a5a610 100644 --- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt +++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt @@ -256,6 +256,9 @@ internal class MeasureAndLayoutDelegate(private val root: LayoutNode) { /** * Makes sure the passed [layoutNode] and its subtree is remeasured and has the final sizes. + * + * The node or some of the nodes in its subtree can still be kept unmeasured if they are + * not placed and don't affect the parent size. See [requestRemeasure] for details. */ fun forceMeasureTheSubtree(layoutNode: LayoutNode) { // if there is nothing in `relayoutNodes` everything is remeasured. @@ -273,8 +276,13 @@ internal class MeasureAndLayoutDelegate(private val root: LayoutNode) { remeasureAndRelayoutIfNeeded(child) } - // run recursively for the subtree. - forceMeasureTheSubtree(child) + // if the child is still in NeedsRemeasure state then this child remeasure wasn't + // needed. it can happen for example when this child is not placed and can't affect + // the parent size. we can skip the whole subtree. + if (child.layoutState != NeedsRemeasure) { + // run recursively for the subtree. + forceMeasureTheSubtree(child) + } } // if the child was resized during the remeasurement it could request a remeasure on @@ -283,8 +291,6 @@ internal class MeasureAndLayoutDelegate(private val root: LayoutNode) { if (layoutNode.layoutState == NeedsRemeasure && relayoutNodes.remove(layoutNode)) { remeasureAndRelayoutIfNeeded(layoutNode) } - - require(layoutNode.layoutState != NeedsRemeasure) } /**