Do not rerun placement block of layout modifiers when parent moves children
Currently we run all the user provided layout modifiers even when the node is not marked dirty. For example it happens on every scroll of lazy lists. We can skip this work in most of the conditions.
A few other small optimizations are added as part of this cl.
This cl improves LazyListScrollingBenchmark.scrollProgrammatically_noNewItems() by ≈ 11 percent from 1,643,646 ns to 1,477,123 ns.
Test: DrawReorderingTest, GraphicsLayerTest and PlacementLayoutCoordinatesTest
Change-Id: I6b849ea0ddc00110446998ee68023a9eeab0bdeb
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt
index f0315ad..107ee85 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt
@@ -18,6 +18,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.list.TrackPlacedElement
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -32,7 +33,6 @@
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
-import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.modifier.modifierLocalConsumer
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.test.junit4.ComposeContentTestRule
@@ -97,7 +97,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -117,7 +117,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -137,7 +137,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -191,13 +191,13 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(Modifier
.size(10.toDp())
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -207,11 +207,10 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
@@ -224,7 +223,6 @@
assertThat(placedItems).containsExactly(5, 6, 7, 8)
assertThat(visibleItems).containsExactly(5, 6, 7)
}
- placedItems.clear()
// Just return true so that we stop as soon as we run this once.
// This should result in one extra item being added.
true
@@ -251,13 +249,13 @@
Box(
Modifier
.size(itemSizeDp)
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(Modifier
.size(itemSizeDp)
- .onPlaced { placedItems += 11 }
+ .trackPlaced(11)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -267,11 +265,10 @@
Box(
Modifier
.size(itemSizeDp)
- .onPlaced { placedItems += index + 12 }
+ .trackPlaced(index + 12)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
@@ -284,7 +281,6 @@
assertThat(placedItems).containsExactly(10, 11, 12, 13, 14, 15, 16)
assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
}
- placedItems.clear()
// Just return true so that we stop as soon as we run this once.
// This should result in one extra item being added.
true
@@ -307,14 +303,14 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -324,17 +320,15 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
if (--extraItemCount > 0) {
- placedItems.clear()
// Return null to continue the search.
null
} else {
@@ -346,7 +340,6 @@
assertThat(placedItems).containsExactly(5, 6, 7, 8, 9)
assertThat(visibleItems).containsExactly(5, 6, 7)
}
- placedItems.clear()
// Return true to stop the search.
true
}
@@ -368,7 +361,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
@@ -378,26 +371,22 @@
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
)
}
items(5) { index ->
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index + 6
- }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
if (hasMoreContent) {
- placedItems.clear()
// Just return null so that we keep adding more items till we reach the end.
null
} else {
@@ -409,7 +398,6 @@
assertThat(placedItems).containsExactly(5, 6, 7, 8, 9, 10)
assertThat(visibleItems).containsExactly(5, 6, 7)
}
- placedItems.clear()
// Return true to end the search.
true
}
@@ -431,14 +419,14 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -448,14 +436,13 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
rule.runOnIdle {
assertThat(placedItems).containsExactly(5, 6, 7)
assertThat(visibleItems).containsExactly(5, 6, 7)
- placedItems.clear()
}
// Act.
@@ -477,7 +464,6 @@
}
}
}
- placedItems.clear()
// Just return true so that we stop as soon as we run this once.
// This should result in one extra item being added.
true
@@ -509,9 +495,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index
- }
+ .trackPlaced(index)
)
}
item {
@@ -521,20 +505,17 @@
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
)
}
items(5) { index ->
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index + 6
- }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
var count = 0
@@ -542,7 +523,6 @@
beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
// Assert that we don't keep iterating when there is no ending condition.
assertThat(count++).isLessThan(lazyGridState.layoutInfo.totalItemsCount)
- placedItems.clear()
// Always return null to continue the search.
null
}
@@ -636,4 +616,7 @@
private fun unsupportedDirection(): Nothing = error(
"Lazy list does not support beyond bounds layout for the specified direction"
)
+
+ private fun Modifier.trackPlaced(index: Int): Modifier =
+ this then TrackPlacedElement(placedItems, index)
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt
index 559f3bb..3be7e19 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt
@@ -31,7 +31,6 @@
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
-import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.modifier.modifierLocalConsumer
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
@@ -51,12 +50,12 @@
private val beyondBoundsLayoutDirection = config.beyondBoundsLayoutDirection
private val reverseLayout = config.reverseLayout
private val layoutDirection = config.layoutDirection
+ private val placedItems = mutableSetOf<Int>()
@OptIn(ExperimentalComposeUiApi::class)
@Test
fun verifyItemsArePlacedBeforeBeyondBoundsItems_oneBeyondBoundItem() {
// Arrange
- val placedItems = mutableSetOf<Int>()
var beyondBoundsLayout: BeyondBoundsLayout? = null
val lazyListState = LazyListState()
rule.setContent {
@@ -71,14 +70,14 @@
Box(
Modifier
.size(10.dp)
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(
Modifier
.size(10.dp)
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -88,14 +87,13 @@
Box(
Modifier
.size(10.dp)
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
}
}
rule.runOnIdle { runBlocking { lazyListState.scrollToItem(5) } }
- rule.runOnIdle { placedItems.clear() }
// Act
rule.runOnUiThread {
@@ -107,7 +105,6 @@
assertThat(placedItems).containsAtLeast(4, 5, 6, 7, 8, 9)
}
assertThat(lazyListState.visibleItems).containsAtLeast(5, 6, 7)
- placedItems.clear()
true
}
}
@@ -123,7 +120,6 @@
@Test
fun verifyItemsArePlacedBeforeBeyondBoundsItems_twoBeyondBoundItem() {
// Arrange
- val placedItems = mutableSetOf<Int>()
var beyondBoundsLayout: BeyondBoundsLayout? = null
val lazyListState = LazyListState()
var extraItemCount = 2
@@ -139,14 +135,14 @@
Box(
Modifier
.size(10.dp)
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(
Modifier
.size(10.dp)
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -156,20 +152,18 @@
Box(
Modifier
.size(10.dp)
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
}
}
rule.runOnIdle { runBlocking { lazyListState.scrollToItem(5) } }
- rule.runOnIdle { placedItems.clear() }
// Act
rule.runOnUiThread {
beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
if (--extraItemCount > 0) {
- placedItems.clear()
// Return null to continue the search.
null
} else {
@@ -180,7 +174,6 @@
assertThat(placedItems).containsAtLeast(4, 5, 6, 7, 8, 9, 10)
}
assertThat(lazyListState.visibleItems).containsAtLeast(5, 6, 7)
- placedItems.clear()
true
}
}
@@ -249,4 +242,7 @@
Before -> true
else -> error("Unsupported BeyondBoundsDirection")
}
+
+ private fun Modifier.trackPlaced(index: Int): Modifier =
+ this then TrackPlacedElement(placedItems, index)
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt
index d431ea4..4e0a1ed 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt
@@ -36,9 +36,12 @@
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
+import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
-import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.modifier.modifierLocalConsumer
+import androidx.compose.ui.node.LayoutAwareModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
@@ -102,7 +105,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -122,7 +125,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -142,7 +145,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -196,13 +199,13 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(Modifier
.size(10.toDp())
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -212,11 +215,10 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
@@ -229,7 +231,6 @@
assertThat(placedItems).containsExactly(5, 6, 7, 8)
assertThat(visibleItems).containsExactly(5, 6, 7)
}
- placedItems.clear()
// Just return true so that we stop as soon as we run this once.
// This should result in one extra item being added.
true
@@ -252,14 +253,14 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -269,17 +270,15 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
if (--extraItemCount > 0) {
- placedItems.clear()
// Return null to continue the search.
null
} else {
@@ -291,7 +290,6 @@
assertThat(placedItems).containsExactly(5, 6, 7, 8, 9)
assertThat(visibleItems).containsExactly(5, 6, 7)
}
- placedItems.clear()
// Return true to stop the search.
true
}
@@ -313,7 +311,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
@@ -323,26 +321,22 @@
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
)
}
items(5) { index ->
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index + 6
- }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
if (hasMoreContent) {
- placedItems.clear()
// Just return null so that we keep adding more items till we reach the end.
null
} else {
@@ -354,7 +348,6 @@
assertThat(placedItems).containsExactly(5, 6, 7, 8, 9, 10)
assertThat(visibleItems).containsExactly(5, 6, 7)
}
- placedItems.clear()
// Return true to end the search.
true
}
@@ -376,14 +369,14 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -393,14 +386,13 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
rule.runOnIdle {
assertThat(placedItems).containsExactly(5, 6, 7)
assertThat(visibleItems).containsExactly(5, 6, 7)
- placedItems.clear()
}
// Act.
@@ -422,7 +414,6 @@
}
}
}
- placedItems.clear()
// Just return true so that we stop as soon as we run this once.
// This should result in one extra item being added.
true
@@ -454,9 +445,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index
- }
+ .trackPlaced(index)
)
}
item {
@@ -466,20 +455,17 @@
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
)
}
items(5) { index ->
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index + 6
- }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
var count = 0
@@ -487,7 +473,6 @@
beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
// Assert that we don't keep iterating when there is no ending condition.
assertThat(count++).isLessThan(lazyListState.layoutInfo.totalItemsCount)
- placedItems.clear()
// Always return null to continue the search.
null
}
@@ -576,4 +561,37 @@
private fun unsupportedDirection(): Nothing = error(
"Lazy list does not support beyond bounds layout for the specified direction"
)
+
+ private fun Modifier.trackPlaced(index: Int): Modifier =
+ this then TrackPlacedElement(placedItems, index)
+}
+
+internal data class TrackPlacedElement(
+ var placedItems: MutableSet<Int>,
+ var index: Int
+) : ModifierNodeElement<TrackPlacedNode>() {
+ override fun create() = TrackPlacedNode(placedItems, index)
+
+ override fun update(node: TrackPlacedNode) {
+ node.placedItems = placedItems
+ node.index = index
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "trackPlaced"
+ properties["index"] = index
+ }
+}
+
+internal class TrackPlacedNode(
+ var placedItems: MutableSet<Int>,
+ var index: Int
+) : LayoutAwareModifierNode, Modifier.Node() {
+ override fun onPlaced(coordinates: LayoutCoordinates) {
+ placedItems += index
+ }
+
+ override fun onDetach() {
+ placedItems -= index
+ }
}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt
index 1b82ba8..116e9ae 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt
@@ -18,6 +18,7 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.list.TrackPlacedElement
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -32,7 +33,6 @@
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
-import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.modifier.modifierLocalConsumer
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.test.junit4.ComposeContentTestRule
@@ -97,7 +97,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -117,7 +117,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -137,7 +137,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -191,13 +191,13 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(Modifier
.size(10.toDp())
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -207,11 +207,10 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
@@ -224,7 +223,6 @@
assertThat(placedItems).containsExactly(5, 6, 7, 8)
assertThat(visibleItems).containsExactly(5, 6, 7)
}
- placedItems.clear()
// Just return true so that we stop as soon as we run this once.
// This should result in one extra item being added.
true
@@ -251,13 +249,13 @@
Box(
Modifier
.size(itemSizeDp)
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(Modifier
.size(itemSizeDp)
- .onPlaced { placedItems += 11 }
+ .trackPlaced(11)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -267,11 +265,10 @@
Box(
Modifier
.size(itemSizeDp)
- .onPlaced { placedItems += index + 12 }
+ .trackPlaced(index + 12)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
@@ -284,7 +281,6 @@
assertThat(placedItems).containsExactly(10, 11, 12, 13, 14, 15, 16)
assertThat(visibleItems).containsExactly(10, 11, 12, 13, 14, 15)
}
- placedItems.clear()
// Just return true so that we stop as soon as we run this once.
// This should result in one extra item being added.
true
@@ -319,13 +315,13 @@
Box(
Modifier
.size(itemSizeDp * if (index % 2 == 0) 2f else 1f)
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item(span = StaggeredGridItemSpan.FullLine) {
Box(Modifier
.size(itemSizeDp)
- .onPlaced { placedItems += 4 }
+ .trackPlaced(4)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -335,11 +331,10 @@
Box(
Modifier
.size(itemSizeDp * if (index % 2 == 0) 2f else 1f)
- .onPlaced { placedItems += index + 5 }
+ .trackPlaced(index + 5)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
@@ -352,7 +347,6 @@
assertThat(placedItems).containsExactly(4, 5, 6, 7, 8)
assertThat(visibleItems).containsExactly(4, 5, 6, 7)
}
- placedItems.clear()
// Just return true so that we stop as soon as we run this once.
// This should result in one extra item being added.
true
@@ -375,14 +369,14 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -392,17 +386,15 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
if (--extraItemCount > 0) {
- placedItems.clear()
// Return null to continue the search.
null
} else {
@@ -414,7 +406,6 @@
assertThat(placedItems).containsExactly(5, 6, 7, 8, 9)
assertThat(visibleItems).containsExactly(5, 6, 7)
}
- placedItems.clear()
// Return true to stop the search.
true
}
@@ -436,7 +427,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
@@ -446,26 +437,22 @@
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
)
}
items(5) { index ->
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index + 6
- }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
if (hasMoreContent) {
- placedItems.clear()
// Just return null so that we keep adding more items till we reach the end.
null
} else {
@@ -477,7 +464,6 @@
assertThat(placedItems).containsExactly(5, 6, 7, 8, 9, 10)
assertThat(visibleItems).containsExactly(5, 6, 7)
}
- placedItems.clear()
// Return true to end the search.
true
}
@@ -499,14 +485,14 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -516,14 +502,13 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
rule.runOnIdle {
assertThat(placedItems).containsExactly(5, 6, 7)
assertThat(visibleItems).containsExactly(5, 6, 7)
- placedItems.clear()
}
// Act.
@@ -545,7 +530,6 @@
}
}
}
- placedItems.clear()
// Just return true so that we stop as soon as we run this once.
// This should result in one extra item being added.
true
@@ -577,9 +561,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index
- }
+ .trackPlaced(index)
)
}
item {
@@ -589,20 +571,17 @@
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
)
}
items(5) { index ->
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index + 6
- }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
var count = 0
@@ -610,7 +589,6 @@
beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
// Assert that we don't keep iterating when there is no ending condition.
assertThat(count++).isLessThan(lazyStaggeredGridState.layoutInfo.totalItemsCount)
- placedItems.clear()
// Always return null to continue the search.
null
}
@@ -704,4 +682,7 @@
private fun unsupportedDirection(): Nothing = error(
"Lazy list does not support beyond bounds layout for the specified direction"
)
+
+ private fun Modifier.trackPlaced(index: Int): Modifier =
+ this then TrackPlacedElement(placedItems, index)
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawReorderingTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawReorderingTest.kt
index 57531e5..ebea073 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawReorderingTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawReorderingTest.kt
@@ -43,6 +43,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.filters.SdkSuppress
+import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import org.junit.Assert.assertNotNull
@@ -1040,20 +1041,28 @@
@Test
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
- fun placingInDifferentOrderTriggersRedraw() {
+ fun changingPlaceOrderInLayout() {
var reverseOrder by mutableStateOf(false)
+ var childRelayoutCount = 0
+ val childRelayoutModifier = Modifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ childRelayoutCount++
+ placeable.place(0, 0)
+ }
+ }
rule.runOnUiThread {
activity.setContent {
Layout(
content = {
- FixedSize(30) {
+ FixedSize(30, childRelayoutModifier) {
FixedSize(
10,
Modifier.padding(10)
.background(Color.White)
)
}
- FixedSize(30) {
+ FixedSize(30, childRelayoutModifier) {
FixedSize(
30,
Modifier.background(Color.Red)
@@ -1084,6 +1093,7 @@
rule.runOnUiThread {
drawLatch = CountDownLatch(1)
reverseOrder = true
+ childRelayoutCount = 0
}
rule.validateSquareColors(
@@ -1092,6 +1102,74 @@
size = 10,
drawLatch = drawLatch
)
+ rule.runOnUiThread {
+ // changing drawing order doesn't require child's layer block rerun
+ assertThat(childRelayoutCount).isEqualTo(0)
+ }
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+ fun changingZIndexInLayout() {
+ var zIndex by mutableStateOf(1f)
+ var childRelayoutCount = 0
+ val childRelayoutModifier = Modifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ childRelayoutCount++
+ placeable.place(0, 0)
+ }
+ }
+ rule.runOnUiThread {
+ activity.setContent {
+ Layout(
+ content = {
+ FixedSize(30, childRelayoutModifier) {
+ FixedSize(
+ 10,
+ Modifier.padding(10)
+ .background(Color.White)
+ )
+ }
+ FixedSize(30, childRelayoutModifier) {
+ FixedSize(
+ 30,
+ Modifier.background(Color.Red)
+ )
+ }
+ },
+ modifier = Modifier.drawLatchModifier()
+ ) { measurables, _ ->
+ val newConstraints = Constraints.fixed(30, 30)
+ val placeables = measurables.map { m ->
+ m.measure(newConstraints)
+ }
+ layout(newConstraints.maxWidth, newConstraints.maxWidth) {
+ placeables[0].place(0, 0)
+ placeables[1].place(0, 0, zIndex)
+ }
+ }
+ }
+ }
+
+ assertTrue(drawLatch.await(1, TimeUnit.SECONDS))
+
+ rule.runOnUiThread {
+ drawLatch = CountDownLatch(1)
+ zIndex = -1f
+ childRelayoutCount = 0
+ }
+
+ rule.validateSquareColors(
+ outerColor = Color.Red,
+ innerColor = Color.White,
+ size = 10,
+ drawLatch = drawLatch
+ )
+ rule.runOnUiThread {
+ // changing zIndex doesn't require child's layer block rerun
+ assertThat(childRelayoutCount).isEqualTo(0)
+ }
}
fun Modifier.drawLatchModifier() = drawBehind { drawLatch.countDown() }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
index ba80c91..cff9158 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
@@ -121,9 +121,12 @@
rule.setContent {
FixedSize(
30,
- Modifier.padding(10).graphicsLayer().onGloballyPositioned {
- coords = it
- }
+ Modifier
+ .padding(10)
+ .graphicsLayer()
+ .onGloballyPositioned {
+ coords = it
+ }
) { /* no-op */ }
}
@@ -148,9 +151,11 @@
Padding(10) {
FixedSize(
10,
- Modifier.graphicsLayer(scaleX = 2f, scaleY = 3f).onGloballyPositioned {
- coords = it
- }
+ Modifier
+ .graphicsLayer(scaleX = 2f, scaleY = 3f)
+ .onGloballyPositioned {
+ coords = it
+ }
) {
}
}
@@ -171,9 +176,11 @@
Padding(10) {
FixedSize(
10,
- Modifier.scale(scaleX = 2f, scaleY = 3f).onGloballyPositioned {
- coords = it
- }
+ Modifier
+ .scale(scaleX = 2f, scaleY = 3f)
+ .onGloballyPositioned {
+ coords = it
+ }
) {
}
}
@@ -194,9 +201,11 @@
Padding(10) {
FixedSize(
10,
- Modifier.scale(scale = 2f).onGloballyPositioned {
- coords = it
- }
+ Modifier
+ .scale(scale = 2f)
+ .onGloballyPositioned {
+ coords = it
+ }
) {
}
}
@@ -217,9 +226,11 @@
Padding(10) {
FixedSize(
10,
- Modifier.graphicsLayer(scaleY = 3f, rotationZ = 90f).onGloballyPositioned {
- coords = it
- }
+ Modifier
+ .graphicsLayer(scaleY = 3f, rotationZ = 90f)
+ .onGloballyPositioned {
+ coords = it
+ }
) {
}
}
@@ -240,9 +251,11 @@
Padding(10) {
FixedSize(
10,
- Modifier.rotate(90f).onGloballyPositioned {
- coords = it
- }
+ Modifier
+ .rotate(90f)
+ .onGloballyPositioned {
+ coords = it
+ }
) {
}
}
@@ -263,12 +276,14 @@
Padding(10) {
FixedSize(
10,
- Modifier.graphicsLayer(
- rotationZ = 90f,
- transformOrigin = TransformOrigin(1.0f, 1.0f)
- ).onGloballyPositioned {
- coords = it
- }
+ Modifier
+ .graphicsLayer(
+ rotationZ = 90f,
+ transformOrigin = TransformOrigin(1.0f, 1.0f)
+ )
+ .onGloballyPositioned {
+ coords = it
+ }
)
}
}
@@ -288,12 +303,14 @@
Padding(10) {
FixedSize(
10,
- Modifier.graphicsLayer(
- translationX = 5.0f,
- translationY = 8.0f
- ).onGloballyPositioned {
- coords = it
- }
+ Modifier
+ .graphicsLayer(
+ translationX = 5.0f,
+ translationY = 8.0f
+ )
+ .onGloballyPositioned {
+ coords = it
+ }
)
}
}
@@ -314,9 +331,11 @@
FixedSize(10, Modifier.graphicsLayer(clip = true)) {
FixedSize(
10,
- Modifier.graphicsLayer(scaleX = 2f).onGloballyPositioned {
- coords = it
- }
+ Modifier
+ .graphicsLayer(scaleX = 2f)
+ .onGloballyPositioned {
+ coords = it
+ }
) {
}
}
@@ -339,18 +358,20 @@
rule.setContent {
with(LocalDensity.current) {
Box(
- Modifier.requiredSize(25.toDp())
+ Modifier
+ .requiredSize(25.toDp())
.graphicsLayer(
rotationZ = 30f,
clip = true
)
) {
Box(
- Modifier.graphicsLayer(
- rotationZ = 90f,
- transformOrigin = TransformOrigin(0f, 1f),
- clip = true
- )
+ Modifier
+ .graphicsLayer(
+ rotationZ = 90f,
+ transformOrigin = TransformOrigin(0f, 1f),
+ clip = true
+ )
.requiredSize(20.toDp(), 10.toDp())
.align(AbsoluteAlignment.TopLeft)
.onGloballyPositioned {
@@ -395,7 +416,9 @@
if (Build.VERSION.SDK_INT == 28) return // b/260095151
val testTag = "parent"
rule.setContent {
- Box(modifier = Modifier.testTag(testTag).wrapContentSize()) {
+ Box(modifier = Modifier
+ .testTag(testTag)
+ .wrapContentSize()) {
Box(
modifier = Modifier
.requiredSize(100.dp)
@@ -431,7 +454,10 @@
}
val tag = "testTag"
rule.setContent {
- Box(modifier = Modifier.testTag(tag).requiredSize(100.dp).background(Color.Blue)) {
+ Box(modifier = Modifier
+ .testTag(tag)
+ .requiredSize(100.dp)
+ .background(Color.Blue)) {
Box(
modifier = Modifier
.matchParentSize()
@@ -454,9 +480,11 @@
FixedSize(10, Modifier.graphicsLayer(clip = true)) {
FixedSize(
10,
- Modifier.padding(20).onGloballyPositioned {
- coords = it
- }
+ Modifier
+ .padding(20)
+ .onGloballyPositioned {
+ coords = it
+ }
) {
}
}
@@ -493,7 +521,8 @@
drawBlock: DrawScope.() -> Unit
) {
Box(
- Modifier.testTag(tag)
+ Modifier
+ .testTag(tag)
.size(size)
.background(Color.Black)
.graphicsLayer {
@@ -606,7 +635,11 @@
val size = (sizePx / density)
val squareSize = (squarePx / density)
offset = (20f / density).roundToInt()
- Box(Modifier.size(size.dp).background(Color.LightGray).testTag(testTag)) {
+ Box(
+ Modifier
+ .size(size.dp)
+ .background(Color.LightGray)
+ .testTag(testTag)) {
Box(
Modifier
.layout { measurable, constraints ->
@@ -664,7 +697,11 @@
val size = (sizePx / density)
val squareSize = (squarePx / density)
offset = (20f / density).roundToInt()
- Box(Modifier.size(size.dp).background(Color.LightGray).testTag(testTag)) {
+ Box(
+ Modifier
+ .size(size.dp)
+ .background(Color.LightGray)
+ .testTag(testTag)) {
Box(
Modifier
.layout { measurable, constraints ->
@@ -689,7 +726,8 @@
drawRect(
color = Color.Red,
topLeft = Offset(-width, -height),
- size = Size(width * 2, height * 2))
+ size = Size(width * 2, height * 2)
+ )
}
)
}
@@ -738,7 +776,11 @@
val size = (sizePx / density)
val squareSize = (squarePx / density)
offset = (20f / density).roundToInt()
- Box(Modifier.size(size.dp).background(Color.LightGray).testTag(testTag)) {
+ Box(
+ Modifier
+ .size(size.dp)
+ .background(Color.LightGray)
+ .testTag(testTag)) {
Box(
Modifier
.layout { measurable, constraints ->
@@ -764,7 +806,8 @@
drawRect(
color = Color.Red,
topLeft = Offset(-width, -height),
- size = Size(width * 2, height * 2))
+ size = Size(width * 2, height * 2)
+ )
}
)
}
@@ -815,7 +858,11 @@
val size = (sizePx / density)
val squareSize = (squarePx / density)
offset = (20f / density).roundToInt()
- Box(Modifier.size(size.dp).background(Color.LightGray).testTag(testTag)) {
+ Box(
+ Modifier
+ .size(size.dp)
+ .background(Color.LightGray)
+ .testTag(testTag)) {
Box(
Modifier
.layout { measurable, constraints ->
@@ -845,7 +892,8 @@
drawRect(
color = Color.Red,
topLeft = Offset(-width, -height),
- size = Size(width * 2, height * 2))
+ size = Size(width * 2, height * 2)
+ )
}
)
}
@@ -896,7 +944,11 @@
val size = (sizePx / density)
val squareSize = (squarePx / density)
offset = (20f / density).roundToInt()
- Box(Modifier.size(size.dp).background(Color.LightGray).testTag(testTag)) {
+ Box(
+ Modifier
+ .size(size.dp)
+ .background(Color.LightGray)
+ .testTag(testTag)) {
Box(
Modifier
.layout { measurable, constraints ->
@@ -971,7 +1023,11 @@
val size = (sizePx / density)
val squareSize = (squarePx / density)
offset = (20f / density).roundToInt()
- Box(Modifier.size(size.dp).background(Color.LightGray).testTag(testTag)) {
+ Box(
+ Modifier
+ .size(size.dp)
+ .background(Color.LightGray)
+ .testTag(testTag)) {
Box(
Modifier
.layout { measurable, constraints ->
@@ -1044,7 +1100,10 @@
rule.setContent {
FixedSize(
5,
- Modifier.graphicsLayer().testTag("tag").background(color)
+ Modifier
+ .graphicsLayer()
+ .testTag("tag")
+ .background(color)
)
}
@@ -1092,14 +1151,18 @@
Layout(
content = {
Box(
- Modifier.fillMaxSize().clickable {
- firstClicked = true
- }
+ Modifier
+ .fillMaxSize()
+ .clickable {
+ firstClicked = true
+ }
)
Box(
- Modifier.fillMaxSize().clickable {
- secondClicked = true
- }
+ Modifier
+ .fillMaxSize()
+ .clickable {
+ secondClicked = true
+ }
)
},
modifier = Modifier.testTag("layout")
@@ -1167,7 +1230,8 @@
rule.setContent {
Canvas(
modifier =
- Modifier.testTag(tag)
+ Modifier
+ .testTag(tag)
.size((dimen / LocalDensity.current.density).dp)
.background(Color.Black)
.graphicsLayer(
@@ -1209,7 +1273,8 @@
rule.setContent {
Canvas(
modifier =
- Modifier.testTag(tag)
+ Modifier
+ .testTag(tag)
.size((dimen / LocalDensity.current.density).dp)
.background(Color.LightGray)
.graphicsLayer(
@@ -1244,7 +1309,8 @@
rule.setContent {
Canvas(
modifier =
- Modifier.testTag(tag)
+ Modifier
+ .testTag(tag)
.size((dimen / LocalDensity.current.density).dp)
.background(Color.Black)
.graphicsLayer(
@@ -1360,7 +1426,10 @@
val size = 100
rule.setContent {
val sizeDp = with(LocalDensity.current) { size.toDp() }
- LazyColumn(Modifier.testTag("lazy").background(Color.Blue)) {
+ LazyColumn(
+ Modifier
+ .testTag("lazy")
+ .background(Color.Blue)) {
items(4) {
Box(
Modifier
@@ -1512,7 +1581,10 @@
}
}
rule.setContent {
- Box(Modifier.graphicsLayer(translationX = translationX).then(layoutModifier)) {
+ Box(
+ Modifier
+ .graphicsLayer(translationX = translationX)
+ .then(layoutModifier)) {
Layout(Modifier.onGloballyPositioned { coordinates = it }) { _, _ ->
layout(10, 10) {}
}
@@ -1550,7 +1622,10 @@
}
}
rule.setContent {
- Box(Modifier.graphicsLayer(lambda).then(layoutModifier)) {
+ Box(
+ Modifier
+ .graphicsLayer(lambda)
+ .then(layoutModifier)) {
Layout(Modifier.onGloballyPositioned { coordinates = it }) { _, _ ->
layout(10, 10) {}
}
@@ -1572,4 +1647,92 @@
assertEquals(0, relayoutCount)
}
}
+
+ @Test
+ fun addingLayerForChildDoesntTriggerChildRelayout() {
+ var relayoutCount = 0
+ var modifierRelayoutCount = 0
+ var needLayer by mutableStateOf(false)
+ var layerBlockCalled = false
+ rule.setContent {
+ Layout(content = {
+ Layout(
+ modifier = Modifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ modifierRelayoutCount++
+ placeable.place(0, 0)
+ }
+ }
+ ) { _, _ ->
+ layout(10, 10) {
+ relayoutCount++
+ }
+ }
+ }) { measurables, constraints ->
+ val placeable = measurables[0].measure(constraints)
+ layout(placeable.width, placeable.height) {
+ if (needLayer) {
+ placeable.placeWithLayer(0, 0) {
+ layerBlockCalled = true
+ }
+ } else {
+ placeable.place(0, 0)
+ }
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ relayoutCount = 0
+ modifierRelayoutCount = 0
+ needLayer = true
+ }
+
+ rule.runOnIdle {
+ assertEquals(0, relayoutCount)
+ assertTrue(layerBlockCalled)
+ assertEquals(0, modifierRelayoutCount)
+ }
+ }
+
+ @Test
+ fun movingChildsLayerDoesntTriggerChildRelayout() {
+ var relayoutCount = 0
+ var modifierRelayoutCount = 0
+ var position by mutableStateOf(0)
+ rule.setContent {
+ Layout(content = {
+ Layout(
+ modifier = Modifier.layout { measurable, constraints ->
+ val placeable = measurable.measure(constraints)
+ layout(placeable.width, placeable.height) {
+ modifierRelayoutCount++
+ placeable.place(0, 0)
+ }
+ }
+ ) { _, _ ->
+ layout(10, 10) {
+ relayoutCount++
+ }
+ }
+ }) { measurables, constraints ->
+ val placeable = measurables[0].measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.placeWithLayer(position, 0)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ relayoutCount = 0
+ modifierRelayoutCount = 0
+ position = 10
+ }
+
+ rule.runOnIdle {
+ assertEquals(0, relayoutCount)
+ assertEquals(0, modifierRelayoutCount)
+ }
+ }
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LayoutCooperationTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LayoutCooperationTest.kt
index 154f1fd..e91f316 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LayoutCooperationTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LayoutCooperationTest.kt
@@ -29,9 +29,12 @@
import androidx.compose.ui.background
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
@@ -52,8 +55,14 @@
val size = 48
var initialOuterSize by mutableStateOf((size / 2).toDp())
rule.setContent {
- Box(Modifier.size(initialOuterSize).testTag("outer")) {
- Box(Modifier.requiredSize(size.toDp()).background(Color.Yellow))
+ Box(
+ Modifier
+ .size(initialOuterSize)
+ .testTag("outer")) {
+ Box(
+ Modifier
+ .requiredSize(size.toDp())
+ .background(Color.Yellow))
}
}
@@ -65,4 +74,45 @@
Color.Yellow
}
}
+
+ @Test
+ fun relayoutSkippingModifiersDoesntBreakCooperation() {
+ with(rule.density) {
+ val containerSize = 100
+ val width = 50
+ val widthDp = width.toDp()
+ val height = 40
+ val heightDp = height.toDp()
+ var offset by mutableStateOf(0)
+ rule.setContent {
+ Layout(content = {
+ Box(Modifier.requiredSize(widthDp, heightDp)) {
+ Box(Modifier.testTag("child"))
+ }
+ }) { measurables, _ ->
+ val placeable =
+ measurables.first().measure(Constraints.fixed(containerSize, containerSize))
+ layout(containerSize, containerSize) {
+ placeable.place(offset, offset)
+ }
+ }
+ }
+
+ var expectedTop = ((containerSize - height) / 2).toDp()
+ var expectedLeft = ((containerSize - width) / 2).toDp()
+ rule.onNodeWithTag("child")
+ .assertTopPositionInRootIsEqualTo(expectedTop)
+ .assertLeftPositionInRootIsEqualTo(expectedLeft)
+
+ rule.runOnIdle {
+ offset = 10
+ }
+
+ expectedTop += offset.toDp()
+ expectedLeft += offset.toDp()
+ rule.onNodeWithTag("child")
+ .assertTopPositionInRootIsEqualTo(expectedTop)
+ .assertLeftPositionInRootIsEqualTo(expectedLeft)
+ }
+ }
}
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
index 942c141..a1e73ac 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt
@@ -1716,6 +1716,7 @@
repeat(3) { id ->
subcompose(id) {
Box(Modifier.trackMainPassPlacement {
+ iteration.toString() // state read to make callback called
actualPlacementOrder.add(id)
})
}.fastMap { it.measure(constraints) }.let { placeables.addAll(it) }
@@ -1726,6 +1727,7 @@
val id = index + 3
subcompose(id) {
Box(Modifier.trackMainPassPlacement {
+ iteration.toString() // state read to make callback called
actualPlacementOrder.add(id)
})
}.fastMap { it.measure(constraints) }.let { allPlaceables.addAll(it) }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt
index f655381..ca5805c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt
@@ -257,7 +257,7 @@
Layout(content, Modifier.alignByBaseline()) { measurables, constraints ->
val p = measurables[0].measure(constraints)
layout(p.width, p.height) {
- locations += coordinates
+ locations += coordinates.use()
p.place(0, 0)
}
}
@@ -287,7 +287,7 @@
Layout(content, Modifier.alignByBaseline()) { measurables, constraints ->
val p = measurables[0].measure(constraints)
layout(p.width, p.height) {
- locations += coordinates
+ locations += coordinates.use()
p.place(0, 0)
}
}
@@ -329,7 +329,7 @@
}) { measurables, constraints ->
val p = measurables[0].measure(constraints)
layout(p.width, p.height) {
- locations += coordinates
+ locations += coordinates.use()
p.place(0, 0)
}
}
@@ -343,7 +343,7 @@
}
@Test
- fun parentCoordateChangeCausesRelayout() {
+ fun parentCoordinateChangeCausesRelayout() {
val locations = mutableStateListOf<LayoutCoordinates?>()
var offset by mutableStateOf(DpOffset(0.dp, 0.dp))
rule.setContent {
@@ -354,7 +354,7 @@
.layout { measurable, constraints ->
val p = measurable.measure(constraints)
layout(p.width, p.height) {
- locations += coordinates
+ locations += coordinates.use()
p.place(0, 0)
}
}
@@ -386,7 +386,7 @@
.layout { measurable, constraints ->
val p = measurable.measure(constraints)
layout(p.width, p.height) {
- locations += coordinates
+ locations += coordinates.use()
p.place(0, 0)
}
}
@@ -422,7 +422,7 @@
.layout { measurable, constraints ->
val p = measurable.measure(constraints)
layout(p.width, p.height) {
- locations += coordinates
+ locations += coordinates.use()
p.place(0, 0)
}
}
@@ -459,7 +459,8 @@
.layout { measurable, constraints ->
val p = measurable.measure(constraints)
layout(p.width, p.height) {
- layoutCalls += if (readCoordinates) coordinates else null
+ layoutCalls +=
+ if (readCoordinates) coordinates.use() else null
p.place(0, 0)
}
}
@@ -507,7 +508,7 @@
.layout { measurable, constraints ->
val p = measurable.measure(constraints)
layout(p.width, p.height) {
- locations += coordinates
+ locations += coordinates.use()
p.place(0, 0)
}
}
@@ -568,7 +569,7 @@
.layout { measurable, constraints ->
val p = measurable.measure(constraints)
layout(p.width, p.height) {
- locations += coordinates
+ locations += coordinates.use()
p.place(0, 0)
}
}
@@ -604,7 +605,7 @@
.layout { measurable, constraints ->
val p = measurable.measure(constraints)
layout(p.width, p.height) {
- locations += coordinates
+ locations += coordinates.use()
p.place(0, 0)
}
}
@@ -634,7 +635,7 @@
.layout { measurable, constraints ->
val p = measurable.measure(constraints)
layout(p.width, p.height) {
- locations += coordinates
+ locations += coordinates.use()
p.place(0, 0)
}
}
@@ -668,4 +669,235 @@
rule.waitForIdle()
assertEquals(1, locations.size)
}
+
+ @Test
+ fun readingFromMainLayoutPolicyAfterMultipleMoves() {
+ var offset by mutableStateOf(0)
+ var layoutBlockCalls = 0
+ rule.setContent {
+ Layout(content = {
+ Layout { _, _ ->
+ layout(10, 10) {
+ coordinates?.positionInParent()
+ layoutBlockCalls++
+ }
+ }
+ }) { measurables, constraints ->
+ val placeable = measurables.first().measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(offset, 0)
+ }
+ }
+ }
+
+ rule.runOnIdle {
+ layoutBlockCalls = 0
+ offset = 1
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, layoutBlockCalls)
+ layoutBlockCalls = 0
+ offset = 2
+ }
+
+ rule.runOnIdle {
+ assertEquals(1, layoutBlockCalls)
+ }
+ }
+
+ @Test
+ fun onlyRealPositionReadsTriggerRelayout() {
+ var offset by mutableStateOf(0)
+ var coordinatesAction: (LayoutCoordinates) -> Unit by mutableStateOf({})
+ var layoutBlockCalls = 0
+ rule.setContent {
+ Layout(content = {
+ Layout { _, _ ->
+ layout(10, 10) {
+ coordinates?.let(coordinatesAction)
+ layoutBlockCalls++
+ }
+ }
+ }) { measurables, constraints ->
+ val placeable = measurables.first().measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(offset, 0)
+ }
+ }
+ }
+
+ fun assert(
+ relayoutExpected: Boolean,
+ description: String,
+ action: (LayoutCoordinates) -> Unit
+ ) {
+ coordinatesAction = action
+ rule.runOnIdle {
+ layoutBlockCalls = 0
+ offset = if (offset == 0) 10 else 0
+ }
+ rule.runOnIdle {
+ assertEquals(
+ "Relayout because of `$description` read was " +
+ "${if (!relayoutExpected) " not" else ""} expected, but " +
+ "$layoutBlockCalls calls happened",
+ if (relayoutExpected) 1 else 0,
+ layoutBlockCalls
+ )
+ }
+ }
+
+ assert(relayoutExpected = true, "positionInParent()") { it.positionInParent() }
+ assert(relayoutExpected = true, "positionInRoot()") { it.positionInRoot() }
+ assert(relayoutExpected = true, "positionInWindow()") { it.positionInWindow() }
+ assert(relayoutExpected = true, "boundsInParent()") { it.boundsInParent() }
+ assert(relayoutExpected = true, "boundsInRoot()") { it.boundsInRoot() }
+ assert(relayoutExpected = true, "boundsInWindow()") { it.boundsInWindow() }
+
+ assert(relayoutExpected = false, "empty") { }
+ assert(relayoutExpected = false, "size") { it.size }
+ assert(relayoutExpected = false, "isAttached") { it.isAttached }
+ assert(relayoutExpected = false, "providedAlignmentLines") { it.providedAlignmentLines }
+ }
+
+ @Test
+ fun onlyRealPositionReadsTriggerRelayout_inModifier() {
+ var offset by mutableStateOf(0)
+ var coordinatesAction: (LayoutCoordinates) -> Unit by mutableStateOf({})
+ var layoutBlockCalls = 0
+ rule.setContent {
+ Layout(content = {
+ Box(
+ Modifier
+ .layout { measurable, constraints ->
+ val p = measurable.measure(constraints)
+ layout(p.width, p.height) {
+ coordinates?.let(coordinatesAction)
+ layoutBlockCalls++
+ p.place(0, 0)
+ }
+ }
+ )
+ }) { measurables, constraints ->
+ val placeable = measurables.first().measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(offset, 0)
+ }
+ }
+ }
+
+ fun assert(
+ relayoutExpected: Boolean,
+ description: String,
+ action: (LayoutCoordinates) -> Unit
+ ) {
+ coordinatesAction = action
+ rule.runOnIdle {
+ layoutBlockCalls = 0
+ offset = if (offset == 0) 10 else 0
+ }
+ rule.runOnIdle {
+ assertEquals(
+ "Relayout because of `$description` read was " +
+ "${if (!relayoutExpected) " not" else ""} expected, but " +
+ "$layoutBlockCalls calls happened",
+ if (relayoutExpected) 1 else 0,
+ layoutBlockCalls
+ )
+ }
+ }
+
+ assert(relayoutExpected = true, "positionInParent()") { it.positionInParent() }
+ assert(relayoutExpected = true, "positionInRoot()") { it.positionInRoot() }
+ assert(relayoutExpected = true, "positionInWindow()") { it.positionInWindow() }
+ assert(relayoutExpected = true, "boundsInParent()") { it.boundsInParent() }
+ assert(relayoutExpected = true, "boundsInRoot()") { it.boundsInRoot() }
+ assert(relayoutExpected = true, "boundsInWindow()") { it.boundsInWindow() }
+
+ assert(relayoutExpected = false, "empty") { }
+ assert(relayoutExpected = false, "size") { it.size }
+ assert(relayoutExpected = false, "isAttached") { it.isAttached }
+ assert(relayoutExpected = false, "providedAlignmentLines") { it.providedAlignmentLines }
+ }
+
+ @OptIn(ExperimentalComposeUiApi::class)
+ @Test
+ fun onlyRealPositionReadsTriggerRelayout_inLookahead() {
+ var offset by mutableStateOf(0)
+ var coordinatesAction: (LayoutCoordinates) -> Unit by mutableStateOf({})
+ var intermediateLayoutBlockCalls = 0
+ rule.setContent {
+ LookaheadScope {
+ Layout(content = {
+ Box(
+ Modifier
+ .intermediateLayout { measurable, constraints ->
+ val p = measurable.measure(constraints)
+ layout(p.width, p.height) {
+ coordinates?.let(coordinatesAction)
+ intermediateLayoutBlockCalls++
+ p.place(0, 0)
+ }
+ }
+ .layout { measurable, constraints ->
+ val p = measurable.measure(constraints)
+ layout(10, 10) {
+ // if we don't read the coordinates here as well
+ // the read of coordinates in intermediate layout could be
+ // skipped as both passes share the same
+ // coordinatesAccessedDuringPlacement property.
+ // filed b/284153462 to track this issue
+ coordinates?.let(coordinatesAction)
+ p.place(0, 0)
+ }
+ }
+ )
+ }) { measurables, constraints ->
+ val placeable = measurables.first().measure(constraints)
+ layout(placeable.width, placeable.height) {
+ placeable.place(offset, 0)
+ }
+ }
+ }
+ }
+
+ fun assert(
+ relayoutExpected: Boolean,
+ description: String,
+ action: (LayoutCoordinates) -> Unit
+ ) {
+ coordinatesAction = action
+ rule.runOnIdle {
+ intermediateLayoutBlockCalls = 0
+ offset = if (offset == 0) 10 else 0
+ }
+ rule.runOnIdle {
+ assertEquals(
+ "Relayout because of `$description` read was " +
+ "${if (!relayoutExpected) " not" else ""} expected, but " +
+ "$intermediateLayoutBlockCalls calls happened",
+ if (relayoutExpected) 1 else 0,
+ intermediateLayoutBlockCalls
+ )
+ }
+ }
+
+ assert(relayoutExpected = true, "positionInParent()") { it.positionInParent() }
+ assert(relayoutExpected = true, "positionInRoot()") { it.positionInRoot() }
+ assert(relayoutExpected = true, "positionInWindow()") { it.positionInWindow() }
+ assert(relayoutExpected = true, "boundsInParent()") { it.boundsInParent() }
+ assert(relayoutExpected = true, "boundsInRoot()") { it.boundsInRoot() }
+ assert(relayoutExpected = true, "boundsInWindow()") { it.boundsInWindow() }
+
+ assert(relayoutExpected = false, "empty") { }
+ assert(relayoutExpected = false, "size") { it.size }
+ assert(relayoutExpected = false, "isAttached") { it.isAttached }
+ assert(relayoutExpected = false, "providedAlignmentLines") { it.providedAlignmentLines }
+ }
+}
+
+private fun LayoutCoordinates?.use(): LayoutCoordinates? {
+ this?.parentCoordinates
+ return this
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index a195828..4902ab9 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -880,20 +880,30 @@
}
override fun measureAndLayout(sendPointerUpdate: Boolean) {
- trace("AndroidOwner:measureAndLayout") {
- val resend = if (sendPointerUpdate) resendMotionEventOnLayout else null
- val rootNodeResized = measureAndLayoutDelegate.measureAndLayout(resend)
- if (rootNodeResized) {
- requestLayout()
+ // only run the logic when we have something pending
+ if (measureAndLayoutDelegate.hasPendingMeasureOrLayout ||
+ measureAndLayoutDelegate.hasPendingOnPositionedCallbacks
+ ) {
+ trace("AndroidOwner:measureAndLayout") {
+ val resend = if (sendPointerUpdate) resendMotionEventOnLayout else null
+ val rootNodeResized = measureAndLayoutDelegate.measureAndLayout(resend)
+ if (rootNodeResized) {
+ requestLayout()
+ }
+ measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
}
- measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
}
}
override fun measureAndLayout(layoutNode: LayoutNode, constraints: Constraints) {
trace("AndroidOwner:measureAndLayout") {
measureAndLayoutDelegate.measureAndLayout(layoutNode, constraints)
- measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
+ // only dispatch the callbacks if we don't have other nodes to process as otherwise
+ // we will have one more measureAndLayout() pass anyway in the same frame.
+ // it allows us to not traverse the hierarchy twice.
+ if (!measureAndLayoutDelegate.hasPendingMeasureOrLayout) {
+ measureAndLayoutDelegate.dispatchOnPositionedCallbacks()
+ }
}
}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
index 11b681a..2476012 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
@@ -208,8 +208,12 @@
val newLeft = position.x
val newTop = position.y
if (oldLeft != newLeft || oldTop != newTop) {
- renderNode.offsetLeftAndRight(newLeft - oldLeft)
- renderNode.offsetTopAndBottom(newTop - oldTop)
+ if (oldLeft != newLeft) {
+ renderNode.offsetLeftAndRight(newLeft - oldLeft)
+ }
+ if (oldTop != newTop) {
+ renderNode.offsetTopAndBottom(newTop - oldTop)
+ }
triggerRepaint()
matrixCache.invalidate()
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
index af49d34..e674eed 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
@@ -24,7 +24,7 @@
import androidx.compose.ui.unit.IntSize
/**
- * A holder of the measured bounds for the layout (MeasureBox).
+ * A holder of the measured bounds for the [Layout].
*/
@JvmDefaultWithCompatibility
interface LayoutCoordinates {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
index 354fe56..a674790 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
@@ -89,6 +89,7 @@
): Offset {
if (sourceCoordinates is LookaheadLayoutCoordinatesImpl) {
val source = sourceCoordinates.lookaheadDelegate
+ source.coordinator.onCoordinatesUsed()
val commonAncestor = coordinator.findCommonAncestor(source.coordinator)
return commonAncestor.lookaheadDelegate?.let { ancestor ->
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
index 94893a6..b20aa16 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt
@@ -71,11 +71,11 @@
set(value) {
if (field != value) {
field = value
- recalculateWidthAndHeight()
+ onMeasuredSizeChanged()
}
}
- private fun recalculateWidthAndHeight() {
+ private fun onMeasuredSizeChanged() {
width = measuredSize.width.coerceIn(
measurementConstraints.minWidth,
measurementConstraints.maxWidth
@@ -84,6 +84,8 @@
measurementConstraints.minHeight,
measurementConstraints.maxHeight
)
+ apparentToRealOffset =
+ IntOffset((width - measuredSize.width) / 2, (height - measuredSize.height) / 2)
}
/**
@@ -110,7 +112,7 @@
set(value) {
if (field != value) {
field = value
- recalculateWidthAndHeight()
+ onMeasuredSizeChanged()
}
}
@@ -119,8 +121,8 @@
* The real layout will be centered on the space assigned by the parent, which computed the
* child's position only seeing its apparent size.
*/
- protected val apparentToRealOffset: IntOffset
- get() = IntOffset((width - measuredSize.width) / 2, (height - measuredSize.height) / 2)
+ protected var apparentToRealOffset: IntOffset = IntOffset.Zero
+ private set
/**
* Receiver scope that permits explicit placement of a [Placeable].
@@ -158,6 +160,10 @@
* When [coordinates] is `null`, there will always be a follow-up placement call in which
* [coordinates] is not-`null`.
*
+ * If you read a position from the coordinates during the placement block the block
+ * will be automatically re-executed when the parent layout changes a position. If you
+ * don't read it the placement block execution can be skipped as an optimization.
+ *
* @sample androidx.compose.ui.samples.PlacementScopeCoordinatesSample
*/
open val coordinates: LayoutCoordinates?
@@ -340,7 +346,11 @@
override val coordinates: LayoutCoordinates?
get() {
- layoutDelegate?.coordinatesAccessedDuringPlacement = true
+ // if coordinates are not null we will only set this flag when the inner
+ // coordinate values are read. see NodeCoordinator.onCoordinatesUsed()
+ if (_coordinates == null) {
+ layoutDelegate?.onCoordinatesUsed()
+ }
return _coordinates
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
index fc84004..b682397 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
@@ -203,7 +203,7 @@
alignmentLinesOwner.requestMeasure()
}
if (usedByModifierLayout) {
- parent.requestLayout()
+ alignmentLinesOwner.requestLayout()
}
parent.alignmentLines.onAlignmentsChanged()
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index e66dc1c..9e079fe 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -26,7 +26,6 @@
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.util.fastForEach
/**
* This class works as a layout delegate for [LayoutNode]. It delegates all the measure/layout
@@ -173,9 +172,30 @@
val oldValue = field
if (oldValue != value) {
field = value
- if (value) {
+ if (value && !coordinatesAccessedDuringModifierPlacement) {
+ // if first out of both flags changes to true increment
childrenAccessingCoordinatesDuringPlacement++
- } else {
+ } else if (!value && !coordinatesAccessedDuringModifierPlacement) {
+ // if both flags changes to false decrement
+ childrenAccessingCoordinatesDuringPlacement--
+ }
+ }
+ }
+
+ /**
+ * Similar to [coordinatesAccessedDuringPlacement], but tracks the coordinates read happening
+ * during the modifier layout blocks run.
+ */
+ var coordinatesAccessedDuringModifierPlacement = false
+ set(value) {
+ val oldValue = field
+ if (oldValue != value) {
+ field = value
+ if (value && !coordinatesAccessedDuringPlacement) {
+ // if first out of both flags changes to true increment
+ childrenAccessingCoordinatesDuringPlacement++
+ } else if (!value && !coordinatesAccessedDuringPlacement) {
+ // if both flags changes to false decrement
childrenAccessingCoordinatesDuringPlacement--
}
}
@@ -216,6 +236,25 @@
internal var lookaheadPassDelegate: LookaheadPassDelegate? = null
private set
+ fun onCoordinatesUsed() {
+ val state = layoutNode.layoutState
+ if (state == LayoutState.LayingOut || state == LayoutState.LookaheadLayingOut) {
+ if (measurePassDelegate.layingOutChildren) {
+ coordinatesAccessedDuringPlacement = true
+ } else {
+ coordinatesAccessedDuringModifierPlacement = true
+ }
+ }
+ if (state == LayoutState.LookaheadLayingOut) {
+ // TODO lookahead should have its own flags b/284153462
+ if (lookaheadPassDelegate?.layingOutChildren == true) {
+ coordinatesAccessedDuringPlacement = true
+ } else {
+ coordinatesAccessedDuringModifierPlacement = true
+ }
+ }
+ }
+
/**
* [MeasurePassDelegate] manages the measure/layout and alignmentLine related queries for the
* actual measure/layout pass.
@@ -293,7 +332,11 @@
return _childDelegates.asMutableList()
}
+ var layingOutChildren = false
+ private set
+
override fun layoutChildren() {
+ layingOutChildren = true
alignmentLines.recalculateQueryOwner()
if (layoutPending) {
@@ -308,6 +351,7 @@
layoutPending = false
val oldLayoutState = layoutState
layoutState = LayoutState.LayingOut
+ coordinatesAccessedDuringPlacement = false
with(layoutNode) {
val owner = requireOwner()
owner.snapshotObserver.observeLayoutSnapshotReads(
@@ -341,6 +385,8 @@
alignmentLines.previousUsedDuringParentLayout = true
}
if (alignmentLines.dirty && alignmentLines.required) alignmentLines.recalculate()
+
+ layingOutChildren = false
}
private fun checkChildrenPlaceOrderForUpdates() {
@@ -463,7 +509,7 @@
}
private inline fun forEachChildDelegate(block: (MeasurePassDelegate) -> Unit) {
- layoutNode.children.fastForEach {
+ layoutNode.forEachChild {
block(it.measurePassDelegate)
}
}
@@ -581,6 +627,10 @@
layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
if (position != lastPosition) {
+ if (coordinatesAccessedDuringModifierPlacement ||
+ coordinatesAccessedDuringPlacement) {
+ layoutPending = true
+ }
notifyChildrenUsingCoordinatesWhilePlacing()
}
// This can actually be called as soon as LookaheadMeasure is done, but devs may expect
@@ -603,9 +653,7 @@
}
// Post-lookahead (if any) placement
- layoutState = LayoutState.LayingOut
placeOuterCoordinator(position, zIndex, layerBlock)
- layoutState = LayoutState.Idle
}
private fun placeOuterCoordinator(
@@ -613,26 +661,34 @@
zIndex: Float,
layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
+ layoutState = LayoutState.LayingOut
+
lastPosition = position
lastZIndex = zIndex
lastLayerBlock = layerBlock
-
placedOnce = true
- alignmentLines.usedByModifierLayout = false
- coordinatesAccessedDuringPlacement = false
+
val owner = layoutNode.requireOwner()
- owner.snapshotObserver.observeLayoutModifierSnapshotReads(
- layoutNode,
- affectsLookahead = false
- ) {
- with(PlacementScope) {
- if (layerBlock == null) {
- outerCoordinator.place(position, zIndex)
- } else {
- outerCoordinator.placeWithLayer(position, zIndex, layerBlock)
+ if (!layoutPending && isPlaced) {
+ outerCoordinator.placeSelfApparentToRealOffset(position, zIndex, layerBlock)
+ onNodePlaced()
+ } else {
+ alignmentLines.usedByModifierLayout = false
+ coordinatesAccessedDuringModifierPlacement = false
+ owner.snapshotObserver.observeLayoutModifierSnapshotReads(
+ layoutNode, affectsLookahead = false
+ ) {
+ with(PlacementScope) {
+ if (layerBlock == null) {
+ outerCoordinator.place(position, zIndex)
+ } else {
+ outerCoordinator.placeWithLayer(position, zIndex, layerBlock)
+ }
}
}
}
+
+ layoutState = LayoutState.Idle
}
/**
@@ -730,7 +786,7 @@
get() = layoutNode.parent?.layoutDelegate?.alignmentLinesOwner
override fun forEachChildAlignmentLinesOwner(block: (AlignmentLinesOwner) -> Unit) {
- layoutNode.children.fastForEach {
+ layoutNode.forEachChild {
block(it.layoutDelegate.alignmentLinesOwner)
}
}
@@ -756,11 +812,11 @@
*/
fun notifyChildrenUsingCoordinatesWhilePlacing() {
if (childrenAccessingCoordinatesDuringPlacement > 0) {
- layoutNode.children.fastForEach { child ->
+ layoutNode.forEachChild { child ->
val childLayoutDelegate = child.layoutDelegate
- if (childLayoutDelegate.coordinatesAccessedDuringPlacement &&
- !childLayoutDelegate.layoutPending
- ) {
+ val accessed = childLayoutDelegate.coordinatesAccessedDuringPlacement ||
+ childLayoutDelegate.coordinatesAccessedDuringModifierPlacement
+ if (accessed && !childLayoutDelegate.layoutPending) {
child.requestRelayout()
}
childLayoutDelegate.measurePassDelegate
@@ -939,12 +995,16 @@
return _childDelegates.asMutableList()
}
+ var layingOutChildren = false
+ private set
+
private inline fun forEachChildDelegate(block: (LookaheadPassDelegate) -> Unit) =
layoutNode.forEachChild {
block(it.layoutDelegate.lookaheadPassDelegate!!)
}
override fun layoutChildren() {
+ layingOutChildren = true
alignmentLines.recalculateQueryOwner()
if (lookaheadLayoutPending) {
@@ -961,6 +1021,7 @@
val oldLayoutState = layoutState
layoutState = LayoutState.LookaheadLayingOut
val owner = layoutNode.requireOwner()
+ coordinatesAccessedDuringPlacement = false
owner.snapshotObserver.observeLayoutSnapshotReads(layoutNode) {
clearPlaceOrder()
forEachChildAlignmentLinesOwner { child ->
@@ -985,6 +1046,8 @@
alignmentLines.previousUsedDuringParentLayout = true
}
if (alignmentLines.dirty && alignmentLines.required) alignmentLines.recalculate()
+
+ layingOutChildren = false
}
private fun checkChildrenPlaceOrderForUpdates() {
@@ -1030,7 +1093,7 @@
get() = layoutNode.parent?.layoutDelegate?.lookaheadAlignmentLinesOwner
override fun forEachChildAlignmentLinesOwner(block: (AlignmentLinesOwner) -> Unit) {
- layoutNode.children.fastForEach {
+ layoutNode.forEachChild {
block(it.layoutDelegate.lookaheadAlignmentLinesOwner!!)
}
}
@@ -1056,11 +1119,11 @@
*/
fun notifyChildrenUsingCoordinatesWhilePlacing() {
if (childrenAccessingCoordinatesDuringPlacement > 0) {
- layoutNode.children.fastForEach { child ->
+ layoutNode.forEachChild { child ->
val childLayoutDelegate = child.layoutDelegate
- if (childLayoutDelegate.coordinatesAccessedDuringPlacement &&
- !childLayoutDelegate.layoutPending
- ) {
+ val accessed = childLayoutDelegate.coordinatesAccessedDuringPlacement ||
+ childLayoutDelegate.coordinatesAccessedDuringModifierPlacement
+ if (accessed && !childLayoutDelegate.layoutPending) {
child.requestLookaheadRelayout()
}
childLayoutDelegate.lookaheadPassDelegate
@@ -1160,14 +1223,23 @@
layoutState = LayoutState.LookaheadLayingOut
placedOnce = true
if (position != lastPosition) {
+ if (coordinatesAccessedDuringModifierPlacement ||
+ coordinatesAccessedDuringPlacement) {
+ lookaheadLayoutPending = true
+ }
notifyChildrenUsingCoordinatesWhilePlacing()
}
- alignmentLines.usedByModifierLayout = false
val owner = layoutNode.requireOwner()
- coordinatesAccessedDuringPlacement = false
- owner.snapshotObserver.observeLayoutModifierSnapshotReads(layoutNode) {
- with(PlacementScope) {
- outerCoordinator.lookaheadDelegate!!.place(position)
+
+ if (!lookaheadLayoutPending && isPlaced) {
+ onNodePlaced()
+ } else {
+ coordinatesAccessedDuringModifierPlacement = false
+ alignmentLines.usedByModifierLayout = false
+ owner.snapshotObserver.observeLayoutModifierSnapshotReads(layoutNode) {
+ with(PlacementScope) {
+ outerCoordinator.lookaheadDelegate!!.place(position)
+ }
}
}
lastPosition = position
@@ -1294,8 +1366,8 @@
}
if (parent != null) {
if (!relayoutWithoutParentInProgress &&
- parent.layoutState == LayoutState.LayingOut ||
- parent.layoutState == LayoutState.LookaheadLayingOut
+ (parent.layoutState == LayoutState.LayingOut ||
+ parent.layoutState == LayoutState.LookaheadLayingOut)
) {
// the parent is currently placing its children
check(placeOrder == NotPlacedPlaceOrder) {
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 d297aef..e743af7 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
@@ -48,6 +48,11 @@
val hasPendingMeasureOrLayout get() = relayoutNodes.isNotEmpty()
/**
+ * Whether any on positioned callbacks need to be dispatched
+ */
+ val hasPendingOnPositionedCallbacks get() = onPositionedDispatcher.isNotEmpty()
+
+ /**
* Flag to indicate that we're currently measuring.
*/
private var duringMeasureLayout = false
@@ -365,7 +370,7 @@
private fun recurseRemeasure(layoutNode: LayoutNode) {
remeasureOnly(layoutNode)
- layoutNode._children.forEach { child ->
+ layoutNode.forEachChild { child ->
if (child.measureAffectsParent) {
recurseRemeasure(child)
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index 6d16589..0e9b845 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -247,15 +247,21 @@
return null
}
+ internal fun onCoordinatesUsed() {
+ layoutNode.layoutDelegate.onCoordinatesUsed()
+ }
+
final override val parentLayoutCoordinates: LayoutCoordinates?
get() {
check(isAttached) { ExpectAttachedLayoutCoordinates }
+ onCoordinatesUsed()
return layoutNode.outerCoordinator.wrappedBy
}
final override val parentCoordinates: LayoutCoordinates?
get() {
check(isAttached) { ExpectAttachedLayoutCoordinates }
+ onCoordinatesUsed()
return wrappedBy
}
@@ -301,6 +307,14 @@
zIndex: Float,
layerBlock: (GraphicsLayerScope.() -> Unit)?
) {
+ placeSelf(position, zIndex, layerBlock)
+ }
+
+ private fun placeSelf(
+ position: IntOffset,
+ zIndex: Float,
+ layerBlock: (GraphicsLayerScope.() -> Unit)?
+ ) {
updateLayerBlock(layerBlock)
if (this.position != position) {
this.position = position
@@ -318,6 +332,14 @@
this.zIndex = zIndex
}
+ fun placeSelfApparentToRealOffset(
+ position: IntOffset,
+ zIndex: Float,
+ layerBlock: (GraphicsLayerScope.() -> Unit)?
+ ) {
+ placeSelf(position + apparentToRealOffset, zIndex, layerBlock)
+ }
+
/**
* Draws the content of the LayoutNode
*/
@@ -374,9 +396,9 @@
layerBlock: (GraphicsLayerScope.() -> Unit)?,
forceUpdateLayerParameters: Boolean = false
) {
- val updateParameters = this.layerBlock !== layerBlock || layerDensity != layoutNode
- .density || layerLayoutDirection != layoutNode.layoutDirection ||
- forceUpdateLayerParameters
+ val layoutNode = layoutNode
+ val updateParameters = forceUpdateLayerParameters || this.layerBlock !== layerBlock ||
+ layerDensity != layoutNode.density || layerLayoutDirection != layoutNode.layoutDirection
this.layerBlock = layerBlock
this.layerDensity = layoutNode.density
this.layerLayoutDirection = layoutNode.layoutDirection
@@ -737,6 +759,7 @@
}
val nodeCoordinator = sourceCoordinates.toCoordinator()
+ nodeCoordinator.onCoordinatesUsed()
val commonAncestor = findCommonAncestor(nodeCoordinator)
var position = relativeToSource
@@ -751,6 +774,7 @@
override fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {
val coordinator = sourceCoordinates.toCoordinator()
+ coordinator.onCoordinatesUsed()
val commonAncestor = findCommonAncestor(coordinator)
matrix.reset()
@@ -795,6 +819,7 @@
"LayoutCoordinates $sourceCoordinates is not attached!"
}
val srcCoordinator = sourceCoordinates.toCoordinator()
+ srcCoordinator.onCoordinatesUsed()
val commonAncestor = findCommonAncestor(srcCoordinator)
val bounds = rectCache
@@ -842,6 +867,7 @@
override fun localToRoot(relativeToLocal: Offset): Offset {
check(isAttached) { ExpectAttachedLayoutCoordinates }
+ onCoordinatesUsed()
var coordinator: NodeCoordinator? = this
var position = relativeToLocal
while (coordinator != null) {
@@ -1160,7 +1186,8 @@
val layoutNode = coordinator.layoutNode
val layoutDelegate = layoutNode.layoutDelegate
if (layoutDelegate.childrenAccessingCoordinatesDuringPlacement > 0) {
- if (layoutDelegate.coordinatesAccessedDuringPlacement) {
+ if (layoutDelegate.coordinatesAccessedDuringModifierPlacement ||
+ layoutDelegate.coordinatesAccessedDuringPlacement) {
layoutNode.requestRelayout()
}
layoutDelegate.measurePassDelegate
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OnPositionedDispatcher.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OnPositionedDispatcher.kt
index 9642118..4b34850 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OnPositionedDispatcher.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OnPositionedDispatcher.kt
@@ -26,6 +26,8 @@
private val layoutNodes = mutableVectorOf<LayoutNode>()
private var cachedNodes: Array<LayoutNode?>? = null
+ fun isNotEmpty() = layoutNodes.isNotEmpty()
+
fun onNodePositioned(node: LayoutNode) {
layoutNodes += node
node.needsOnPositionedDispatch = true
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index f245fde..defab69 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -520,10 +520,10 @@
@Test
fun testLocalPositionOfWithSiblings() {
- val node0 = LayoutNode()
+ val node0 = ZeroSizedLayoutNode()
node0.attach(MockOwner())
- val node1 = LayoutNode()
- val node2 = LayoutNode()
+ val node1 = ZeroSizedLayoutNode()
+ val node2 = ZeroSizedLayoutNode()
node0.insertAt(0, node1)
node0.insertAt(1, node2)
node1.place(10, 20)
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt
index 149cf47..4544704 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt
@@ -39,9 +39,12 @@
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Below
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Left
import androidx.compose.ui.layout.BeyondBoundsLayout.LayoutDirection.Companion.Right
+import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.ModifierLocalBeyondBoundsLayout
-import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.modifier.modifierLocalConsumer
+import androidx.compose.ui.node.LayoutAwareModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.platform.InspectorInfo
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
@@ -107,7 +110,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -127,7 +130,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -147,7 +150,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -181,8 +184,7 @@
}
// Act.
- rule.waitForIdle()
- val hasMoreContent = rule.runOnUiThread {
+ val hasMoreContent = rule.runOnIdle {
beyondBoundsLayoutRef.layout(beyondBoundsLayoutDirection) {
hasMoreContent
}
@@ -202,28 +204,27 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(
Modifier
- .size(10.toDp())
- .onPlaced { placedItems += 5 }
- .modifierLocalConsumer {
- beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
- }
+ .size(10.toDp())
+ .trackPlaced(5)
+ .modifierLocalConsumer {
+ beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
+ }
)
}
items(5) { index ->
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
@@ -236,7 +237,6 @@
assertThat(placedItems).containsExactly(5, 6, 7, 8)
assertThat(visibleItems).containsExactly(5, 6, 7)
}
- placedItems.clear()
// Just return true so that we stop as soon as we run this once.
// This should result in one extra item being added.
true
@@ -259,14 +259,14 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -276,17 +276,15 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
if (--extraItemCount > 0) {
- placedItems.clear()
// Return null to continue the search.
null
} else {
@@ -298,7 +296,6 @@
assertThat(placedItems).containsExactly(5, 6, 7, 8, 9)
assertThat(visibleItems).containsExactly(5, 6, 7)
}
- placedItems.clear()
// Return true to stop the search.
true
}
@@ -320,7 +317,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
@@ -330,26 +327,22 @@
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
)
}
items(5) { index ->
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index + 6
- }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
if (hasMoreContent) {
- placedItems.clear()
// Just return null so that we keep adding more items till we reach the end.
null
} else {
@@ -361,7 +354,6 @@
assertThat(placedItems).containsExactly(5, 6, 7, 8, 9, 10)
assertThat(visibleItems).containsExactly(5, 6, 7)
}
- placedItems.clear()
// Return true to end the search.
true
}
@@ -383,14 +375,14 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
@@ -400,14 +392,13 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
rule.runOnIdle {
assertThat(placedItems).containsExactly(5, 6, 7)
assertThat(visibleItems).containsExactly(5, 6, 7)
- placedItems.clear()
}
// Act.
@@ -416,7 +407,6 @@
beyondBoundsLayoutCount++
when (beyondBoundsLayoutDirection) {
Left, Right, Above, Below -> {
- assertThat(placedItems).containsExactlyElementsIn(visibleItems)
assertThat(placedItems).containsExactly(5, 6, 7)
assertThat(visibleItems).containsExactly(5, 6, 7)
}
@@ -430,7 +420,6 @@
}
}
}
- placedItems.clear()
// Just return true so that we stop as soon as we run this once.
// This should result in one extra item being added.
true
@@ -462,9 +451,7 @@
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index
- }
+ .trackPlaced(index)
)
}
item {
@@ -474,20 +461,17 @@
.modifierLocalConsumer {
beyondBoundsLayout = ModifierLocalBeyondBoundsLayout.current
}
- .onPlaced { placedItems += 5 }
+ .trackPlaced(5)
)
}
items(5) { index ->
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index + 6
- }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
var count = 0
@@ -495,7 +479,6 @@
beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
// Assert that we don't keep iterating when there is no ending condition.
assertThat(count++).isLessThan(lazyListState.layoutInfo.totalItemsCount)
- placedItems.clear()
// Always return null to continue the search.
null
}
@@ -515,7 +498,9 @@
Column {
BasicText(
text = "Outer button",
- Modifier.focusRequester(buttonFocusRequester).focusable())
+ Modifier
+ .focusRequester(buttonFocusRequester)
+ .focusable())
TvLazyColumn {
items(3) {
@@ -617,4 +602,37 @@
private fun unsupportedDirection(): Nothing = error(
"Lazy list does not support beyond bounds layout for the specified direction"
)
+
+ private fun Modifier.trackPlaced(index: Int): Modifier =
+ this then TrackPlacedElement(placedItems, index)
+}
+
+internal data class TrackPlacedElement(
+ var placedItems: MutableSet<Int>,
+ var index: Int
+) : ModifierNodeElement<TrackPlacedNode>() {
+ override fun create() = TrackPlacedNode(placedItems, index)
+
+ override fun update(node: TrackPlacedNode) {
+ node.placedItems = placedItems
+ node.index = index
+ }
+
+ override fun InspectorInfo.inspectableProperties() {
+ name = "trackPlaced"
+ properties["index"] = index
+ }
+}
+
+internal class TrackPlacedNode(
+ var placedItems: MutableSet<Int>,
+ var index: Int
+) : LayoutAwareModifierNode, Modifier.Node() {
+ override fun onPlaced(coordinates: LayoutCoordinates) {
+ placedItems += index
+ }
+
+ override fun onDetach() {
+ placedItems -= index
+ }
}