aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridBeyondBoundsTest.kt67
-rw-r--r--compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsAndExtraItemsTest.kt24
-rw-r--r--compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt92
-rw-r--r--compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridBeyondBoundsTest.kt75
-rw-r--r--compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt3
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt70
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt16
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt26
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt77
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt22
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt97
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItemProvider.kt16
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt51
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt16
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt52
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt62
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt70
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt26
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt102
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt16
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt5
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLineProvider.kt16
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt51
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt16
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt25
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt23
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt2
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutKeyIndexMap.kt97
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasureScope.kt7
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutNearestRangeState.kt64
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt5
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt7
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt13
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemPlacementAnimator.kt28
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt61
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt151
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt8
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt69
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt11
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt75
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/MeasuredPage.kt115
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt32
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt3
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollPosition.kt46
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerSemantics.kt4
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt6
-rw-r--r--compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PositionedPage.kt44
-rw-r--r--compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt2
-rw-r--r--compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt64
-rw-r--r--compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt4
-rw-r--r--compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt14
-rw-r--r--compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/DrawReorderingTest.kt84
-rw-r--r--compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt297
-rw-r--r--compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LayoutCooperationTest.kt54
-rw-r--r--compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadScopeTest.kt2
-rw-r--r--compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/PlacementLayoutCoordinatesTest.kt256
-rw-r--r--compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt24
-rw-r--r--compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt8
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt2
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt1
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Placeable.kt22
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt2
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt142
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt7
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt35
-rw-r--r--compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OnPositionedDispatcher.kt2
-rw-r--r--compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt6
-rw-r--r--tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt108
68 files changed, 1889 insertions, 1211 deletions
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 f0315addc13..107ee850b21 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 @@ package androidx.compose.foundation.lazy.grid
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.B
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 @@ class LazyGridBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -117,7 +117,7 @@ class LazyGridBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -137,7 +137,7 @@ class LazyGridBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -191,13 +191,13 @@ class LazyGridBeyondBoundsTest(param: Param) {
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 @@ class LazyGridBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
@@ -224,7 +223,6 @@ class LazyGridBeyondBoundsTest(param: Param) {
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 @@ class LazyGridBeyondBoundsTest(param: Param) {
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 @@ class LazyGridBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(itemSizeDp)
- .onPlaced { placedItems += index + 12 }
+ .trackPlaced(index + 12)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
@@ -284,7 +281,6 @@ class LazyGridBeyondBoundsTest(param: Param) {
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 @@ class LazyGridBeyondBoundsTest(param: Param) {
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 @@ class LazyGridBeyondBoundsTest(param: Param) {
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 @@ class LazyGridBeyondBoundsTest(param: Param) {
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 @@ class LazyGridBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
@@ -378,26 +371,22 @@ class LazyGridBeyondBoundsTest(param: Param) {
.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 @@ class LazyGridBeyondBoundsTest(param: Param) {
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 @@ class LazyGridBeyondBoundsTest(param: Param) {
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 @@ class LazyGridBeyondBoundsTest(param: Param) {
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 @@ class LazyGridBeyondBoundsTest(param: Param) {
}
}
}
- 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 @@ class LazyGridBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index
- }
+ .trackPlaced(index)
)
}
item {
@@ -521,20 +505,17 @@ class LazyGridBeyondBoundsTest(param: Param) {
.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 @@ class LazyGridBeyondBoundsTest(param: Param) {
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 @@ class LazyGridBeyondBoundsTest(param: Param) {
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 559f3bb4f3e..3be7e1910ac 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.B
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 @@ class LazyListBeyondBoundsAndExtraItemsTest(val config: Config) :
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 @@ class LazyListBeyondBoundsAndExtraItemsTest(val config: Config) :
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 @@ class LazyListBeyondBoundsAndExtraItemsTest(val config: Config) :
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 @@ class LazyListBeyondBoundsAndExtraItemsTest(val config: Config) :
assertThat(placedItems).containsAtLeast(4, 5, 6, 7, 8, 9)
}
assertThat(lazyListState.visibleItems).containsAtLeast(5, 6, 7)
- placedItems.clear()
true
}
}
@@ -123,7 +120,6 @@ class LazyListBeyondBoundsAndExtraItemsTest(val config: Config) :
@Test
fun verifyItemsArePlacedBeforeBeyondBoundsItems_twoBeyondBoundItem() {
// Arrange
- val placedItems = mutableSetOf<Int>()
var beyondBoundsLayout: BeyondBoundsLayout? = null
val lazyListState = LazyListState()
var extraItemCount = 2
@@ -139,14 +135,14 @@ class LazyListBeyondBoundsAndExtraItemsTest(val config: Config) :
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 @@ class LazyListBeyondBoundsAndExtraItemsTest(val config: Config) :
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 @@ class LazyListBeyondBoundsAndExtraItemsTest(val config: Config) :
assertThat(placedItems).containsAtLeast(4, 5, 6, 7, 8, 9, 10)
}
assertThat(lazyListState.visibleItems).containsAtLeast(5, 6, 7)
- placedItems.clear()
true
}
}
@@ -249,4 +242,7 @@ class LazyListBeyondBoundsAndExtraItemsTest(val config: Config) :
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 d431ea40844..4e0a1edc243 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.B
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 @@ class LazyListBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -122,7 +125,7 @@ class LazyListBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -142,7 +145,7 @@ class LazyListBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -196,13 +199,13 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
@@ -229,7 +231,6 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
@@ -323,26 +321,22 @@ class LazyListBeyondBoundsTest(param: Param) {
.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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
}
}
}
- 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 @@ class LazyListBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index
- }
+ .trackPlaced(index)
)
}
item {
@@ -466,20 +455,17 @@ class LazyListBeyondBoundsTest(param: Param) {
.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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 1b82ba82f6a..116e9ae7a50 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 @@ package androidx.compose.foundation.lazy.staggeredgrid
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.B
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -117,7 +117,7 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -137,7 +137,7 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -191,13 +191,13 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index + 6 }
+ .trackPlaced(index + 6)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
@@ -224,7 +223,6 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(itemSizeDp)
- .onPlaced { placedItems += index + 12 }
+ .trackPlaced(index + 12)
)
}
}
- rule.runOnIdle { placedItems.clear() }
// Act.
rule.runOnUiThread {
@@ -284,7 +281,6 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
@@ -446,26 +437,22 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
.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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
}
}
}
- 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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index
- }
+ .trackPlaced(index)
)
}
item {
@@ -589,20 +571,17 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
.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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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 @@ class LazyStaggeredGridBeyondBoundsTest(param: Param) {
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/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
index db7cb63db0f..eddfb4491c6 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetcher.android.kt
@@ -145,7 +145,8 @@ internal class LazyLayoutPrefetcher(
// a next frame callback in which we will post the message in the handler again.
if (enoughTimeLeft(beforeTimeNs, nextFrameNs, timeTracker.compositionTimeNs)) {
val key = itemProvider.getKey(request.index)
- val content = itemContentFactory.getContent(request.index, key)
+ val contentType = itemProvider.getContentType(request.index)
+ val content = itemContentFactory.getContent(request.index, key, contentType)
timeTracker.trackComposition {
request.precomposeHandle =
subcomposeLayoutState.precompose(key, content)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
index 05949c78d1d..27b1d40a6e6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -38,6 +38,7 @@ import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntOffset
@@ -75,12 +76,12 @@ internal fun LazyList(
/** The content of the list */
content: LazyListScope.() -> Unit
) {
- val itemProvider = rememberLazyListItemProvider(state, content)
+ val itemProviderLambda = rememberLazyListItemProviderLambda(state, content)
val semanticState = rememberLazyListSemanticState(state, isVertical)
val measurePolicy = rememberLazyListMeasurePolicy(
- itemProvider,
+ itemProviderLambda,
state,
contentPadding,
reverseLayout,
@@ -92,7 +93,7 @@ internal fun LazyList(
verticalArrangement
)
- ScrollPositionUpdater(itemProvider, state)
+ ScrollPositionUpdater(itemProviderLambda, state)
val overscrollEffect = ScrollableDefaults.overscrollEffect()
val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal
@@ -101,7 +102,7 @@ internal fun LazyList(
.then(state.remeasurementModifier)
.then(state.awaitLayoutModifier)
.lazyLayoutSemantics(
- itemProvider = itemProvider,
+ itemProviderLambda = itemProviderLambda,
state = semanticState,
orientation = orientation,
userScrollEnabled = userScrollEnabled,
@@ -130,7 +131,7 @@ internal fun LazyList(
),
prefetchState = state.prefetchState,
measurePolicy = measurePolicy,
- itemProvider = itemProvider
+ itemProvider = itemProviderLambda
)
}
@@ -138,9 +139,10 @@ internal fun LazyList(
@ExperimentalFoundationApi
@Composable
private fun ScrollPositionUpdater(
- itemProvider: LazyListItemProvider,
+ itemProviderLambda: () -> LazyListItemProvider,
state: LazyListState
) {
+ val itemProvider = itemProviderLambda()
if (itemProvider.itemCount > 0) {
state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
}
@@ -150,7 +152,7 @@ private fun ScrollPositionUpdater(
@Composable
private fun rememberLazyListMeasurePolicy(
/** Items provider of the list. */
- itemProvider: LazyListItemProvider,
+ itemProviderLambda: () -> LazyListItemProvider,
/** The state of the list. */
state: LazyListState,
/** The inner padding to be added for the whole content(nor for each individual item) */
@@ -216,11 +218,10 @@ private fun rememberLazyListMeasurePolicy(
val contentConstraints =
containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
- state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
-
// Update the state's cached Density
state.density = this
+ val itemProvider = itemProviderLambda()
// this will update the scope used by the item composables
itemProvider.itemScope.setMaxSize(
width = contentConstraints.maxWidth,
@@ -254,37 +255,46 @@ private fun rememberLazyListMeasurePolicy(
)
}
- val measuredItemProvider = LazyListMeasuredItemProvider(
+ val measuredItemProvider = object : LazyListMeasuredItemProvider(
contentConstraints,
isVertical,
itemProvider,
this
- ) { index, key, contentType, placeables ->
- // we add spaceBetweenItems as an extra spacing for all items apart from the last one so
- // the lazy list measuring logic will take it into account.
- val spacing = if (index == itemsCount - 1) 0 else spaceBetweenItems
- LazyListMeasuredItem(
- index = index,
- placeables = placeables,
- isVertical = isVertical,
- horizontalAlignment = horizontalAlignment,
- verticalAlignment = verticalAlignment,
- layoutDirection = layoutDirection,
- reverseLayout = reverseLayout,
- beforeContentPadding = beforeContentPadding,
- afterContentPadding = afterContentPadding,
- spacing = spacing,
- visualOffset = visualItemOffset,
- key = key,
- contentType = contentType
- )
+ ) {
+ override fun createItem(
+ index: Int,
+ key: Any,
+ contentType: Any?,
+ placeables: List<Placeable>
+ ): LazyListMeasuredItem {
+ // we add spaceBetweenItems as an extra spacing for all items apart from the last one so
+ // the lazy list measuring logic will take it into account.
+ val spacing = if (index == itemsCount - 1) 0 else spaceBetweenItems
+ return LazyListMeasuredItem(
+ index = index,
+ placeables = placeables,
+ isVertical = isVertical,
+ horizontalAlignment = horizontalAlignment,
+ verticalAlignment = verticalAlignment,
+ layoutDirection = layoutDirection,
+ reverseLayout = reverseLayout,
+ beforeContentPadding = beforeContentPadding,
+ afterContentPadding = afterContentPadding,
+ spacing = spacing,
+ visualOffset = visualItemOffset,
+ key = key,
+ contentType = contentType
+ )
+ }
}
state.premeasureConstraints = measuredItemProvider.childConstraints
val firstVisibleItemIndex: Int
val firstVisibleScrollOffset: Int
Snapshot.withoutReadObservation {
- firstVisibleItemIndex = state.firstVisibleItemIndex
+ firstVisibleItemIndex = state.updateScrollPositionIfTheFirstItemWasMoved(
+ itemProvider, state.firstVisibleItemIndex
+ )
firstVisibleScrollOffset = state.firstVisibleItemScrollOffset
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt
index 978d3a81489..8cf55e5f531 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListHeaders.kt
@@ -28,13 +28,13 @@ import androidx.compose.ui.util.fastForEachIndexed
* @param beforeContentPadding the padding before the first item in the list
*/
internal fun findOrComposeLazyListHeader(
- composedVisibleItems: MutableList<LazyListPositionedItem>,
+ composedVisibleItems: MutableList<LazyListMeasuredItem>,
itemProvider: LazyListMeasuredItemProvider,
headerIndexes: List<Int>,
beforeContentPadding: Int,
layoutWidth: Int,
layoutHeight: Int,
-): LazyListPositionedItem? {
+): LazyListMeasuredItem? {
var currentHeaderOffset: Int = Int.MIN_VALUE
var nextHeaderOffset: Int = Int.MIN_VALUE
@@ -83,11 +83,11 @@ internal fun findOrComposeLazyListHeader(
headerOffset = minOf(headerOffset, nextHeaderOffset - measuredHeaderItem.size)
}
- return measuredHeaderItem.position(headerOffset, layoutWidth, layoutHeight).also {
- if (indexInComposedVisibleItems != -1) {
- composedVisibleItems[indexInComposedVisibleItems] = it
- } else {
- composedVisibleItems.add(0, it)
- }
+ measuredHeaderItem.position(headerOffset, layoutWidth, layoutHeight)
+ if (indexInComposedVisibleItems != -1) {
+ composedVisibleItems[indexInComposedVisibleItems] = measuredHeaderItem
+ } else {
+ composedVisibleItems.add(0, measuredHeaderItem)
}
+ return measuredHeaderItem
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt
index 497ba6d4cdc..ec0fb82e7e4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt
@@ -40,8 +40,8 @@ internal class LazyListItemPlacementAnimator {
// stored to not allocate it every pass.
private val movingAwayKeys = LinkedHashSet<Any>()
- private val movingInFromStartBound = mutableListOf<LazyListPositionedItem>()
- private val movingInFromEndBound = mutableListOf<LazyListPositionedItem>()
+ private val movingInFromStartBound = mutableListOf<LazyListMeasuredItem>()
+ private val movingInFromEndBound = mutableListOf<LazyListMeasuredItem>()
private val movingAwayToStartBound = mutableListOf<LazyListMeasuredItem>()
private val movingAwayToEndBound = mutableListOf<LazyListMeasuredItem>()
@@ -54,7 +54,7 @@ internal class LazyListItemPlacementAnimator {
consumedScroll: Int,
layoutWidth: Int,
layoutHeight: Int,
- positionedItems: MutableList<LazyListPositionedItem>,
+ positionedItems: MutableList<LazyListMeasuredItem>,
itemProvider: LazyListMeasuredItemProvider,
isVertical: Boolean
) {
@@ -167,9 +167,9 @@ internal class LazyListItemPlacementAnimator {
accumulatedOffset += item.size
val mainAxisOffset = 0 - accumulatedOffset
- val positionedItem = item.position(mainAxisOffset, layoutWidth, layoutHeight)
- positionedItems.add(positionedItem)
- startAnimationsIfNeeded(positionedItem)
+ item.position(mainAxisOffset, layoutWidth, layoutHeight)
+ positionedItems.add(item)
+ startAnimationsIfNeeded(item)
}
accumulatedOffset = 0
movingAwayToEndBound.sortBy { keyIndexMap.getIndex(it.key) }
@@ -177,9 +177,9 @@ internal class LazyListItemPlacementAnimator {
val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset
accumulatedOffset += item.size
- val positionedItem = item.position(mainAxisOffset, layoutWidth, layoutHeight)
- positionedItems.add(positionedItem)
- startAnimationsIfNeeded(positionedItem)
+ item.position(mainAxisOffset, layoutWidth, layoutHeight)
+ positionedItems.add(item)
+ startAnimationsIfNeeded(item)
}
movingInFromStartBound.clear()
@@ -200,7 +200,7 @@ internal class LazyListItemPlacementAnimator {
}
private fun initializeNode(
- item: LazyListPositionedItem,
+ item: LazyListMeasuredItem,
mainAxisOffset: Int
) {
val firstPlaceableOffset = item.getOffset(0)
@@ -219,7 +219,7 @@ internal class LazyListItemPlacementAnimator {
}
}
- private fun startAnimationsIfNeeded(item: LazyListPositionedItem) {
+ private fun startAnimationsIfNeeded(item: LazyListMeasuredItem) {
item.forEachNode { placeableIndex, node ->
val newTarget = item.getOffset(placeableIndex)
val currentTarget = node.rawOffset
@@ -234,13 +234,13 @@ internal class LazyListItemPlacementAnimator {
private val Any?.node get() = this as? LazyLayoutAnimateItemModifierNode
- private val LazyListPositionedItem.hasAnimations: Boolean
+ private val LazyListMeasuredItem.hasAnimations: Boolean
get() {
forEachNode { _, _ -> return true }
return false
}
- private inline fun LazyListPositionedItem.forEachNode(
+ private inline fun LazyListMeasuredItem.forEachNode(
block: (placeableIndex: Int, node: LazyLayoutAnimateItemModifierNode) -> Unit
) {
repeat(placeablesCount) { index ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
index 1a5eb66a97c..ae95eb22156 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
@@ -20,10 +20,9 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap
import androidx.compose.foundation.lazy.layout.LazyLayoutPinnableItem
-import androidx.compose.foundation.lazy.layout.NearestRangeKeyIndexMapState
+import androidx.compose.foundation.lazy.layout.NearestRangeKeyIndexMap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
import androidx.compose.runtime.referentialEqualityPolicy
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
@@ -39,64 +38,68 @@ internal interface LazyListItemProvider : LazyLayoutItemProvider {
@OptIn(ExperimentalFoundationApi::class)
@Composable
-internal fun rememberLazyListItemProvider(
+internal fun rememberLazyListItemProviderLambda(
state: LazyListState,
content: LazyListScope.() -> Unit
-): LazyListItemProvider {
+): () -> LazyListItemProvider {
val latestContent = rememberUpdatedState(content)
- return remember(state, latestContent) {
- LazyListItemProviderImpl(
- state = state,
- latestContent = { latestContent.value },
- itemScope = LazyItemScopeImpl()
- )
+ return remember(state) {
+ val scope = LazyItemScopeImpl()
+ val intervalContentState = derivedStateOf(referentialEqualityPolicy()) {
+ LazyListIntervalContent(latestContent.value)
+ }
+ val itemProviderState = derivedStateOf(referentialEqualityPolicy()) {
+ val intervalContent = intervalContentState.value
+ val map = NearestRangeKeyIndexMap(state.nearestRange, intervalContent)
+ LazyListItemProviderImpl(
+ state = state,
+ intervalContent = intervalContent,
+ itemScope = scope,
+ keyIndexMap = map
+ )
+ }
+ itemProviderState::value
}
}
@ExperimentalFoundationApi
private class LazyListItemProviderImpl constructor(
private val state: LazyListState,
- private val latestContent: () -> (LazyListScope.() -> Unit),
- override val itemScope: LazyItemScopeImpl
+ private val intervalContent: LazyListIntervalContent,
+ override val itemScope: LazyItemScopeImpl,
+ override val keyIndexMap: LazyLayoutKeyIndexMap,
) : LazyListItemProvider {
- private val listContent by derivedStateOf(referentialEqualityPolicy()) {
- LazyListIntervalContent(latestContent())
- }
- override val itemCount: Int get() = listContent.itemCount
+ override val itemCount: Int get() = intervalContent.itemCount
@Composable
override fun Item(index: Int, key: Any) {
LazyLayoutPinnableItem(key, index, state.pinnedItems) {
- listContent.withInterval(index) { localIndex, content ->
+ intervalContent.withInterval(index) { localIndex, content ->
content.item(itemScope, localIndex)
}
}
}
- override fun getKey(index: Int): Any = keyIndexMap.getKey(index) ?: listContent.getKey(index)
+ override fun getKey(index: Int): Any =
+ keyIndexMap.getKey(index) ?: intervalContent.getKey(index)
- override fun getContentType(index: Int): Any? = listContent.getContentType(index)
+ override fun getContentType(index: Int): Any? = intervalContent.getContentType(index)
- override val headerIndexes: List<Int> get() = listContent.headerIndexes
-
- override val keyIndexMap by NearestRangeKeyIndexMapState(
- firstVisibleItemIndex = { state.firstVisibleItemIndex },
- slidingWindowSize = { NearestItemsSlidingWindowSize },
- extraItemCount = { NearestItemsExtraItemCount },
- content = { listContent }
- )
+ override val headerIndexes: List<Int> get() = intervalContent.headerIndexes
override fun getIndex(key: Any): Int = keyIndexMap.getIndex(key)
-}
-/**
- * We use the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- */
-internal const val NearestItemsSlidingWindowSize = 30
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is LazyListItemProviderImpl) return false
-/**
- * The minimum amount of items near the current first visible item we want to have mapping for.
- */
-internal const val NearestItemsExtraItemCount = 100 \ No newline at end of file
+ // the identity of this class is represented by intervalContent object.
+ // having equals() allows us to skip items recomposition when intervalContent didn't change
+ return intervalContent == other.intervalContent
+ }
+
+ override fun hashCode(): Int {
+ return intervalContent.hashCode()
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index c247a1dcf82..7b4d920828d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -102,7 +102,7 @@ internal fun measureLazyList(
}
// this will contain all the MeasuredItems representing the visible items
- val visibleItems = mutableListOf<LazyListMeasuredItem>()
+ val visibleItems = ArrayDeque<LazyListMeasuredItem>()
// define min and max offsets
val minOffset = -beforeContentPadding + if (spaceBetweenItems < 0) spaceBetweenItems else 0
@@ -403,7 +403,7 @@ private fun calculateItemsOffsets(
horizontalArrangement: Arrangement.Horizontal?,
reverseLayout: Boolean,
density: Density,
-): MutableList<LazyListPositionedItem> {
+): MutableList<LazyListMeasuredItem> {
val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
val hasSpareSpace = finalMainAxisOffset < minOf(mainAxisLayoutSize, maxOffset)
if (hasSpareSpace) {
@@ -411,7 +411,7 @@ private fun calculateItemsOffsets(
}
val positionedItems =
- ArrayList<LazyListPositionedItem>(items.size + extraItemsBefore.size + extraItemsAfter.size)
+ ArrayList<LazyListMeasuredItem>(items.size + extraItemsBefore.size + extraItemsAfter.size)
if (hasSpareSpace) {
require(extraItemsBefore.isEmpty() && extraItemsAfter.isEmpty())
@@ -447,29 +447,29 @@ private fun calculateItemsOffsets(
} else {
absoluteOffset
}
- positionedItems.add(item.position(relativeOffset, layoutWidth, layoutHeight))
+ item.position(relativeOffset, layoutWidth, layoutHeight)
+ positionedItems.add(item)
}
} else {
var currentMainAxis = itemsScrollOffset
extraItemsBefore.fastForEach {
currentMainAxis -= it.sizeWithSpacings
- positionedItems.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ it.position(currentMainAxis, layoutWidth, layoutHeight)
+ positionedItems.add(it)
}
currentMainAxis = itemsScrollOffset
items.fastForEach {
- positionedItems.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ it.position(currentMainAxis, layoutWidth, layoutHeight)
+ positionedItems.add(it)
currentMainAxis += it.sizeWithSpacings
}
extraItemsAfter.fastForEach {
- positionedItems.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ it.position(currentMainAxis, layoutWidth, layoutHeight)
+ positionedItems.add(it)
currentMainAxis += it.sizeWithSpacings
}
}
return positionedItems
}
-
-private val EmptyRange = Int.MIN_VALUE to Int.MIN_VALUE
-private val Int.notInEmptyRange
- get() = this != Int.MIN_VALUE \ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt
index 0c4b130f962..5fb7dc952f8 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItem.kt
@@ -23,15 +23,16 @@ import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachIndexed
/**
* Represents one measured item of the lazy list. It can in fact consist of multiple placeables
* if the user emit multiple layout nodes in the item callback.
*/
internal class LazyListMeasuredItem @ExperimentalFoundationApi constructor(
- val index: Int,
+ override val index: Int,
private val placeables: List<Placeable>,
- private val isVertical: Boolean,
+ val isVertical: Boolean,
private val horizontalAlignment: Alignment.Horizontal?,
private val verticalAlignment: Alignment.Vertical?,
private val layoutDirection: LayoutDirection,
@@ -48,13 +49,16 @@ internal class LazyListMeasuredItem @ExperimentalFoundationApi constructor(
* value passed into the place() call.
*/
private val visualOffset: IntOffset,
- val key: Any,
- private val contentType: Any?
-) {
+ override val key: Any,
+ override val contentType: Any?
+) : LazyListItemInfo {
+ override var offset: Int = 0
+ private set
+
/**
* Sum of the main axis sizes of all the inner placeables.
*/
- val size: Int
+ override val size: Int
/**
* Sum of the main axis sizes of all the inner placeables and [spacing].
@@ -66,6 +70,14 @@ internal class LazyListMeasuredItem @ExperimentalFoundationApi constructor(
*/
val crossAxisSize: Int
+ private var mainAxisLayoutSize: Int = Unset
+ private var minMainAxisOffset: Int = 0
+ private var maxMainAxisOffset: Int = 0
+
+ // optimized for storing x and y offsets for each placeable one by one.
+ // array's size == placeables.size * 2, first we store x, then y.
+ private val placeableOffsets: IntArray
+
init {
var mainAxisSize = 0
var maxCrossAxis = 0
@@ -76,6 +88,7 @@ internal class LazyListMeasuredItem @ExperimentalFoundationApi constructor(
size = mainAxisSize
sizeWithSpacings = (size + spacing).coerceAtLeast(0)
crossAxisSize = maxCrossAxis
+ placeableOffsets = IntArray(placeables.size * 2)
}
val placeablesCount: Int get() = placeables.size
@@ -90,64 +103,37 @@ internal class LazyListMeasuredItem @ExperimentalFoundationApi constructor(
offset: Int,
layoutWidth: Int,
layoutHeight: Int
- ): LazyListPositionedItem {
- val wrappers = mutableListOf<LazyListPlaceableWrapper>()
- val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+ ) {
+ this.offset = offset
+ mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
var mainAxisOffset = offset
- placeables.fastForEach {
- val placeableOffset = if (isVertical) {
- val x = requireNotNull(horizontalAlignment)
- .align(it.width, layoutWidth, layoutDirection)
- IntOffset(x, mainAxisOffset)
+ placeables.fastForEachIndexed { index, placeable ->
+ val indexInArray = index * 2
+ if (isVertical) {
+ placeableOffsets[indexInArray] = requireNotNull(horizontalAlignment)
+ .align(placeable.width, layoutWidth, layoutDirection)
+ placeableOffsets[indexInArray + 1] = mainAxisOffset
+ mainAxisOffset += placeable.height
} else {
- val y = requireNotNull(verticalAlignment).align(it.height, layoutHeight)
- IntOffset(mainAxisOffset, y)
+ placeableOffsets[indexInArray] = mainAxisOffset
+ placeableOffsets[indexInArray + 1] = requireNotNull(verticalAlignment)
+ .align(placeable.height, layoutHeight)
+ mainAxisOffset += placeable.width
}
- mainAxisOffset += if (isVertical) it.height else it.width
- wrappers.add(LazyListPlaceableWrapper(placeableOffset, it))
}
- return LazyListPositionedItem(
- offset = offset,
- index = this.index,
- key = key,
- size = size,
- minMainAxisOffset = -beforeContentPadding,
- maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding,
- isVertical = isVertical,
- wrappers = wrappers,
- visualOffset = visualOffset,
- reverseLayout = reverseLayout,
- mainAxisLayoutSize = mainAxisLayoutSize,
- contentType = contentType
- )
+ minMainAxisOffset = -beforeContentPadding
+ maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding
}
-}
-
-internal class LazyListPositionedItem(
- override val offset: Int,
- override val index: Int,
- override val key: Any,
- override val size: Int,
- private val minMainAxisOffset: Int,
- private val maxMainAxisOffset: Int,
- val isVertical: Boolean,
- private val wrappers: List<LazyListPlaceableWrapper>,
- private val visualOffset: IntOffset,
- private val reverseLayout: Boolean,
- private val mainAxisLayoutSize: Int,
- override val contentType: Any?
-) : LazyListItemInfo {
- val placeablesCount: Int get() = wrappers.size
-
- fun getOffset(index: Int) = wrappers[index].offset
- fun getParentData(index: Int) = wrappers[index].placeable.parentData
+ fun getOffset(index: Int) =
+ IntOffset(placeableOffsets[index * 2], placeableOffsets[index * 2 + 1])
fun place(
scope: Placeable.PlacementScope,
) = with(scope) {
+ require(mainAxisLayoutSize != Unset) { "position() should be called first" }
repeat(placeablesCount) { index ->
- val placeable = wrappers[index].placeable
+ val placeable = placeables[index]
val minOffset = minMainAxisOffset - placeable.mainAxisSize
val maxOffset = maxMainAxisOffset
var offset = getOffset(index)
@@ -182,7 +168,4 @@ internal class LazyListPositionedItem(
IntOffset(if (isVertical) x else mainAxisMap(x), if (isVertical) mainAxisMap(y) else y)
}
-internal class LazyListPlaceableWrapper(
- val offset: IntOffset,
- val placeable: Placeable
-)
+private const val Unset = Int.MIN_VALUE \ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItemProvider.kt
index 77ec0760a3f..d983cd71b49 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasuredItemProvider.kt
@@ -26,12 +26,11 @@ import androidx.compose.ui.unit.Constraints
* Abstracts away the subcomposition from the measuring logic.
*/
@OptIn(ExperimentalFoundationApi::class)
-internal class LazyListMeasuredItemProvider @ExperimentalFoundationApi constructor(
+internal abstract class LazyListMeasuredItemProvider @ExperimentalFoundationApi constructor(
constraints: Constraints,
isVertical: Boolean,
private val itemProvider: LazyListItemProvider,
- private val measureScope: LazyLayoutMeasureScope,
- private val measuredItemFactory: MeasuredItemFactory
+ private val measureScope: LazyLayoutMeasureScope
) {
// the constraints we will measure child with. the main axis is not restricted
val childConstraints = Constraints(
@@ -44,22 +43,19 @@ internal class LazyListMeasuredItemProvider @ExperimentalFoundationApi construct
* correct constraints and wrapped into [LazyListMeasuredItem].
*/
fun getAndMeasure(index: Int): LazyListMeasuredItem {
- val key = keyIndexMap.getKey(index) ?: itemProvider.getKey(index)
+ val key = itemProvider.getKey(index)
val contentType = itemProvider.getContentType(index)
val placeables = measureScope.measure(index, childConstraints)
- return measuredItemFactory.createItem(index, key, contentType, placeables)
+ return createItem(index, key, contentType, placeables)
}
/**
* Contains the mapping between the key and the index. It could contain not all the items of
* the list as an optimization.
**/
- val keyIndexMap: LazyLayoutKeyIndexMap = itemProvider.keyIndexMap
-}
+ val keyIndexMap: LazyLayoutKeyIndexMap get() = itemProvider.keyIndexMap
-// This interface allows to avoid autoboxing on index param
-internal fun interface MeasuredItemFactory {
- fun createItem(
+ abstract fun createItem(
index: Int,
key: Any,
contentType: Any?,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt
index 16eb1c71ecb..44eade109b2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScrollPosition.kt
@@ -17,11 +17,11 @@
package androidx.compose.foundation.lazy
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutNearestRangeState
import androidx.compose.foundation.lazy.layout.findIndexByKey
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.Snapshot
/**
* Contains the current scroll position represented by the first visible item index and the first
@@ -41,6 +41,12 @@ internal class LazyListScrollPosition(
/** The last know key of the item at [index] position. */
private var lastKnownFirstItemKey: Any? = null
+ val nearestRangeState = LazyLayoutNearestRangeState(
+ initialIndex,
+ NearestItemsSlidingWindowSize,
+ NearestItemsExtraItemCount
+ )
+
/**
* Updates the current scroll position based on the results of the last measurement.
*/
@@ -54,12 +60,8 @@ internal class LazyListScrollPosition(
val scrollOffset = measureResult.firstVisibleItemScrollOffset
check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" }
- Snapshot.withoutReadObservation {
- update(
- measureResult.firstVisibleItem?.index ?: 0,
- scrollOffset
- )
- }
+ val firstIndex = measureResult.firstVisibleItem?.index ?: 0
+ update(firstIndex, scrollOffset)
}
}
@@ -88,22 +90,33 @@ internal class LazyListScrollPosition(
* as the first visible one even given that its index has been changed.
*/
@ExperimentalFoundationApi
- fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyListItemProvider) {
- Snapshot.withoutReadObservation {
- update(
- itemProvider.findIndexByKey(lastKnownFirstItemKey, index),
- scrollOffset
- )
+ fun updateScrollPositionIfTheFirstItemWasMoved(
+ itemProvider: LazyListItemProvider,
+ index: Int
+ ): Int {
+ val newIndex = itemProvider.findIndexByKey(lastKnownFirstItemKey, index)
+ if (index != newIndex) {
+ this.index = newIndex
+ nearestRangeState.update(index)
}
+ return newIndex
}
private fun update(index: Int, scrollOffset: Int) {
require(index >= 0f) { "Index should be non-negative ($index)" }
- if (index != this.index) {
- this.index = index
- }
- if (scrollOffset != this.scrollOffset) {
- this.scrollOffset = scrollOffset
- }
+ this.index = index
+ nearestRangeState.update(index)
+ this.scrollOffset = scrollOffset
}
}
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+internal const val NearestItemsSlidingWindowSize = 30
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+internal const val NearestItemsExtraItemCount = 100
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 3bd36be0054..1d2cf7a396e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -36,6 +36,7 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.layout.Remeasurement
import androidx.compose.ui.layout.RemeasurementModifier
import androidx.compose.ui.unit.Constraints
@@ -152,7 +153,7 @@ class LazyListState constructor(
/**
* Needed for [animateScrollToItem]. Updated on every measure.
*/
- internal var density: Density by mutableStateOf(Density(1f, 1f))
+ internal var density: Density = Density(1f, 1f)
/**
* The ScrollableController instance. We keep it as we need to call stopAnimation on it once
@@ -193,7 +194,7 @@ class LazyListState constructor(
* The [Remeasurement] object associated with our layout. It allows us to remeasure
* synchronously during scroll.
*/
- internal var remeasurement: Remeasurement? by mutableStateOf(null)
+ internal var remeasurement: Remeasurement? = null
private set
/**
@@ -218,13 +219,15 @@ class LazyListState constructor(
/**
* Constraints passed to the prefetcher for premeasuring the prefetched items.
*/
- internal var premeasureConstraints by mutableStateOf(Constraints())
+ internal var premeasureConstraints = Constraints()
/**
* Stores currently pinned items which are always composed.
*/
internal val pinnedItems = LazyLayoutPinnedItemList()
+ internal val nearestRange: IntRange by scrollPosition.nearestRangeState
+
/**
* Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
* pixels.
@@ -401,9 +404,10 @@ class LazyListState constructor(
* items added or removed before our current first visible item and keep this item
* as the first visible one even given that its index has been changed.
*/
- internal fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyListItemProvider) {
- scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
- }
+ internal fun updateScrollPositionIfTheFirstItemWasMoved(
+ itemProvider: LazyListItemProvider,
+ firstItemIndex: Int = Snapshot.withoutReadObservation { scrollPosition.index }
+ ): Int = scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex)
companion object {
/**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index 4f84478fc76..385b153e953 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -37,6 +37,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -75,12 +76,12 @@ internal fun LazyGrid(
) {
val overscrollEffect = ScrollableDefaults.overscrollEffect()
- val itemProvider = rememberLazyGridItemProvider(state, content)
+ val itemProviderLambda = rememberLazyGridItemProviderLambda(state, content)
val semanticState = rememberLazyGridSemanticState(state, reverseLayout)
val measurePolicy = rememberLazyGridMeasurePolicy(
- itemProvider,
+ itemProviderLambda,
state,
slots,
contentPadding,
@@ -92,7 +93,7 @@ internal fun LazyGrid(
state.isVertical = isVertical
- ScrollPositionUpdater(itemProvider, state)
+ ScrollPositionUpdater(itemProviderLambda, state)
val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal
LazyLayout(
@@ -100,7 +101,7 @@ internal fun LazyGrid(
.then(state.remeasurementModifier)
.then(state.awaitLayoutModifier)
.lazyLayoutSemantics(
- itemProvider = itemProvider,
+ itemProviderLambda = itemProviderLambda,
state = semanticState,
orientation = orientation,
userScrollEnabled = userScrollEnabled,
@@ -128,7 +129,7 @@ internal fun LazyGrid(
),
prefetchState = state.prefetchState,
measurePolicy = measurePolicy,
- itemProvider = itemProvider
+ itemProvider = itemProviderLambda
)
}
@@ -136,9 +137,10 @@ internal fun LazyGrid(
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ScrollPositionUpdater(
- itemProvider: LazyGridItemProvider,
+ itemProviderLambda: () -> LazyGridItemProvider,
state: LazyGridState
) {
+ val itemProvider = itemProviderLambda()
if (itemProvider.itemCount > 0) {
state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
}
@@ -154,7 +156,7 @@ internal class LazyGridSlots(
@Composable
private fun rememberLazyGridMeasurePolicy(
/** Items provider of the list. */
- itemProvider: LazyGridItemProvider,
+ itemProviderLambda: () -> LazyGridItemProvider,
/** The state of the list. */
state: LazyGridState,
/** Prefix sums of cross axis sizes of slots of the grid. */
@@ -215,8 +217,7 @@ private fun rememberLazyGridMeasurePolicy(
val contentConstraints =
containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding)
- state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
-
+ val itemProvider = itemProviderLambda()
val spanLayoutProvider = itemProvider.spanLayoutProvider
val resolvedSlots = slots(containerConstraints)
val slotsPerLine = resolvedSlots.sizes.size
@@ -252,12 +253,19 @@ private fun rememberLazyGridMeasurePolicy(
)
}
- val measuredItemProvider = LazyGridMeasuredItemProvider(
+ val measuredItemProvider = object : LazyGridMeasuredItemProvider(
itemProvider,
this,
spaceBetweenLines
- ) { index, key, contentType, crossAxisSize, mainAxisSpacing, placeables ->
- LazyGridMeasuredItem(
+ ) {
+ override fun createItem(
+ index: Int,
+ key: Any,
+ contentType: Any?,
+ crossAxisSize: Int,
+ mainAxisSpacing: Int,
+ placeables: List<Placeable>
+ ) = LazyGridMeasuredItem(
index = index,
key = key,
isVertical = isVertical,
@@ -272,15 +280,20 @@ private fun rememberLazyGridMeasurePolicy(
contentType = contentType
)
}
- val measuredLineProvider = LazyGridMeasuredLineProvider(
+ val measuredLineProvider = object : LazyGridMeasuredLineProvider(
isVertical = isVertical,
slots = resolvedSlots,
gridItemsCount = itemsCount,
spaceBetweenLines = spaceBetweenLines,
measuredItemProvider = measuredItemProvider,
spanLayoutProvider = spanLayoutProvider
- ) { index, items, spans, mainAxisSpacing ->
- LazyGridMeasuredLine(
+ ) {
+ override fun createLine(
+ index: Int,
+ items: Array<LazyGridMeasuredItem>,
+ spans: List<GridItemSpan>,
+ mainAxisSpacing: Int
+ ) = LazyGridMeasuredLine(
index = index,
items = items,
spans = spans,
@@ -307,10 +320,11 @@ private fun rememberLazyGridMeasurePolicy(
val firstVisibleLineScrollOffset: Int
Snapshot.withoutReadObservation {
- if (state.firstVisibleItemIndex < itemsCount || itemsCount <= 0) {
- firstVisibleLineIndex = spanLayoutProvider.getLineIndexOfItem(
- state.firstVisibleItemIndex
- )
+ val index = state.updateScrollPositionIfTheFirstItemWasMoved(
+ itemProvider, state.firstVisibleItemIndex
+ )
+ if (index < itemsCount || itemsCount <= 0) {
+ firstVisibleLineIndex = spanLayoutProvider.getLineIndexOfItem(index)
firstVisibleLineScrollOffset = state.firstVisibleItemScrollOffset
} else {
// the data set has been updated and now we have less items that we were
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
index fbd96ee7e8a..918f2a2881f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -41,8 +41,8 @@ internal class LazyGridItemPlacementAnimator {
// stored to not allocate it every pass.
private val movingAwayKeys = LinkedHashSet<Any>()
- private val movingInFromStartBound = mutableListOf<LazyGridPositionedItem>()
- private val movingInFromEndBound = mutableListOf<LazyGridPositionedItem>()
+ private val movingInFromStartBound = mutableListOf<LazyGridMeasuredItem>()
+ private val movingInFromEndBound = mutableListOf<LazyGridMeasuredItem>()
private val movingAwayToStartBound = mutableListOf<LazyGridMeasuredItem>()
private val movingAwayToEndBound = mutableListOf<LazyGridMeasuredItem>()
@@ -55,7 +55,7 @@ internal class LazyGridItemPlacementAnimator {
consumedScroll: Int,
layoutWidth: Int,
layoutHeight: Int,
- positionedItems: MutableList<LazyGridPositionedItem>,
+ positionedItems: MutableList<LazyGridMeasuredItem>,
itemProvider: LazyGridMeasuredItemProvider,
spanLayoutProvider: LazyGridSpanLayoutProvider,
isVertical: Boolean
@@ -91,7 +91,7 @@ internal class LazyGridItemPlacementAnimator {
// there is no state associated with this item yet
if (itemInfo == null) {
keyToItemInfoMap[item.key] =
- ItemInfo(item.getCrossAxisSize(), item.getCrossAxisOffset())
+ ItemInfo(item.crossAxisSize, item.crossAxisOffset)
val previousIndex = previousKeyToIndexMap.getIndex(item.key)
if (previousIndex != -1 && item.index != previousIndex) {
if (previousIndex < previousFirstVisibleIndex) {
@@ -112,8 +112,8 @@ internal class LazyGridItemPlacementAnimator {
it.rawOffset += scrollOffset
}
}
- itemInfo.crossAxisSize = item.getCrossAxisSize()
- itemInfo.crossAxisOffset = item.getCrossAxisOffset()
+ itemInfo.crossAxisSize = item.crossAxisSize
+ itemInfo.crossAxisOffset = item.crossAxisOffset
startAnimationsIfNeeded(item)
}
} else {
@@ -129,13 +129,13 @@ internal class LazyGridItemPlacementAnimator {
movingInFromStartBound.fastForEach { item ->
val line = if (isVertical) item.row else item.column
if (line != -1 && line == previousLine) {
- previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.getMainAxisSize())
+ previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.mainAxisSize)
} else {
accumulatedOffset += previousLineMainAxisSize
- previousLineMainAxisSize = item.getMainAxisSize()
+ previousLineMainAxisSize = item.mainAxisSize
previousLine = line
}
- val mainAxisOffset = 0 - accumulatedOffset - item.getMainAxisSize()
+ val mainAxisOffset = 0 - accumulatedOffset - item.mainAxisSize
initializeNode(item, mainAxisOffset)
startAnimationsIfNeeded(item)
}
@@ -146,10 +146,10 @@ internal class LazyGridItemPlacementAnimator {
movingInFromEndBound.fastForEach { item ->
val line = if (isVertical) item.row else item.column
if (line != -1 && line == previousLine) {
- previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.getMainAxisSize())
+ previousLineMainAxisSize = maxOf(previousLineMainAxisSize, item.mainAxisSize)
} else {
accumulatedOffset += previousLineMainAxisSize
- previousLineMainAxisSize = item.getMainAxisSize()
+ previousLineMainAxisSize = item.mainAxisSize
previousLine = line
}
val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset
@@ -211,16 +211,14 @@ internal class LazyGridItemPlacementAnimator {
val itemInfo = keyToItemInfoMap.getValue(item.key)
- val positionedItem = item.position(
- mainAxisOffset,
- itemInfo.crossAxisOffset,
- layoutWidth,
- layoutHeight,
- LazyGridItemInfo.UnknownRow,
- LazyGridItemInfo.UnknownColumn
+ item.position(
+ mainAxisOffset = mainAxisOffset,
+ crossAxisOffset = itemInfo.crossAxisOffset,
+ layoutWidth = layoutWidth,
+ layoutHeight = layoutHeight
)
- positionedItems.add(positionedItem)
- startAnimationsIfNeeded(positionedItem)
+ positionedItems.add(item)
+ startAnimationsIfNeeded(item)
}
accumulatedOffset = 0
previousLine = -1
@@ -238,17 +236,15 @@ internal class LazyGridItemPlacementAnimator {
val mainAxisOffset = mainAxisLayoutSize + accumulatedOffset
val itemInfo = keyToItemInfoMap.getValue(item.key)
- val positionedItem = item.position(
- mainAxisOffset,
- itemInfo.crossAxisOffset,
- layoutWidth,
- layoutHeight,
- LazyGridItemInfo.UnknownRow,
- LazyGridItemInfo.UnknownColumn
+ item.position(
+ mainAxisOffset = mainAxisOffset,
+ crossAxisOffset = itemInfo.crossAxisOffset,
+ layoutWidth = layoutWidth,
+ layoutHeight = layoutHeight,
)
- positionedItems.add(positionedItem)
- startAnimationsIfNeeded(positionedItem)
+ positionedItems.add(item)
+ startAnimationsIfNeeded(item)
}
movingInFromStartBound.clear()
@@ -269,7 +265,7 @@ internal class LazyGridItemPlacementAnimator {
}
private fun initializeNode(
- item: LazyGridPositionedItem,
+ item: LazyGridMeasuredItem,
mainAxisOffset: Int
) {
val firstPlaceableOffset = item.offset
@@ -288,7 +284,7 @@ internal class LazyGridItemPlacementAnimator {
}
}
- private fun startAnimationsIfNeeded(item: LazyGridPositionedItem) {
+ private fun startAnimationsIfNeeded(item: LazyGridMeasuredItem) {
item.forEachNode { node ->
val newTarget = item.offset
val currentTarget = node.rawOffset
@@ -303,13 +299,13 @@ internal class LazyGridItemPlacementAnimator {
private val Any?.node get() = this as? LazyLayoutAnimateItemModifierNode
- private val LazyGridPositionedItem.hasAnimations: Boolean
+ private val LazyGridMeasuredItem.hasAnimations: Boolean
get() {
forEachNode { return true }
return false
}
- private inline fun LazyGridPositionedItem.forEachNode(
+ private inline fun LazyGridMeasuredItem.forEachNode(
block: (LazyLayoutAnimateItemModifierNode) -> Unit
) {
repeat(placeablesCount) { index ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
index 95c2af305b6..55d346b836d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
@@ -20,10 +20,9 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap
import androidx.compose.foundation.lazy.layout.LazyLayoutPinnableItem
-import androidx.compose.foundation.lazy.layout.NearestRangeKeyIndexMapState
+import androidx.compose.foundation.lazy.layout.NearestRangeKeyIndexMap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
import androidx.compose.runtime.referentialEqualityPolicy
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
@@ -36,63 +35,66 @@ internal interface LazyGridItemProvider : LazyLayoutItemProvider {
@ExperimentalFoundationApi
@Composable
-internal fun rememberLazyGridItemProvider(
+internal fun rememberLazyGridItemProviderLambda(
state: LazyGridState,
content: LazyGridScope.() -> Unit,
-): LazyGridItemProvider {
+): () -> LazyGridItemProvider {
val latestContent = rememberUpdatedState(content)
return remember(state) {
- LazyGridItemProviderImpl(
- state,
- { latestContent.value },
- )
+ val intervalContentState = derivedStateOf(referentialEqualityPolicy()) {
+ LazyGridIntervalContent(latestContent.value)
+ }
+ val itemProviderState = derivedStateOf(referentialEqualityPolicy()) {
+ val intervalContent = intervalContentState.value
+ val map = NearestRangeKeyIndexMap(state.nearestRange, intervalContent)
+ LazyGridItemProviderImpl(
+ state = state,
+ intervalContent = intervalContent,
+ keyIndexMap = map
+ )
+ }
+ itemProviderState::value
}
}
@ExperimentalFoundationApi
private class LazyGridItemProviderImpl(
private val state: LazyGridState,
- private val latestContent: () -> (LazyGridScope.() -> Unit)
+ private val intervalContent: LazyGridIntervalContent,
+ override val keyIndexMap: LazyLayoutKeyIndexMap,
) : LazyGridItemProvider {
- private val gridContent by derivedStateOf(referentialEqualityPolicy()) {
- LazyGridIntervalContent(latestContent())
- }
- override val keyIndexMap: LazyLayoutKeyIndexMap by NearestRangeKeyIndexMapState(
- firstVisibleItemIndex = { state.firstVisibleItemIndex },
- slidingWindowSize = { NearestItemsSlidingWindowSize },
- extraItemCount = { NearestItemsExtraItemCount },
- content = { gridContent }
- )
+ override val itemCount: Int get() = intervalContent.itemCount
- override val itemCount: Int get() = gridContent.itemCount
+ override fun getKey(index: Int): Any =
+ keyIndexMap.getKey(index) ?: intervalContent.getKey(index)
- override fun getKey(index: Int): Any = keyIndexMap.getKey(index) ?: gridContent.getKey(index)
-
- override fun getContentType(index: Int): Any? = gridContent.getContentType(index)
+ override fun getContentType(index: Int): Any? = intervalContent.getContentType(index)
@Composable
override fun Item(index: Int, key: Any) {
LazyLayoutPinnableItem(key, index, state.pinnedItems) {
- gridContent.withInterval(index) { localIndex, content ->
+ intervalContent.withInterval(index) { localIndex, content ->
content.item(LazyGridItemScopeImpl, localIndex)
}
}
}
override val spanLayoutProvider: LazyGridSpanLayoutProvider
- get() = gridContent.spanLayoutProvider
+ get() = intervalContent.spanLayoutProvider
override fun getIndex(key: Any): Int = keyIndexMap.getIndex(key)
-}
-/**
- * We use the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- */
-private const val NearestItemsSlidingWindowSize = 90
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is LazyGridItemProviderImpl) return false
-/**
- * The minimum amount of items near the current first visible item we want to have mapping for.
- */
-private const val NearestItemsExtraItemCount = 200 \ No newline at end of file
+ // the identity of this class is represented by intervalContent object.
+ // having equals() allows us to skip items recomposition when intervalContent didn't change
+ return intervalContent == other.intervalContent
+ }
+
+ override fun hashCode(): Int {
+ return intervalContent.hashCode()
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index d5fa3ecadbf..b9258895247 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -98,7 +98,7 @@ internal fun measureLazyGrid(
}
// this will contain all the MeasuredItems representing the visible lines
- val visibleLines = mutableListOf<LazyGridMeasuredLine>()
+ val visibleLines = ArrayDeque<LazyGridMeasuredLine>()
// define min and max offsets
val minOffset = -beforeContentPadding + if (spaceBetweenLines < 0) spaceBetweenLines else 0
@@ -343,14 +343,14 @@ private fun calculateItemsOffsets(
horizontalArrangement: Arrangement.Horizontal?,
reverseLayout: Boolean,
density: Density,
-): MutableList<LazyGridPositionedItem> {
+): MutableList<LazyGridMeasuredItem> {
val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
val hasSpareSpace = finalMainAxisOffset < min(mainAxisLayoutSize, maxOffset)
if (hasSpareSpace) {
check(firstLineScrollOffset == 0)
}
- val positionedItems = ArrayList<LazyGridPositionedItem>(lines.fastSumBy { it.items.size })
+ val positionedItems = ArrayList<LazyGridMeasuredItem>(lines.fastSumBy { it.items.size })
if (hasSpareSpace) {
require(itemsBefore.isEmpty() && itemsAfter.isEmpty())
@@ -395,7 +395,8 @@ private fun calculateItemsOffsets(
itemsBefore.fastForEach {
currentMainAxis -= it.mainAxisSizeWithSpacings
- positionedItems.add(it.positionExtraItem(currentMainAxis, layoutWidth, layoutHeight))
+ it.position(currentMainAxis, 0, layoutWidth, layoutHeight)
+ positionedItems.add(it)
}
currentMainAxis = firstLineScrollOffset
@@ -405,23 +406,10 @@ private fun calculateItemsOffsets(
}
itemsAfter.fastForEach {
- positionedItems.add(it.positionExtraItem(currentMainAxis, layoutWidth, layoutHeight))
+ it.position(currentMainAxis, 0, layoutWidth, layoutHeight)
+ positionedItems.add(it)
currentMainAxis += it.mainAxisSizeWithSpacings
}
}
return positionedItems
}
-
-private fun LazyGridMeasuredItem.positionExtraItem(
- mainAxisOffset: Int,
- layoutWidth: Int,
- layoutHeight: Int
-): LazyGridPositionedItem =
- position(
- mainAxisOffset = mainAxisOffset,
- crossAxisOffset = 0,
- layoutWidth = layoutWidth,
- layoutHeight = layoutHeight,
- row = -1,
- column = -1
- )
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
index 74a6896e2c9..ecf9ff7f2ec 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItem.kt
@@ -28,27 +28,27 @@ import androidx.compose.ui.util.fastForEach
* if the user emit multiple layout nodes in the item callback.
*/
internal class LazyGridMeasuredItem(
- val index: Int,
- val key: Any,
- private val isVertical: Boolean,
+ override val index: Int,
+ override val key: Any,
+ val isVertical: Boolean,
/**
* Cross axis size is the same for all [placeables]. Take it as parameter for the case when
* [placeables] is empty.
*/
val crossAxisSize: Int,
- val mainAxisSpacing: Int,
+ mainAxisSpacing: Int,
private val reverseLayout: Boolean,
private val layoutDirection: LayoutDirection,
private val beforeContentPadding: Int,
private val afterContentPadding: Int,
- val placeables: List<Placeable>,
+ private val placeables: List<Placeable>,
/**
* The offset which shouldn't affect any calculations but needs to be applied for the final
* value passed into the place() call.
*/
private val visualOffset: IntOffset,
- private val contentType: Any?
-) {
+ override val contentType: Any?
+) : LazyGridItemInfo {
/**
* Main axis size of the item - the max main axis size of the placeables.
*/
@@ -61,6 +61,10 @@ internal class LazyGridMeasuredItem(
val placeablesCount: Int get() = placeables.size
+ private var mainAxisLayoutSize: Int = Unset
+ private var minMainAxisOffset: Int = 0
+ private var maxMainAxisOffset: Int = 0
+
fun getParentData(index: Int) = placeables[index].parentData
init {
@@ -72,6 +76,19 @@ internal class LazyGridMeasuredItem(
mainAxisSizeWithSpacings = (maxMainAxis + mainAxisSpacing).coerceAtLeast(0)
}
+ override val size: IntSize = if (isVertical) {
+ IntSize(crossAxisSize, mainAxisSize)
+ } else {
+ IntSize(mainAxisSize, crossAxisSize)
+ }
+ override var offset: IntOffset = IntOffset.Zero
+ private set
+ val crossAxisOffset get() = if (isVertical) offset.x else offset.y
+ override var row: Int = LazyGridItemInfo.UnknownRow
+ private set
+ override var column: Int = LazyGridItemInfo.UnknownColumn
+ private set
+
/**
* Calculates positions for the inner placeables at [mainAxisOffset], [crossAxisOffset].
* [layoutWidth] and [layoutHeight] should be provided to not place placeables which are ended
@@ -84,10 +101,10 @@ internal class LazyGridMeasuredItem(
crossAxisOffset: Int,
layoutWidth: Int,
layoutHeight: Int,
- row: Int,
- column: Int
- ): LazyGridPositionedItem {
- val mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
+ row: Int = LazyGridItemInfo.UnknownRow,
+ column: Int = LazyGridItemInfo.UnknownColumn
+ ) {
+ mainAxisLayoutSize = if (isVertical) layoutHeight else layoutWidth
val crossAxisLayoutSize = if (isVertical) layoutWidth else layoutHeight
@Suppress("NAME_SHADOWING")
val crossAxisOffset = if (isVertical && layoutDirection == LayoutDirection.Rtl) {
@@ -95,62 +112,21 @@ internal class LazyGridMeasuredItem(
} else {
crossAxisOffset
}
- return LazyGridPositionedItem(
- offset = if (isVertical) {
- IntOffset(crossAxisOffset, mainAxisOffset)
- } else {
- IntOffset(mainAxisOffset, crossAxisOffset)
- },
- index = index,
- key = key,
- row = row,
- column = column,
- size = if (isVertical) {
- IntSize(crossAxisSize, mainAxisSize)
- } else {
- IntSize(mainAxisSize, crossAxisSize)
- },
- minMainAxisOffset = -beforeContentPadding,
- maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding,
- isVertical = isVertical,
- placeables = placeables,
- visualOffset = visualOffset,
- mainAxisLayoutSize = mainAxisLayoutSize,
- reverseLayout = reverseLayout,
- contentType = contentType
- )
+ offset = if (isVertical) {
+ IntOffset(crossAxisOffset, mainAxisOffset)
+ } else {
+ IntOffset(mainAxisOffset, crossAxisOffset)
+ }
+ this.row = row
+ this.column = column
+ minMainAxisOffset = -beforeContentPadding
+ maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding
}
-}
-
-internal class LazyGridPositionedItem(
- override val offset: IntOffset,
- override val index: Int,
- override val key: Any,
- override val row: Int,
- override val column: Int,
- override val size: IntSize,
- private val minMainAxisOffset: Int,
- private val maxMainAxisOffset: Int,
- val isVertical: Boolean,
- private val placeables: List<Placeable>,
- private val visualOffset: IntOffset,
- private val mainAxisLayoutSize: Int,
- private val reverseLayout: Boolean,
- override val contentType: Any?
-) : LazyGridItemInfo {
- val placeablesCount: Int get() = placeables.size
-
- fun getMainAxisSize() = if (isVertical) size.height else size.width
-
- fun getCrossAxisSize() = if (isVertical) size.width else size.height
-
- fun getCrossAxisOffset() = if (isVertical) offset.x else offset.y
-
- fun getParentData(index: Int) = placeables[index].parentData
fun place(
scope: Placeable.PlacementScope,
) = with(scope) {
+ require(mainAxisLayoutSize != Unset) { "position() should be called first" }
repeat(placeablesCount) { index ->
val placeable = placeables[index]
val minOffset = minMainAxisOffset - placeable.mainAxisSize
@@ -187,3 +163,5 @@ internal class LazyGridPositionedItem(
private inline fun IntOffset.copy(mainAxisMap: (Int) -> Int): IntOffset =
IntOffset(if (isVertical) x else mainAxisMap(x), if (isVertical) mainAxisMap(y) else y)
}
+
+private const val Unset = Int.MIN_VALUE \ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt
index c9846316c71..05be19d8eba 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredItemProvider.kt
@@ -26,11 +26,10 @@ import androidx.compose.ui.unit.Constraints
* Abstracts away the subcomposition from the measuring logic.
*/
@OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridMeasuredItemProvider @ExperimentalFoundationApi constructor(
+internal abstract class LazyGridMeasuredItemProvider @ExperimentalFoundationApi constructor(
private val itemProvider: LazyGridItemProvider,
private val measureScope: LazyLayoutMeasureScope,
- private val defaultMainAxisSpacing: Int,
- private val measuredItemFactory: MeasuredItemFactory
+ private val defaultMainAxisSpacing: Int
) {
/**
* Used to subcompose individual items of lazy grids. Composed placeables will be measured
@@ -41,7 +40,7 @@ internal class LazyGridMeasuredItemProvider @ExperimentalFoundationApi construct
mainAxisSpacing: Int = defaultMainAxisSpacing,
constraints: Constraints
): LazyGridMeasuredItem {
- val key = keyIndexMap.getKey(index) ?: itemProvider.getKey(index)
+ val key = itemProvider.getKey(index)
val contentType = itemProvider.getContentType(index)
val placeables = measureScope.measure(index, constraints)
val crossAxisSize = if (constraints.hasFixedWidth) {
@@ -50,7 +49,7 @@ internal class LazyGridMeasuredItemProvider @ExperimentalFoundationApi construct
require(constraints.hasFixedHeight)
constraints.minHeight
}
- return measuredItemFactory.createItem(
+ return createItem(
index,
key,
contentType,
@@ -64,12 +63,9 @@ internal class LazyGridMeasuredItemProvider @ExperimentalFoundationApi construct
* Contains the mapping between the key and the index. It could contain not all the items of
* the list as an optimization.
**/
- val keyIndexMap: LazyLayoutKeyIndexMap = itemProvider.keyIndexMap
-}
+ val keyIndexMap: LazyLayoutKeyIndexMap get() = itemProvider.keyIndexMap
-// This interface allows to avoid autoboxing on index param
-internal fun interface MeasuredItemFactory {
- fun createItem(
+ abstract fun createItem(
index: Int,
key: Any,
contentType: Any?,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt
index 0b3f5e04527..f5f8349b8a1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLine.kt
@@ -66,9 +66,9 @@ internal class LazyGridMeasuredLine constructor(
offset: Int,
layoutWidth: Int,
layoutHeight: Int
- ): List<LazyGridPositionedItem> {
+ ): Array<LazyGridMeasuredItem> {
var usedSpan = 0
- return items.mapIndexed { itemIndex, item ->
+ items.forEachIndexed { itemIndex, item ->
val span = spans[itemIndex].currentLineSpan
val startSlot = usedSpan
@@ -83,5 +83,6 @@ internal class LazyGridMeasuredLine constructor(
usedSpan += span
}
}
+ return items
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLineProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLineProvider.kt
index fe5ff5498b3..c852c3f3fca 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLineProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasuredLineProvider.kt
@@ -24,14 +24,13 @@ import androidx.compose.ui.unit.Constraints
* Abstracts away subcomposition and span calculation from the measuring logic of entire lines.
*/
@OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridMeasuredLineProvider(
+internal abstract class LazyGridMeasuredLineProvider(
private val isVertical: Boolean,
private val slots: LazyGridSlots,
private val gridItemsCount: Int,
private val spaceBetweenLines: Int,
private val measuredItemProvider: LazyGridMeasuredItemProvider,
- private val spanLayoutProvider: LazyGridSpanLayoutProvider,
- private val measuredLineFactory: MeasuredLineFactory
+ private val spanLayoutProvider: LazyGridSpanLayoutProvider
) {
// The constraints for cross axis size. The main axis is not restricted.
internal fun childConstraints(startSlot: Int, span: Int): Constraints {
@@ -67,7 +66,8 @@ internal class LazyGridMeasuredLineProvider(
// we add space between lines as an extra spacing for all lines apart from the last one
// so the lazy grid measuring logic will take it into account.
val mainAxisSpacing = if (lineItemsCount == 0 ||
- lineConfiguration.firstItemIndex + lineItemsCount == gridItemsCount) {
+ lineConfiguration.firstItemIndex + lineItemsCount == gridItemsCount
+ ) {
0
} else {
spaceBetweenLines
@@ -83,7 +83,7 @@ internal class LazyGridMeasuredLineProvider(
constraints
).also { startSlot += span }
}
- return measuredLineFactory.createLine(
+ return createLine(
lineIndex,
items,
lineConfiguration.spans,
@@ -96,12 +96,8 @@ internal class LazyGridMeasuredLineProvider(
* the list as an optimization.
**/
val keyIndexMap: LazyLayoutKeyIndexMap get() = measuredItemProvider.keyIndexMap
-}
-// This interface allows to avoid autoboxing on index param
-@OptIn(ExperimentalFoundationApi::class)
-internal fun interface MeasuredLineFactory {
- fun createLine(
+ abstract fun createLine(
index: Int,
items: Array<LazyGridMeasuredItem>,
spans: List<GridItemSpan>,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt
index 7b196194664..7ec7f7020b4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScrollPosition.kt
@@ -17,11 +17,11 @@
package androidx.compose.foundation.lazy.grid
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutNearestRangeState
import androidx.compose.foundation.lazy.layout.findIndexByKey
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.Snapshot
/**
* Contains the current scroll position represented by the first visible item index and the first
@@ -43,6 +43,12 @@ internal class LazyGridScrollPosition(
/** The last known key of the first item at [index] line. */
private var lastKnownFirstItemKey: Any? = null
+ val nearestRangeState = LazyLayoutNearestRangeState(
+ initialIndex,
+ NearestItemsSlidingWindowSize,
+ NearestItemsExtraItemCount
+ )
+
/**
* Updates the current scroll position based on the results of the last measurement.
*/
@@ -56,12 +62,8 @@ internal class LazyGridScrollPosition(
val scrollOffset = measureResult.firstVisibleLineScrollOffset
check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" }
- Snapshot.withoutReadObservation {
- update(
- measureResult.firstVisibleLine?.items?.firstOrNull()?.index ?: 0,
- scrollOffset
- )
- }
+ val firstIndex = measureResult.firstVisibleLine?.items?.firstOrNull()?.index ?: 0
+ update(firstIndex, scrollOffset)
}
}
@@ -89,22 +91,33 @@ internal class LazyGridScrollPosition(
* there were items added or removed before our current first visible item and keep this item
* as the first visible one even given that its index has been changed.
*/
- fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyGridItemProvider) {
- Snapshot.withoutReadObservation {
- update(
- itemProvider.findIndexByKey(lastKnownFirstItemKey, index),
- scrollOffset
- )
+ fun updateScrollPositionIfTheFirstItemWasMoved(
+ itemProvider: LazyGridItemProvider,
+ index: Int
+ ): Int {
+ val newIndex = itemProvider.findIndexByKey(lastKnownFirstItemKey, index)
+ if (index != newIndex) {
+ this.index = newIndex
+ nearestRangeState.update(index)
}
+ return newIndex
}
private fun update(index: Int, scrollOffset: Int) {
require(index >= 0f) { "Index should be non-negative ($index)" }
- if (index != this.index) {
- this.index = index
- }
- if (scrollOffset != this.scrollOffset) {
- this.scrollOffset = scrollOffset
- }
+ this.index = index
+ nearestRangeState.update(index)
+ this.scrollOffset = scrollOffset
}
}
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+private const val NearestItemsSlidingWindowSize = 90
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+private const val NearestItemsExtraItemCount = 200
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
index 73e2a21b4f7..2ab8a169a16 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
@@ -38,6 +38,7 @@ import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.layout.Remeasurement
import androidx.compose.ui.layout.RemeasurementModifier
import androidx.compose.ui.unit.Constraints
@@ -155,12 +156,12 @@ class LazyGridState constructor(
/**
* Needed for [animateScrollToItem]. Updated on every measure.
*/
- internal var density: Density by mutableStateOf(Density(1f, 1f))
+ internal var density: Density = Density(1f, 1f)
/**
* Needed for [notifyPrefetch].
*/
- internal var isVertical: Boolean by mutableStateOf(true)
+ internal var isVertical: Boolean = true
/**
* The ScrollableController instance. We keep it as we need to call stopAnimation on it once
@@ -202,7 +203,7 @@ class LazyGridState constructor(
* The [Remeasurement] object associated with our layout. It allows us to remeasure
* synchronously during scroll.
*/
- internal var remeasurement: Remeasurement? by mutableStateOf(null)
+ internal var remeasurement: Remeasurement? = null
/**
* The modifier which provides [remeasurement].
@@ -236,6 +237,8 @@ class LazyGridState constructor(
*/
internal val pinnedItems = LazyLayoutPinnedItemList()
+ internal val nearestRange: IntRange by scrollPosition.nearestRangeState
+
/**
* Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset]
* pixels.
@@ -428,9 +431,10 @@ class LazyGridState constructor(
* items added or removed before our current first visible item and keep this item
* as the first visible one even given that its index has been changed.
*/
- internal fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyGridItemProvider) {
- scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
- }
+ internal fun updateScrollPositionIfTheFirstItemWasMoved(
+ itemProvider: LazyGridItemProvider,
+ firstItemIndex: Int = Snapshot.withoutReadObservation { scrollPosition.index }
+ ): Int = scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex)
companion object {
/**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
index f43b2748ddd..ffc8ddc6c3e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayout.kt
@@ -45,11 +45,22 @@ fun LazyLayout(
prefetchState: LazyLayoutPrefetchState? = null,
measurePolicy: LazyLayoutMeasureScope.(Constraints) -> MeasureResult
) {
+ LazyLayout({ itemProvider }, modifier, prefetchState, measurePolicy)
+}
+
+@ExperimentalFoundationApi
+@Composable
+internal fun LazyLayout(
+ itemProvider: () -> LazyLayoutItemProvider,
+ modifier: Modifier = Modifier,
+ prefetchState: LazyLayoutPrefetchState? = null,
+ measurePolicy: LazyLayoutMeasureScope.(Constraints) -> MeasureResult
+) {
val currentItemProvider = rememberUpdatedState(itemProvider)
LazySaveableStateHolderProvider { saveableStateHolder ->
val itemContentFactory = remember {
- LazyLayoutItemContentFactory(saveableStateHolder) { currentItemProvider.value }
+ LazyLayoutItemContentFactory(saveableStateHolder) { currentItemProvider.value() }
}
val subcomposeLayoutState = remember {
SubcomposeLayoutState(LazyLayoutItemReusePolicy(itemContentFactory))
@@ -67,11 +78,13 @@ fun LazyLayout(
modifier,
remember(itemContentFactory, measurePolicy) {
{ constraints ->
- with(LazyLayoutMeasureScopeImpl(
- itemContentFactory,
- this,
- prefetchState?.prefetcher?.timeTracker
- )) {
+ with(
+ LazyLayoutMeasureScopeImpl(
+ itemContentFactory,
+ this,
+ prefetchState?.prefetcher?.timeTracker
+ )
+ ) {
measurePolicy(constraints)
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
index 0b3039be798..c6a41abd995 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemContentFactory.kt
@@ -21,10 +21,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.ReusableContentHost
import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.saveable.SaveableStateHolder
-import androidx.compose.runtime.setValue
/**
* This class:
@@ -52,7 +49,7 @@ internal class LazyLayoutItemContentFactory(
val cachedContent = lambdasCache[key]
return if (cachedContent != null) {
- cachedContent.type
+ cachedContent.contentType
} else {
val itemProvider = itemProvider()
val index = itemProvider.getIndex(key)
@@ -67,24 +64,24 @@ internal class LazyLayoutItemContentFactory(
/**
* Return cached item content lambda or creates a new lambda and puts it in the cache.
*/
- fun getContent(index: Int, key: Any): @Composable () -> Unit {
+ fun getContent(index: Int, key: Any, contentType: Any?): @Composable () -> Unit {
val cached = lambdasCache[key]
- val type = itemProvider().getContentType(index)
- return if (cached != null && cached.lastKnownIndex == index && cached.type == type) {
+ return if (cached != null && cached.index == index && cached.contentType == contentType) {
cached.content
} else {
- val newContent = CachedItemContent(index, key, type)
+ val newContent = CachedItemContent(index, key, contentType)
lambdasCache[key] = newContent
newContent.content
}
}
private inner class CachedItemContent(
- initialIndex: Int,
+ index: Int,
val key: Any,
- val type: Any?
+ val contentType: Any?
) {
- var lastKnownIndex by mutableIntStateOf(initialIndex)
+ // the index resolved during the latest composition
+ var index = index
private set
private var _content: (@Composable () -> Unit)? = null
@@ -94,10 +91,10 @@ internal class LazyLayoutItemContentFactory(
private fun createContentLambda() = @Composable {
val itemProvider = itemProvider()
- var index = lastKnownIndex
+ var index = index
if (index >= itemProvider.itemCount || itemProvider.getKey(index) != key) {
index = itemProvider.getIndex(key)
- if (index != -1) lastKnownIndex = index
+ if (index != -1) this.index = index
}
ReusableContentHost(active = index != -1) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
index 9498578b954..e9f28587d31 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
@@ -70,7 +70,7 @@ internal fun LazyLayoutItemProvider.findIndexByKey(
key: Any?,
lastKnownIndex: Int,
): Int {
- if (key == null) {
+ if (key == null || itemCount == 0) {
// there were no real item during the previous measure
return lastKnownIndex
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutKeyIndexMap.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutKeyIndexMap.kt
index a6485eea17d..8ba9d37d270 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutKeyIndexMap.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutKeyIndexMap.kt
@@ -17,11 +17,6 @@
package androidx.compose.foundation.lazy.layout
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.referentialEqualityPolicy
-import androidx.compose.runtime.structuralEqualityPolicy
/**
* A key-index mapping used inside the [LazyLayoutItemProvider]. It might not contain all items
@@ -51,60 +46,22 @@ internal interface LazyLayoutKeyIndexMap {
}
/**
- * State containing [LazyLayoutKeyIndexMap] precalculated for range of indexes near first visible
- * item.
- * It is optimized to return the same range for small changes in the firstVisibleItemIndex
- * value so we do not regenerate the map on each scroll.
- *
- * @param firstVisibleItemIndex Provider of the first item index currently visible on screen.
- * @param slidingWindowSize Number of items between current and `firstVisibleItem` until
- * [LazyLayoutKeyIndexMap] is regenerated.
- * @param extraItemCount The minimum amount of items in one direction near the first visible item
- * to calculate mapping for.
- * @param content Provider of [LazyLayoutIntervalContent] to generate key index mapping for.
- */
-@ExperimentalFoundationApi
-internal class NearestRangeKeyIndexMapState(
- firstVisibleItemIndex: () -> Int,
- slidingWindowSize: () -> Int,
- extraItemCount: () -> Int,
- content: () -> LazyLayoutIntervalContent<*>
-) : State<LazyLayoutKeyIndexMap> {
- private val nearestRangeState by derivedStateOf(structuralEqualityPolicy()) {
- if (content().itemCount < extraItemCount() * 2 + slidingWindowSize()) {
- 0 until content().itemCount
- } else {
- calculateNearestItemsRange(
- firstVisibleItemIndex(),
- slidingWindowSize(),
- extraItemCount()
- )
- }
- }
-
- override val value: LazyLayoutKeyIndexMap by derivedStateOf(referentialEqualityPolicy()) {
- NearestRangeKeyIndexMap(nearestRangeState, content())
- }
-}
-
-/**
* Implementation of [LazyLayoutKeyIndexMap] indexing over given [IntRange] of items.
* Items outside of given range are considered unknown, with null returned as the index.
*/
@ExperimentalFoundationApi
-private class NearestRangeKeyIndexMap(
+internal class NearestRangeKeyIndexMap(
nearestRange: IntRange,
- content: LazyLayoutIntervalContent<*>
+ intervalContent: LazyLayoutIntervalContent<*>
) : LazyLayoutKeyIndexMap {
private val map: Map<Any, Int>
private val keys: Array<Any?>
private val keysStartIndex: Int
init {
- // Traverses the interval [list] in order to create a mapping from the key to the index for all
- // the indexes in the passed [range].
- // The returned map will not contain the values for intervals with no key mapping provided.
- val list = content.intervals
+ // Traverses the interval [list] in order to create a mapping from the key to the index for
+ // all the indexes in the passed [range].
+ val list = intervalContent.intervals
val first = nearestRange.first
check(first >= 0)
val last = minOf(nearestRange.last, list.size - 1)
@@ -113,31 +70,24 @@ private class NearestRangeKeyIndexMap(
keys = emptyArray()
keysStartIndex = 0
} else {
- var tmpKeys = emptyArray<Any?>()
- var tmpKeysStartIndex = 0
+ keys = arrayOfNulls<Any?>(last - first + 1)
+ keysStartIndex = first
map = hashMapOf<Any, Int>().also { map ->
list.forEach(
fromIndex = first,
toIndex = last,
) {
- if (it.value.key != null) {
- val keyFactory = requireNotNull(it.value.key)
- val start = maxOf(first, it.startIndex)
- if (tmpKeys.isEmpty()) {
- tmpKeysStartIndex = start
- tmpKeys = Array(last - start + 1) { null }
- }
- val end = minOf(last, it.startIndex + it.size - 1)
- for (i in start..end) {
- val key = keyFactory(i - it.startIndex)
- map[key] = i
- tmpKeys[i - tmpKeysStartIndex] = key
- }
+ val keyFactory = it.value.key
+ val start = maxOf(first, it.startIndex)
+ val end = minOf(last, it.startIndex + it.size - 1)
+ for (i in start..end) {
+ val key =
+ keyFactory?.invoke(i - it.startIndex) ?: getDefaultLazyLayoutKey(i)
+ map[key] = i
+ keys[i - keysStartIndex] = key
}
}
}
- keys = tmpKeys
- keysStartIndex = tmpKeysStartIndex
}
}
@@ -146,20 +96,3 @@ private class NearestRangeKeyIndexMap(
override fun getKey(index: Int) =
keys.getOrElse(index - keysStartIndex) { null }
}
-
-/**
- * Returns a range of indexes which contains at least [extraItemCount] items near
- * the first visible item. It is optimized to return the same range for small changes in the
- * firstVisibleItem value so we do not regenerate the map on each scroll.
- */
-private fun calculateNearestItemsRange(
- firstVisibleItem: Int,
- slidingWindowSize: Int,
- extraItemCount: Int
-): IntRange {
- val slidingWindowStart = slidingWindowSize * (firstVisibleItem / slidingWindowSize)
-
- val start = maxOf(slidingWindowStart - extraItemCount, 0)
- val end = slidingWindowStart + slidingWindowSize + extraItemCount
- return start until end
-} \ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasureScope.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasureScope.kt
index fad74679e0a..2a8bb4582b8 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasureScope.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutMeasureScope.kt
@@ -103,6 +103,8 @@ internal class LazyLayoutMeasureScopeImpl internal constructor(
private val timeTracker: LazyLayoutPrefetchState.AverageTimeTracker?
) : LazyLayoutMeasureScope, MeasureScope by subcomposeMeasureScope {
+ private val itemProvider = itemContentFactory.itemProvider()
+
/**
* A cache of the previously composed items. It allows us to support [get]
* re-executions with the same index during the same measure pass.
@@ -114,8 +116,9 @@ internal class LazyLayoutMeasureScopeImpl internal constructor(
return if (cachedPlaceable != null) {
cachedPlaceable
} else {
- val key = itemContentFactory.itemProvider().getKey(index)
- val itemContent = itemContentFactory.getContent(index, key)
+ val key = itemProvider.getKey(index)
+ val contentType = itemProvider.getContentType(index)
+ val itemContent = itemContentFactory.getContent(index, key, contentType)
val measurables = trackComposition {
subcomposeMeasureScope.subcompose(key, itemContent)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutNearestRangeState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutNearestRangeState.kt
new file mode 100644
index 00000000000..69766ea67ca
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutNearestRangeState.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.foundation.lazy.layout
+
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.structuralEqualityPolicy
+
+internal class LazyLayoutNearestRangeState(
+ firstVisibleItem: Int,
+ private val slidingWindowSize: Int,
+ private val extraItemCount: Int
+) : State<IntRange> {
+
+ override var value: IntRange by mutableStateOf(
+ calculateNearestItemsRange(firstVisibleItem, slidingWindowSize, extraItemCount),
+ structuralEqualityPolicy()
+ )
+ private set
+
+ private var lastFirstVisibleItem = firstVisibleItem
+
+ fun update(firstVisibleItem: Int) {
+ if (firstVisibleItem != lastFirstVisibleItem) {
+ lastFirstVisibleItem = firstVisibleItem
+ value = calculateNearestItemsRange(firstVisibleItem, slidingWindowSize, extraItemCount)
+ }
+ }
+
+ private companion object {
+ /**
+ * Returns a range of indexes which contains at least [extraItemCount] items near
+ * the first visible item. It is optimized to return the same range for small changes in the
+ * firstVisibleItem value so we do not regenerate the map on each scroll.
+ */
+ private fun calculateNearestItemsRange(
+ firstVisibleItem: Int,
+ slidingWindowSize: Int,
+ extraItemCount: Int
+ ): IntRange {
+ val slidingWindowStart = slidingWindowSize * (firstVisibleItem / slidingWindowSize)
+
+ val start = maxOf(slidingWindowStart - extraItemCount, 0)
+ val end = slidingWindowStart + slidingWindowSize + extraItemCount
+ return start until end
+ }
+ }
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt
index 9a470179d5f..1184ff7a798 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt
@@ -18,9 +18,6 @@ package androidx.compose.foundation.lazy.layout
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Stable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
import androidx.compose.ui.unit.Constraints
/**
@@ -29,7 +26,7 @@ import androidx.compose.ui.unit.Constraints
@ExperimentalFoundationApi
@Stable
class LazyLayoutPrefetchState {
- internal var prefetcher: Prefetcher? by mutableStateOf(null)
+ internal var prefetcher: Prefetcher? = null
/**
* Schedules precomposition and premeasure for the new item.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
index a81ca87c4e2..b7e1ca9dbb6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutSemantics.kt
@@ -39,7 +39,7 @@ import kotlinx.coroutines.launch
@Suppress("ComposableModifierFactory")
@Composable
internal fun Modifier.lazyLayoutSemantics(
- itemProvider: LazyLayoutItemProvider,
+ itemProviderLambda: () -> LazyLayoutItemProvider,
state: LazyLayoutSemanticState,
orientation: Orientation,
userScrollEnabled: Boolean,
@@ -48,13 +48,14 @@ internal fun Modifier.lazyLayoutSemantics(
val coroutineScope = rememberCoroutineScope()
return this.then(
remember(
- itemProvider,
+ itemProviderLambda,
state,
orientation,
userScrollEnabled
) {
val isVertical = orientation == Orientation.Vertical
val indexForKeyMapping: (Any) -> Int = { needle ->
+ val itemProvider = itemProviderLambda()
var result = -1
for (index in 0 until itemProvider.itemCount) {
if (itemProvider.getKey(index) == needle) {
@@ -74,6 +75,7 @@ internal fun Modifier.lazyLayoutSemantics(
state.currentPosition
},
maxValue = {
+ val itemProvider = itemProviderLambda()
if (state.canScrollForward) {
// If we can scroll further, we don't know the end yet,
// but it's upper bounded by #items + 1
@@ -105,6 +107,7 @@ internal fun Modifier.lazyLayoutSemantics(
val scrollToIndexAction: ((Int) -> Boolean)? = if (userScrollEnabled) {
{ index ->
+ val itemProvider = itemProviderLambda()
require(index >= 0 && index < itemProvider.itemCount) {
"Can't scroll to index $index, it is out of " +
"bounds [0, ${itemProvider.itemCount})"
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt
index 79423ba3422..50ae912f34b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGrid.kt
@@ -63,10 +63,10 @@ internal fun LazyStaggeredGrid(
) {
val overscrollEffect = ScrollableDefaults.overscrollEffect()
- val itemProvider = rememberStaggeredGridItemProvider(state, content)
+ val itemProviderLambda = rememberStaggeredGridItemProviderLambda(state, content)
val measurePolicy = rememberStaggeredGridMeasurePolicy(
state,
- itemProvider,
+ itemProviderLambda,
contentPadding,
reverseLayout,
orientation,
@@ -76,14 +76,14 @@ internal fun LazyStaggeredGrid(
)
val semanticState = rememberLazyStaggeredGridSemanticState(state, reverseLayout)
- ScrollPositionUpdater(itemProvider, state)
+ ScrollPositionUpdater(itemProviderLambda, state)
LazyLayout(
modifier = modifier
.then(state.remeasurementModifier)
.then(state.awaitLayoutModifier)
.lazyLayoutSemantics(
- itemProvider = itemProvider,
+ itemProviderLambda = itemProviderLambda,
state = semanticState,
orientation = orientation,
userScrollEnabled = userScrollEnabled,
@@ -110,7 +110,7 @@ internal fun LazyStaggeredGrid(
enabled = userScrollEnabled
),
prefetchState = state.prefetchState,
- itemProvider = itemProvider,
+ itemProvider = itemProviderLambda,
measurePolicy = measurePolicy
)
}
@@ -119,9 +119,10 @@ internal fun LazyStaggeredGrid(
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ScrollPositionUpdater(
- itemProvider: LazyLayoutItemProvider,
+ itemProviderLambda: () -> LazyLayoutItemProvider,
state: LazyStaggeredGridState
) {
+ val itemProvider = itemProviderLambda()
if (itemProvider.itemCount > 0) {
state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemPlacementAnimator.kt
index f9dec0d82b5..4c368126f1c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemPlacementAnimator.kt
@@ -41,8 +41,8 @@ internal class LazyStaggeredGridItemPlacementAnimator {
// stored to not allocate it every pass.
private val movingAwayKeys = LinkedHashSet<Any>()
- private val movingInFromStartBound = mutableListOf<LazyStaggeredGridPositionedItem>()
- private val movingInFromEndBound = mutableListOf<LazyStaggeredGridPositionedItem>()
+ private val movingInFromStartBound = mutableListOf<LazyStaggeredGridMeasuredItem>()
+ private val movingInFromEndBound = mutableListOf<LazyStaggeredGridMeasuredItem>()
private val movingAwayToStartBound = mutableListOf<LazyStaggeredGridMeasuredItem>()
private val movingAwayToEndBound = mutableListOf<LazyStaggeredGridMeasuredItem>()
@@ -55,7 +55,7 @@ internal class LazyStaggeredGridItemPlacementAnimator {
consumedScroll: Int,
layoutWidth: Int,
layoutHeight: Int,
- positionedItems: MutableList<LazyStaggeredGridPositionedItem>,
+ positionedItems: MutableList<LazyStaggeredGridMeasuredItem>,
itemProvider: LazyStaggeredGridMeasureProvider,
isVertical: Boolean,
laneCount: Int
@@ -185,10 +185,9 @@ internal class LazyStaggeredGridItemPlacementAnimator {
val mainAxisOffset = 0 - accumulatedOffsetPerLane[item.lane]
val itemInfo = keyToItemInfoMap.getValue(item.key)
- val positionedItem =
- item.position(mainAxisOffset, itemInfo.crossAxisOffset, mainAxisLayoutSize)
- positionedItems.add(positionedItem)
- startAnimationsIfNeeded(positionedItem)
+ item.position(mainAxisOffset, itemInfo.crossAxisOffset, mainAxisLayoutSize)
+ positionedItems.add(item)
+ startAnimationsIfNeeded(item)
}
accumulatedOffsetPerLane.fill(0)
}
@@ -199,10 +198,9 @@ internal class LazyStaggeredGridItemPlacementAnimator {
accumulatedOffsetPerLane[item.lane] += item.mainAxisSize
val itemInfo = keyToItemInfoMap.getValue(item.key)
- val positionedItem =
- item.position(mainAxisOffset, itemInfo.crossAxisOffset, mainAxisLayoutSize)
- positionedItems.add(positionedItem)
- startAnimationsIfNeeded(positionedItem)
+ item.position(mainAxisOffset, itemInfo.crossAxisOffset, mainAxisLayoutSize)
+ positionedItems.add(item)
+ startAnimationsIfNeeded(item)
}
}
@@ -224,7 +222,7 @@ internal class LazyStaggeredGridItemPlacementAnimator {
}
private fun initializeNode(
- item: LazyStaggeredGridPositionedItem,
+ item: LazyStaggeredGridMeasuredItem,
mainAxisOffset: Int
) {
val firstPlaceableOffset = item.offset
@@ -243,7 +241,7 @@ internal class LazyStaggeredGridItemPlacementAnimator {
}
}
- private fun startAnimationsIfNeeded(item: LazyStaggeredGridPositionedItem) {
+ private fun startAnimationsIfNeeded(item: LazyStaggeredGridMeasuredItem) {
item.forEachNode { node ->
val newTarget = item.offset
val currentTarget = node.rawOffset
@@ -258,13 +256,13 @@ internal class LazyStaggeredGridItemPlacementAnimator {
private val Any?.node get() = this as? LazyLayoutAnimateItemModifierNode
- private val LazyStaggeredGridPositionedItem.hasAnimations: Boolean
+ private val LazyStaggeredGridMeasuredItem.hasAnimations: Boolean
get() {
forEachNode { return true }
return false
}
- private inline fun LazyStaggeredGridPositionedItem.forEachNode(
+ private inline fun LazyStaggeredGridMeasuredItem.forEachNode(
block: (LazyLayoutAnimateItemModifierNode) -> Unit
) {
repeat(placeablesCount) { index ->
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt
index b76423c5317..c1569e316bd 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridItemProvider.kt
@@ -20,10 +20,9 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap
import androidx.compose.foundation.lazy.layout.LazyLayoutPinnableItem
-import androidx.compose.foundation.lazy.layout.NearestRangeKeyIndexMapState
+import androidx.compose.foundation.lazy.layout.NearestRangeKeyIndexMap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
import androidx.compose.runtime.referentialEqualityPolicy
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
@@ -34,53 +33,67 @@ internal interface LazyStaggeredGridItemProvider : LazyLayoutItemProvider {
val keyIndexMap: LazyLayoutKeyIndexMap
}
+@OptIn(ExperimentalFoundationApi::class)
@Composable
-internal fun rememberStaggeredGridItemProvider(
+internal fun rememberStaggeredGridItemProviderLambda(
state: LazyStaggeredGridState,
content: LazyStaggeredGridScope.() -> Unit,
-): LazyStaggeredGridItemProvider {
+): () -> LazyStaggeredGridItemProvider {
val latestContent = rememberUpdatedState(content)
return remember(state) {
- LazyStaggeredGridItemProviderImpl(
- state,
- { latestContent.value },
- )
+ val intervalContentState = derivedStateOf(referentialEqualityPolicy()) {
+ LazyStaggeredGridIntervalContent(latestContent.value)
+ }
+ val itemProviderState = derivedStateOf(referentialEqualityPolicy()) {
+ val intervalContent = intervalContentState.value
+ val map = NearestRangeKeyIndexMap(state.nearestRange, intervalContent)
+ LazyStaggeredGridItemProviderImpl(
+ state = state,
+ intervalContent = intervalContent,
+ keyIndexMap = map
+ )
+ }
+ itemProviderState::value
}
}
@OptIn(ExperimentalFoundationApi::class)
private class LazyStaggeredGridItemProviderImpl(
private val state: LazyStaggeredGridState,
- private val latestContent: () -> (LazyStaggeredGridScope.() -> Unit)
+ private val intervalContent: LazyStaggeredGridIntervalContent,
+ override val keyIndexMap: LazyLayoutKeyIndexMap,
) : LazyStaggeredGridItemProvider {
- private val staggeredGridContent by derivedStateOf(referentialEqualityPolicy()) {
- LazyStaggeredGridIntervalContent(latestContent())
- }
-
- override val keyIndexMap: LazyLayoutKeyIndexMap by NearestRangeKeyIndexMapState(
- firstVisibleItemIndex = { state.firstVisibleItemIndex },
- slidingWindowSize = { 90 },
- extraItemCount = { 200 },
- content = { staggeredGridContent }
- )
- override val itemCount: Int get() = staggeredGridContent.itemCount
+ override val itemCount: Int get() = intervalContent.itemCount
override fun getKey(index: Int): Any =
- keyIndexMap.getKey(index) ?: staggeredGridContent.getKey(index)
+ keyIndexMap.getKey(index) ?: intervalContent.getKey(index)
override fun getIndex(key: Any): Int = keyIndexMap.getIndex(key)
- override fun getContentType(index: Int): Any? = staggeredGridContent.getContentType(index)
+ override fun getContentType(index: Int): Any? = intervalContent.getContentType(index)
@Composable
override fun Item(index: Int, key: Any) {
LazyLayoutPinnableItem(key, index, state.pinnedItems) {
- staggeredGridContent.withInterval(index) { localIndex, content ->
+ intervalContent.withInterval(index) { localIndex, content ->
content.item(LazyStaggeredGridItemScopeImpl, localIndex)
}
}
}
- override val spanProvider get() = staggeredGridContent.spanProvider
+ override val spanProvider get() = intervalContent.spanProvider
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is LazyStaggeredGridItemProviderImpl) return false
+
+ // the identity of this class is represented by intervalContent object.
+ // having equals() allows us to skip items recomposition when intervalContent didn't change
+ return intervalContent == other.intervalContent
+ }
+
+ override fun hashCode(): Int {
+ return intervalContent.hashCode()
+ }
} \ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index 460d75afff0..51ef3b9e5a0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -109,7 +109,12 @@ internal fun LazyLayoutMeasureScope.measureStaggeredGrid(
val initialItemOffsets: IntArray
Snapshot.withoutReadObservation {
- val firstVisibleIndices = state.scrollPosition.indices
+ // ensure scroll position is up to date
+ val firstVisibleIndices =
+ state.updateScrollPositionIfTheFirstItemWasMoved(
+ itemProvider,
+ state.scrollPosition.indices
+ )
val firstVisibleOffsets = state.scrollPosition.offsets
initialItemIndices =
@@ -181,13 +186,20 @@ internal class LazyStaggeredGridMeasureContext(
val reverseLayout: Boolean,
val mainAxisSpacing: Int,
) {
- val measuredItemProvider = LazyStaggeredGridMeasureProvider(
+ val measuredItemProvider = object : LazyStaggeredGridMeasureProvider(
isVertical = isVertical,
itemProvider = itemProvider,
measureScope = measureScope,
resolvedSlots = resolvedSlots,
- ) { index, lane, span, key, contentType, placeables ->
- LazyStaggeredGridMeasuredItem(
+ ) {
+ override fun createItem(
+ index: Int,
+ lane: Int,
+ span: Int,
+ key: Any,
+ contentType: Any?,
+ placeables: List<Placeable>
+ ) = LazyStaggeredGridMeasuredItem(
index = index,
key = key,
placeables = placeables,
@@ -751,13 +763,12 @@ private fun LazyStaggeredGridMeasureContext.measure(
extraItemOffset = itemScrollOffsets[0]
val extraItemsAfter = calculateExtraItems(
position = {
- val positionedItem = it.position(
+ it.position(
mainAxis = extraItemOffset,
crossAxis = 0,
mainAxisLayoutSize = mainAxisLayoutSize
)
extraItemOffset += it.sizeWithSpacings
- positionedItem
},
filter = { itemIndex ->
if (itemIndex >= itemCount) {
@@ -775,7 +786,7 @@ private fun LazyStaggeredGridMeasureContext.measure(
}
)
- val positionedItems = mutableListOf<LazyStaggeredGridPositionedItem>()
+ val positionedItems = mutableListOf<LazyStaggeredGridMeasuredItem>()
positionedItems.addAll(extraItemsBefore)
positionedItems.addAll(visibleItems)
positionedItems.addAll(extraItemsAfter)
@@ -830,8 +841,8 @@ private fun LazyStaggeredGridMeasureContext.calculateVisibleItems(
measuredItems: Array<ArrayDeque<LazyStaggeredGridMeasuredItem>>,
itemScrollOffsets: IntArray,
mainAxisLayoutSize: Int,
-): List<LazyStaggeredGridPositionedItem> {
- val positionedItems = ArrayList<LazyStaggeredGridPositionedItem>(
+): List<LazyStaggeredGridMeasuredItem> {
+ val positionedItems = ArrayList<LazyStaggeredGridMeasuredItem>(
measuredItems.sumOf { it.size }
)
while (measuredItems.any { it.isNotEmpty() }) {
@@ -853,14 +864,12 @@ private fun LazyStaggeredGridMeasureContext.calculateVisibleItems(
// nothing to place, ignore spacings
continue
}
-
- positionedItems +=
- item.position(
- mainAxis = mainAxisOffset,
- crossAxis = crossAxisOffset,
- mainAxisLayoutSize = mainAxisLayoutSize,
- lane = laneIndex
- )
+ item.position(
+ mainAxis = mainAxisOffset,
+ crossAxis = crossAxisOffset,
+ mainAxisLayoutSize = mainAxisLayoutSize,
+ )
+ positionedItems += item
spanRange.forEach { lane ->
itemScrollOffsets[lane] = mainAxisOffset + item.sizeWithSpacings
}
@@ -870,10 +879,10 @@ private fun LazyStaggeredGridMeasureContext.calculateVisibleItems(
@ExperimentalFoundationApi
private inline fun LazyStaggeredGridMeasureContext.calculateExtraItems(
- position: (LazyStaggeredGridMeasuredItem) -> LazyStaggeredGridPositionedItem,
+ position: (LazyStaggeredGridMeasuredItem) -> Unit,
filter: (itemIndex: Int) -> Boolean
-): List<LazyStaggeredGridPositionedItem> {
- var result: MutableList<LazyStaggeredGridPositionedItem>? = null
+): List<LazyStaggeredGridMeasuredItem> {
+ var result: MutableList<LazyStaggeredGridMeasuredItem>? = null
pinnedItems.fastForEach { index ->
if (filter(index)) {
@@ -882,7 +891,8 @@ private inline fun LazyStaggeredGridMeasureContext.calculateExtraItems(
result = mutableListOf()
}
val measuredItem = measuredItemProvider.getAndMeasure(index, spanRange)
- result?.add(position(measuredItem))
+ position(measuredItem)
+ result?.add(measuredItem)
}
}
@@ -987,12 +997,11 @@ private fun LazyStaggeredGridMeasureContext.findPreviousItemIndex(item: Int, lan
laneInfo.findPreviousItemIndex(item, lane)
@OptIn(ExperimentalFoundationApi::class)
-internal class LazyStaggeredGridMeasureProvider(
+internal abstract class LazyStaggeredGridMeasureProvider(
private val isVertical: Boolean,
private val itemProvider: LazyStaggeredGridItemProvider,
private val measureScope: LazyLayoutMeasureScope,
- private val resolvedSlots: LazyStaggeredGridSlots,
- private val measuredItemFactory: MeasuredItemFactory,
+ private val resolvedSlots: LazyStaggeredGridSlots
) {
private fun childConstraints(requestedSlot: Int, requestedSpan: Int): Constraints {
val slotCount = resolvedSlots.sizes.size
@@ -1017,10 +1026,10 @@ internal class LazyStaggeredGridMeasureProvider(
}
fun getAndMeasure(index: Int, span: SpanRange): LazyStaggeredGridMeasuredItem {
- val key = keyIndexMap.getKey(index) ?: itemProvider.getKey(index)
+ val key = itemProvider.getKey(index)
val contentType = itemProvider.getContentType(index)
val placeables = measureScope.measure(index, childConstraints(span.start, span.size))
- return measuredItemFactory.createItem(
+ return createItem(
index,
span.start,
span.size,
@@ -1030,12 +1039,9 @@ internal class LazyStaggeredGridMeasureProvider(
)
}
- val keyIndexMap: LazyLayoutKeyIndexMap = itemProvider.keyIndexMap
-}
+ val keyIndexMap: LazyLayoutKeyIndexMap get() = itemProvider.keyIndexMap
-// This interface allows to avoid autoboxing on index param
-internal fun interface MeasuredItemFactory {
- fun createItem(
+ abstract fun createItem(
index: Int,
lane: Int,
span: Int,
@@ -1046,17 +1052,17 @@ internal fun interface MeasuredItemFactory {
}
internal class LazyStaggeredGridMeasuredItem(
- val index: Int,
- val key: Any,
+ override val index: Int,
+ override val key: Any,
private val placeables: List<Placeable>,
- private val isVertical: Boolean,
+ val isVertical: Boolean,
spacing: Int,
- val lane: Int,
+ override val lane: Int,
val span: Int,
private val beforeContentPadding: Int,
private val afterContentPadding: Int,
- private val contentType: Any?
-) {
+ override val contentType: Any?
+) : LazyStaggeredGridItemInfo {
var isVisible = true
val placeablesCount: Int get() = placeables.size
@@ -1073,63 +1079,40 @@ internal class LazyStaggeredGridMeasuredItem(
if (isVertical) it.width else it.height
} ?: 0
+ private var mainAxisLayoutSize: Int = Unset
+ private var minMainAxisOffset: Int = 0
+ private var maxMainAxisOffset: Int = 0
+
+ override val size: IntSize = if (isVertical) {
+ IntSize(crossAxisSize, mainAxisSize)
+ } else {
+ IntSize(mainAxisSize, crossAxisSize)
+ }
+ override var offset: IntOffset = IntOffset.Zero
+ private set
+
fun position(
mainAxis: Int,
crossAxis: Int,
mainAxisLayoutSize: Int,
- lane: Int = 0
- ): LazyStaggeredGridPositionedItem =
- LazyStaggeredGridPositionedItem(
- offset = if (isVertical) {
- IntOffset(crossAxis, mainAxis)
- } else {
- IntOffset(mainAxis, crossAxis)
- },
- lane = lane,
- index = index,
- key = key,
- size = if (isVertical) {
- IntSize(crossAxisSize, mainAxisSize)
- } else {
- IntSize(mainAxisSize, crossAxisSize)
- },
- placeables = placeables,
- isVertical = isVertical,
- mainAxisLayoutSize = mainAxisLayoutSize,
- minMainAxisOffset = -beforeContentPadding,
- maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding,
- span = span,
- contentType = contentType
- )
-}
-
-internal class LazyStaggeredGridPositionedItem(
- override val offset: IntOffset,
- override val index: Int,
- override val lane: Int,
- override val key: Any,
- override val size: IntSize,
- private val placeables: List<Placeable>,
- val isVertical: Boolean,
- private val mainAxisLayoutSize: Int,
- private val minMainAxisOffset: Int,
- private val maxMainAxisOffset: Int,
- val span: Int,
- override val contentType: Any?
-) : LazyStaggeredGridItemInfo {
-
- val placeablesCount: Int get() = placeables.size
-
- val mainAxisSize get() = if (isVertical) size.height else size.width
+ ) {
+ this.mainAxisLayoutSize = mainAxisLayoutSize
+ minMainAxisOffset = -beforeContentPadding
+ maxMainAxisOffset = mainAxisLayoutSize + afterContentPadding
+ offset = if (isVertical) {
+ IntOffset(crossAxis, mainAxis)
+ } else {
+ IntOffset(mainAxis, crossAxis)
+ }
+ }
val crossAxisOffset get() = if (isVertical) offset.x else offset.y
- fun getParentData(index: Int) = placeables[index].parentData
-
fun place(
scope: Placeable.PlacementScope,
context: LazyStaggeredGridMeasureContext
) = with(context) {
+ require(mainAxisLayoutSize != Unset) { "position() should be called first" }
with(scope) {
placeables.fastForEachIndexed { index, placeable ->
val minOffset = minMainAxisOffset - placeable.mainAxisSize
@@ -1170,3 +1153,5 @@ internal class LazyStaggeredGridPositionedItem(
super.toString()
}
}
+
+private const val Unset = Int.MIN_VALUE
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
index c72c1145bcb..2ef1a74feb4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasurePolicy.kt
@@ -38,7 +38,7 @@ import androidx.compose.ui.unit.constrainWidth
@Composable
internal fun rememberStaggeredGridMeasurePolicy(
state: LazyStaggeredGridState,
- itemProvider: LazyStaggeredGridItemProvider,
+ itemProviderLambda: () -> LazyStaggeredGridItemProvider,
contentPadding: PaddingValues,
reverseLayout: Boolean,
orientation: Orientation,
@@ -47,7 +47,7 @@ internal fun rememberStaggeredGridMeasurePolicy(
slots: Density.(Constraints) -> LazyStaggeredGridSlots
): LazyLayoutMeasureScope.(Constraints) -> LazyStaggeredGridMeasureResult = remember(
state,
- itemProvider,
+ itemProviderLambda,
contentPadding,
reverseLayout,
orientation,
@@ -62,15 +62,13 @@ internal fun rememberStaggeredGridMeasurePolicy(
)
val resolvedSlots = slots(this, constraints)
val isVertical = orientation == Orientation.Vertical
+ val itemProvider = itemProviderLambda()
// setup information for prefetch
state.slots = resolvedSlots
state.isVertical = isVertical
state.spanProvider = itemProvider.spanProvider
- // ensure scroll position is up to date
- state.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
-
// setup measure
val beforeContentPadding = contentPadding.beforePadding(
orientation, reverseLayout, layoutDirection
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt
index 5d5fe3306f4..7f8809d8d48 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollPosition.kt
@@ -18,7 +18,9 @@ package androidx.compose.foundation.lazy.staggeredgrid
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.LazyLayoutNearestRangeState
import androidx.compose.foundation.lazy.layout.findIndexByKey
+import androidx.compose.runtime.SnapshotMutationPolicy
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
@@ -30,15 +32,23 @@ internal class LazyStaggeredGridScrollPosition(
initialIndices: IntArray,
initialOffsets: IntArray,
private val fillIndices: (targetIndex: Int, laneCount: Int) -> IntArray
-) {
- var indices by mutableStateOf(initialIndices)
- var offsets by mutableStateOf(initialOffsets)
+) : SnapshotMutationPolicy<IntArray> {
+ var indices by mutableStateOf(initialIndices, this)
+ private set
+ var offsets by mutableStateOf(initialOffsets, this)
+ private set
private var hadFirstNotEmptyLayout = false
/** The last know key of the item at lowest of [indices] position. */
private var lastKnownFirstItemKey: Any? = null
+ val nearestRangeState = LazyLayoutNearestRangeState(
+ initialIndices.minOrNull() ?: 0,
+ NearestItemsSlidingWindowSize,
+ NearestItemsExtraItemCount
+ )
+
/**
* Updates the current scroll position based on the results of the last measurement.
*/
@@ -50,6 +60,7 @@ internal class LazyStaggeredGridScrollPosition(
lastKnownFirstItemKey = measureResult.visibleItemsInfo
.fastFirstOrNull { it.index == firstVisibleIndex }
?.key
+ nearestRangeState.update(firstVisibleIndex)
// we ignore the index and offset from measureResult until we get at least one
// measurement with real items. otherwise the initial index and scroll passed to the
// state would be lost and overridden with zeros.
@@ -79,6 +90,7 @@ internal class LazyStaggeredGridScrollPosition(
val newIndices = fillIndices(index, indices.size)
val newOffsets = IntArray(newIndices.size) { scrollOffset }
update(newIndices, newOffsets)
+ nearestRangeState.update(index)
// clear the stored key as we have a direct request to scroll to [index] position and the
// next [updateScrollPositionIfTheFirstItemWasMoved] shouldn't override this.
lastKnownFirstItemKey = null
@@ -91,27 +103,40 @@ internal class LazyStaggeredGridScrollPosition(
* as the first visible one even given that its index has been changed.
*/
@ExperimentalFoundationApi
- fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyLayoutItemProvider) {
- Snapshot.withoutReadObservation {
- val lastIndex = itemProvider.findIndexByKey(
- key = lastKnownFirstItemKey,
- lastKnownIndex = indices.getOrNull(0) ?: 0
- )
- if (lastIndex !in indices) {
- update(
- fillIndices(lastIndex, indices.size),
- offsets
- )
- }
+ fun updateScrollPositionIfTheFirstItemWasMoved(
+ itemProvider: LazyLayoutItemProvider,
+ indices: IntArray
+ ): IntArray {
+ val newIndex = itemProvider.findIndexByKey(
+ key = lastKnownFirstItemKey,
+ lastKnownIndex = indices.getOrNull(0) ?: 0
+ )
+ return if (newIndex !in indices) {
+ nearestRangeState.update(newIndex)
+ val newIndices = fillIndices(newIndex, indices.size)
+ this.indices = newIndices
+ newIndices
+ } else {
+ indices
}
}
private fun update(indices: IntArray, offsets: IntArray) {
- if (!indices.contentEquals(this.indices)) {
- this.indices = indices
- }
- if (!offsets.contentEquals(this.offsets)) {
- this.offsets = offsets
- }
+ this.indices = indices
+ this.offsets = offsets
}
-} \ No newline at end of file
+
+ // mutation policy for int arrays
+ override fun equivalent(a: IntArray, b: IntArray) = a.contentEquals(b)
+}
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+private const val NearestItemsSlidingWindowSize = 90
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+private const val NearestItemsExtraItemCount = 200
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
index 84f9df6a64c..f37ea64496b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
@@ -39,6 +39,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.layout.Remeasurement
import androidx.compose.ui.layout.RemeasurementModifier
@@ -223,6 +224,8 @@ class LazyStaggeredGridState private constructor(
internal val placementAnimator = LazyStaggeredGridItemPlacementAnimator()
+ internal val nearestRange: IntRange by scrollPosition.nearestRangeState
+
/**
* Call this function to take control of scrolling and gain the ability to send scroll events
* via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be
@@ -331,9 +334,11 @@ class LazyStaggeredGridState private constructor(
/**
* Maintain scroll position for item based on custom key if its index has changed.
*/
- internal fun updateScrollPositionIfTheFirstItemWasMoved(itemProvider: LazyLayoutItemProvider) {
- scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider)
- }
+ internal fun updateScrollPositionIfTheFirstItemWasMoved(
+ itemProvider: LazyLayoutItemProvider,
+ firstItemIndex: IntArray = Snapshot.withoutReadObservation { scrollPosition.indices }
+ ): IntArray =
+ scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex)
override fun dispatchRawDelta(delta: Float): Float =
scrollableState.dispatchRawDelta(delta)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
index d42eaed5b47..9c4c19b6d62 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/LazyLayoutPager.kt
@@ -25,8 +25,6 @@ import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.lazy.NearestItemsExtraItemCount
-import androidx.compose.foundation.lazy.NearestItemsSlidingWindowSize
import androidx.compose.foundation.lazy.layout.IntervalList
import androidx.compose.foundation.lazy.layout.LazyLayout
import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
@@ -34,15 +32,14 @@ import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap
import androidx.compose.foundation.lazy.layout.LazyLayoutPinnableItem
import androidx.compose.foundation.lazy.layout.MutableIntervalList
-import androidx.compose.foundation.lazy.layout.NearestRangeKeyIndexMapState
+import androidx.compose.foundation.lazy.layout.NearestRangeKeyIndexMap
import androidx.compose.foundation.lazy.layout.lazyLayoutSemantics
import androidx.compose.foundation.overscroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.getValue
+import androidx.compose.runtime.referentialEqualityPolicy
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.structuralEqualityPolicy
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
@@ -98,7 +95,7 @@ internal fun Pager(
val overscrollEffect = ScrollableDefaults.overscrollEffect()
- val pagerItemProvider = rememberPagerItemProvider(
+ val pagerItemProvider = rememberPagerItemProviderLambda(
state = state,
pageContent = pageContent,
key = key
@@ -114,7 +111,7 @@ internal fun Pager(
pageSize = pageSize,
horizontalAlignment = horizontalAlignment,
verticalAlignment = verticalAlignment,
- itemProvider = pagerItemProvider,
+ itemProviderLambda = pagerItemProvider,
pageCount = { state.pageCount },
)
@@ -130,7 +127,6 @@ internal fun Pager(
val semanticState = rememberPagerSemanticState(
state,
- pagerItemProvider,
reverseLayout,
orientation == Orientation.Vertical
)
@@ -141,7 +137,7 @@ internal fun Pager(
.then(state.awaitLayoutModifier)
.then(pagerSemantics)
.lazyLayoutSemantics(
- itemProvider = pagerItemProvider,
+ itemProviderLambda = pagerItemProvider,
state = semanticState,
orientation = orientation,
userScrollEnabled = userScrollEnabled,
@@ -178,38 +174,42 @@ internal fun Pager(
@ExperimentalFoundationApi
internal class PagerLazyLayoutItemProvider(
- val state: PagerState,
- latestContent: () -> (@Composable PagerScope.(page: Int) -> Unit),
- key: ((index: Int) -> Any)?,
- pageCount: () -> Int
+ private val state: PagerState,
+ private val intervalContent: LazyLayoutIntervalContent<PagerIntervalContent>,
+ private val keyIndexMap: LazyLayoutKeyIndexMap,
) : LazyLayoutItemProvider {
- private val pagerContent by derivedStateOf(structuralEqualityPolicy()) {
- PagerLayoutIntervalContent(latestContent(), key = key, pageCount = pageCount())
- }
- private val keyToIndexMap: LazyLayoutKeyIndexMap by NearestRangeKeyIndexMapState(
- firstVisibleItemIndex = { state.firstVisiblePage },
- slidingWindowSize = { NearestItemsSlidingWindowSize },
- extraItemCount = { NearestItemsExtraItemCount },
- content = { pagerContent }
- )
private val pagerScopeImpl = PagerScopeImpl
override val itemCount: Int
- get() = pagerContent.itemCount
+ get() = intervalContent.itemCount
@Composable
override fun Item(index: Int, key: Any) {
LazyLayoutPinnableItem(key, index, state.pinnedPages) {
- pagerContent.withInterval(index) { localIndex, content ->
+ intervalContent.withInterval(index) { localIndex, content ->
content.item(pagerScopeImpl, localIndex)
}
}
}
- override fun getKey(index: Int): Any = keyToIndexMap.getKey(index) ?: pagerContent.getKey(index)
+ override fun getKey(index: Int): Any =
+ keyIndexMap.getKey(index) ?: intervalContent.getKey(index)
- override fun getIndex(key: Any): Int = keyToIndexMap.getIndex(key)
+ override fun getIndex(key: Any): Int = keyIndexMap.getIndex(key)
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is PagerLazyLayoutItemProvider) return false
+
+ // the identity of this class is represented by intervalContent object.
+ // having equals() allows us to skip items recomposition when intervalContent didn't change
+ return intervalContent == other.intervalContent
+ }
+
+ override fun hashCode(): Int {
+ return intervalContent.hashCode()
+ }
}
@OptIn(ExperimentalFoundationApi::class)
@@ -232,20 +232,27 @@ internal class PagerIntervalContent(
@OptIn(ExperimentalFoundationApi::class)
@Composable
-private fun rememberPagerItemProvider(
+private fun rememberPagerItemProviderLambda(
state: PagerState,
pageContent: @Composable PagerScope.(page: Int) -> Unit,
key: ((index: Int) -> Any)?,
pageCount: () -> Int
-): PagerLazyLayoutItemProvider {
+): () -> PagerLazyLayoutItemProvider {
val latestContent = rememberUpdatedState(pageContent)
return remember(state, latestContent, key, pageCount) {
- PagerLazyLayoutItemProvider(
- state = state,
- latestContent = { latestContent.value },
- key = key,
- pageCount = pageCount
- )
+ val intervalContentState = derivedStateOf(referentialEqualityPolicy()) {
+ PagerLayoutIntervalContent(latestContent.value, key, pageCount())
+ }
+ val itemProviderState = derivedStateOf(referentialEqualityPolicy()) {
+ val intervalContent = intervalContentState.value
+ val map = NearestRangeKeyIndexMap(state.nearestRange, intervalContent)
+ PagerLazyLayoutItemProvider(
+ state = state,
+ intervalContent = intervalContent,
+ keyIndexMap = map
+ )
+ }
+ itemProviderState::value
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/MeasuredPage.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/MeasuredPage.kt
index 8543acdf57f..8ad0a3ec072 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/MeasuredPage.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/MeasuredPage.kt
@@ -16,86 +16,103 @@
package androidx.compose.foundation.pager
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.ui.Alignment
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachIndexed
+@OptIn(ExperimentalFoundationApi::class)
internal class MeasuredPage(
- val index: Int,
+ override val index: Int,
val size: Int,
- val placeables: List<Placeable>,
- val visualOffset: IntOffset,
+ private val placeables: List<Placeable>,
+ private val visualOffset: IntOffset,
val key: Any,
- val orientation: Orientation,
- val horizontalAlignment: Alignment.Horizontal?,
- val verticalAlignment: Alignment.Vertical?,
- val layoutDirection: LayoutDirection,
- val reverseLayout: Boolean,
- val beforeContentPadding: Int,
- val afterContentPadding: Int,
-) {
+ orientation: Orientation,
+ private val horizontalAlignment: Alignment.Horizontal?,
+ private val verticalAlignment: Alignment.Vertical?,
+ private val layoutDirection: LayoutDirection,
+ private val reverseLayout: Boolean
+) : PageInfo {
+
+ private val isVertical = orientation == Orientation.Vertical
val crossAxisSize: Int
+ // optimized for storing x and y offsets for each placeable one by one.
+ // array's size == placeables.size * 2, first we store x, then y.
+ private val placeableOffsets: IntArray
+
init {
var maxCrossAxis = 0
placeables.fastForEach {
maxCrossAxis = maxOf(
maxCrossAxis,
- if (orientation != Orientation.Vertical) it.height else it.width
+ if (!isVertical) it.height else it.width
)
}
crossAxisSize = maxCrossAxis
+ placeableOffsets = IntArray(placeables.size * 2)
}
+ override var offset: Int = 0
+ private set
+
+ private var mainAxisLayoutSize: Int = Unset
+
fun position(
offset: Int,
layoutWidth: Int,
layoutHeight: Int
- ): PositionedPage {
- val wrappers = mutableListOf<PagerPlaceableWrapper>()
- val mainAxisLayoutSize =
- if (orientation == Orientation.Vertical) layoutHeight else layoutWidth
- var mainAxisOffset = if (reverseLayout) {
- mainAxisLayoutSize - offset - size
- } else {
- offset
+ ) {
+ this.offset = offset
+ mainAxisLayoutSize =
+ if (isVertical) layoutHeight else layoutWidth
+ var mainAxisOffset = offset
+ placeables.fastForEachIndexed { index, placeable ->
+ val indexInArray = index * 2
+ if (isVertical) {
+ placeableOffsets[indexInArray] = requireNotNull(horizontalAlignment)
+ .align(placeable.width, layoutWidth, layoutDirection)
+ placeableOffsets[indexInArray + 1] = mainAxisOffset
+ mainAxisOffset += placeable.height
+ } else {
+ placeableOffsets[indexInArray] = mainAxisOffset
+ placeableOffsets[indexInArray + 1] = requireNotNull(verticalAlignment)
+ .align(placeable.height, layoutHeight)
+ mainAxisOffset += placeable.width
+ }
}
- var index = if (reverseLayout) placeables.lastIndex else 0
- while (if (reverseLayout) index >= 0 else index < placeables.size) {
- val it = placeables[index]
- val addIndex = if (reverseLayout) 0 else wrappers.size
- val placeableOffset = if (orientation == Orientation.Vertical) {
- val x = requireNotNull(horizontalAlignment)
- .align(it.width, layoutWidth, layoutDirection)
- IntOffset(x, mainAxisOffset)
+ }
+
+ fun place(scope: Placeable.PlacementScope) = with(scope) {
+ require(mainAxisLayoutSize != Unset) { "position() should be called first" }
+ repeat(placeables.size) { index ->
+ val placeable = placeables[index]
+ var offset = getOffset(index)
+ if (reverseLayout) {
+ offset = offset.copy { mainAxisOffset ->
+ mainAxisLayoutSize - mainAxisOffset - placeable.mainAxisSize
+ }
+ }
+ offset += visualOffset
+ if (isVertical) {
+ placeable.placeWithLayer(offset)
} else {
- val y = requireNotNull(verticalAlignment).align(it.height, layoutHeight)
- IntOffset(mainAxisOffset, y)
+ placeable.placeRelativeWithLayer(offset)
}
- mainAxisOffset += if (orientation == Orientation.Vertical) it.height else it.width
- wrappers.add(
- addIndex,
- PagerPlaceableWrapper(placeableOffset, it, placeables[index].parentData)
- )
- if (reverseLayout) index-- else index++
}
- return PositionedPage(
- offset = offset,
- index = this.index,
- key = key,
- orientation = orientation,
- wrappers = wrappers,
- visualOffset = visualOffset,
- )
}
+
+ private fun getOffset(index: Int) =
+ IntOffset(placeableOffsets[index * 2], placeableOffsets[index * 2 + 1])
+ private val Placeable.mainAxisSize get() = if (isVertical) height else width
+ private inline fun IntOffset.copy(mainAxisMap: (Int) -> Int): IntOffset =
+ IntOffset(if (isVertical) x else mainAxisMap(x), if (isVertical) mainAxisMap(y) else y)
}
-internal class PagerPlaceableWrapper(
- val offset: IntOffset,
- val placeable: Placeable,
- val parentData: Any?
-) \ No newline at end of file
+private const val Unset = Int.MIN_VALUE
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
index 4c6eb096ab9..90e0f3db618 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasure.kt
@@ -119,7 +119,7 @@ internal fun LazyLayoutMeasureScope.measurePager(
}
// this will contain all the measured pages representing the visible pages
- val visiblePages = mutableListOf<MeasuredPage>()
+ val visiblePages = ArrayDeque<MeasuredPage>()
// define min and max offsets
val minOffset = -beforeContentPadding + if (spaceBetweenPages < 0) spaceBetweenPages else 0
@@ -146,8 +146,6 @@ internal fun LazyLayoutMeasureScope.measurePager(
orientation = orientation,
horizontalAlignment = horizontalAlignment,
verticalAlignment = verticalAlignment,
- afterContentPadding = afterContentPadding,
- beforeContentPadding = beforeContentPadding,
layoutDirection = layoutDirection,
reverseLayout = reverseLayout,
pageAvailableSize = pageAvailableSize
@@ -194,8 +192,6 @@ internal fun LazyLayoutMeasureScope.measurePager(
orientation = orientation,
horizontalAlignment = horizontalAlignment,
verticalAlignment = verticalAlignment,
- afterContentPadding = afterContentPadding,
- beforeContentPadding = beforeContentPadding,
layoutDirection = layoutDirection,
reverseLayout = reverseLayout,
pageAvailableSize = pageAvailableSize
@@ -232,8 +228,6 @@ internal fun LazyLayoutMeasureScope.measurePager(
orientation = orientation,
horizontalAlignment = horizontalAlignment,
verticalAlignment = verticalAlignment,
- afterContentPadding = afterContentPadding,
- beforeContentPadding = beforeContentPadding,
layoutDirection = layoutDirection,
reverseLayout = reverseLayout,
pageAvailableSize = pageAvailableSize
@@ -298,8 +292,6 @@ internal fun LazyLayoutMeasureScope.measurePager(
orientation = orientation,
horizontalAlignment = horizontalAlignment,
verticalAlignment = verticalAlignment,
- afterContentPadding = afterContentPadding,
- beforeContentPadding = beforeContentPadding,
layoutDirection = layoutDirection,
reverseLayout = reverseLayout,
pageAvailableSize = pageAvailableSize
@@ -326,8 +318,6 @@ internal fun LazyLayoutMeasureScope.measurePager(
orientation = orientation,
horizontalAlignment = horizontalAlignment,
verticalAlignment = verticalAlignment,
- afterContentPadding = afterContentPadding,
- beforeContentPadding = beforeContentPadding,
layoutDirection = layoutDirection,
reverseLayout = reverseLayout,
pageAvailableSize = pageAvailableSize
@@ -478,8 +468,6 @@ private fun LazyLayoutMeasureScope.getAndMeasure(
orientation: Orientation,
horizontalAlignment: Alignment.Horizontal?,
verticalAlignment: Alignment.Vertical?,
- afterContentPadding: Int,
- beforeContentPadding: Int,
layoutDirection: LayoutDirection,
reverseLayout: Boolean,
pageAvailableSize: Int
@@ -493,8 +481,6 @@ private fun LazyLayoutMeasureScope.getAndMeasure(
visualOffset = visualPageOffset,
horizontalAlignment = horizontalAlignment,
verticalAlignment = verticalAlignment,
- afterContentPadding = afterContentPadding,
- beforeContentPadding = beforeContentPadding,
layoutDirection = layoutDirection,
reverseLayout = reverseLayout,
size = pageAvailableSize,
@@ -518,7 +504,7 @@ private fun LazyLayoutMeasureScope.calculatePagesOffsets(
density: Density,
spaceBetweenPages: Int,
pageAvailableSize: Int
-): MutableList<PositionedPage> {
+): MutableList<MeasuredPage> {
val pageSizeWithSpacing = (pageAvailableSize + spaceBetweenPages)
val mainAxisLayoutSize = if (orientation == Orientation.Vertical) layoutHeight else layoutWidth
val hasSpareSpace = finalMainAxisOffset < minOf(mainAxisLayoutSize, maxOffset)
@@ -526,7 +512,7 @@ private fun LazyLayoutMeasureScope.calculatePagesOffsets(
check(pagesScrollOffset == 0)
}
val positionedPages =
- ArrayList<PositionedPage>(pages.size + extraPagesBefore.size + extraPagesAfter.size)
+ ArrayList<MeasuredPage>(pages.size + extraPagesBefore.size + extraPagesAfter.size)
if (hasSpareSpace) {
require(extraPagesBefore.isEmpty() && extraPagesAfter.isEmpty())
@@ -560,23 +546,27 @@ private fun LazyLayoutMeasureScope.calculatePagesOffsets(
} else {
absoluteOffset
}
- positionedPages.add(page.position(relativeOffset, layoutWidth, layoutHeight))
+ page.position(relativeOffset, layoutWidth, layoutHeight)
+ positionedPages.add(page)
}
} else {
var currentMainAxis = pagesScrollOffset
extraPagesBefore.fastForEach {
currentMainAxis -= pageSizeWithSpacing
- positionedPages.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ it.position(currentMainAxis, layoutWidth, layoutHeight)
+ positionedPages.add(it)
}
currentMainAxis = pagesScrollOffset
pages.fastForEach {
- positionedPages.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ it.position(currentMainAxis, layoutWidth, layoutHeight)
+ positionedPages.add(it)
currentMainAxis += pageSizeWithSpacing
}
extraPagesAfter.fastForEach {
- positionedPages.add(it.position(currentMainAxis, layoutWidth, layoutHeight))
+ it.position(currentMainAxis, layoutWidth, layoutHeight)
+ positionedPages.add(it)
currentMainAxis += pageSizeWithSpacing
}
}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt
index 2049609192c..d803fb0b451 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerMeasurePolicy.kt
@@ -40,7 +40,7 @@ import kotlin.math.roundToInt
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun rememberPagerMeasurePolicy(
- itemProvider: PagerLazyLayoutItemProvider,
+ itemProviderLambda: () -> PagerLazyLayoutItemProvider,
state: PagerState,
contentPadding: PaddingValues,
reverseLayout: Boolean,
@@ -150,6 +150,7 @@ internal fun rememberPagerMeasurePolicy(
}
}
+ val itemProvider = itemProviderLambda()
val pinnedPages = itemProvider.calculateLazyLayoutPinnedIndices(
pinnedItemList = state.pinnedPages,
beyondBoundsInfo = state.beyondBoundsInfo
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollPosition.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollPosition.kt
index cecb3717f04..5597496a395 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollPosition.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerScrollPosition.kt
@@ -17,10 +17,10 @@
package androidx.compose.foundation.pager
import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutNearestRangeState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.Snapshot
/**
* Contains the current scroll position represented by the first visible page and the first
@@ -42,6 +42,12 @@ internal class PagerScrollPosition(
/** The last know key of the page at [firstVisiblePage] position. */
private var lastKnownFirstPageKey: Any? = null
+ val nearestRangeState = LazyLayoutNearestRangeState(
+ initialPage,
+ NearestItemsSlidingWindowSize,
+ NearestItemsExtraItemCount
+ )
+
/**
* Updates the current scroll position based on the results of the last measurement.
*/
@@ -55,16 +61,12 @@ internal class PagerScrollPosition(
val scrollOffset = measureResult.firstVisiblePageOffset
check(scrollOffset >= 0f) { "scrollOffset should be non-negative ($scrollOffset)" }
- Snapshot.withoutReadObservation {
- update(
- measureResult.firstVisiblePage?.index ?: 0,
- scrollOffset
- )
- measureResult.closestPageToSnapPosition?.index?.let {
- if (it != this.currentPage) {
- this.currentPage = it
- }
- }
+ update(
+ measureResult.firstVisiblePage?.index ?: 0,
+ scrollOffset
+ )
+ measureResult.closestPageToSnapPosition?.index?.let {
+ this.currentPage = it
}
}
}
@@ -89,11 +91,19 @@ internal class PagerScrollPosition(
private fun update(index: Int, scrollOffset: Int) {
require(index >= 0f) { "Index should be non-negative ($index)" }
- if (index != this.firstVisiblePage) {
- this.firstVisiblePage = index
- }
- if (scrollOffset != this.scrollOffset) {
- this.scrollOffset = scrollOffset
- }
+ this.firstVisiblePage = index
+ nearestRangeState.update(index)
+ this.scrollOffset = scrollOffset
}
-} \ No newline at end of file
+}
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+internal const val NearestItemsSlidingWindowSize = 30
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+internal const val NearestItemsExtraItemCount = 100
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerSemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerSemantics.kt
index 82d10fdf645..ed274f670cb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerSemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerSemantics.kt
@@ -17,7 +17,6 @@
package androidx.compose.foundation.pager
import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
import androidx.compose.foundation.lazy.layout.LazyLayoutSemanticState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@@ -26,11 +25,10 @@ import androidx.compose.runtime.remember
@Composable
internal fun rememberPagerSemanticState(
state: PagerState,
- itemProvider: LazyLayoutItemProvider,
reverseScrolling: Boolean,
isVertical: Boolean
): LazyLayoutSemanticState {
- return remember(state, itemProvider, reverseScrolling, isVertical) {
+ return remember(state, reverseScrolling, isVertical) {
LazyLayoutSemanticState(state, isVertical)
}
} \ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
index ea799c26ccf..6c99d848427 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
@@ -240,7 +240,7 @@ abstract class PagerState(
internal val pageSize: Int
get() = pagerLayoutInfoState.value.pageSize
- internal var density: Density by mutableStateOf(UnitDensity)
+ internal var density: Density = UnitDensity
private val visiblePages: List<PageInfo>
get() = pagerLayoutInfoState.value.visiblePagesInfo
@@ -397,13 +397,15 @@ abstract class PagerState(
/**
* Constraints passed to the prefetcher for premeasuring the prefetched items.
*/
- internal var premeasureConstraints by mutableStateOf(Constraints())
+ internal var premeasureConstraints = Constraints()
/**
* Stores currently pinned pages which are always composed, used by for beyond bound pages.
*/
internal val pinnedPages = LazyLayoutPinnedItemList()
+ internal val nearestRange: IntRange by scrollPosition.nearestRangeState
+
/**
* Scroll (jump immediately) to a given [page].
*
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PositionedPage.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PositionedPage.kt
deleted file mode 100644
index 57ce77f2c02..00000000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PositionedPage.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.pager
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.unit.IntOffset
-
-@OptIn(ExperimentalFoundationApi::class)
-internal class PositionedPage(
- override val index: Int,
- override val offset: Int,
- val key: Any,
- val orientation: Orientation,
- val wrappers: MutableList<PagerPlaceableWrapper>,
- val visualOffset: IntOffset
-) : PageInfo {
- fun place(scope: Placeable.PlacementScope) = with(scope) {
- repeat(wrappers.size) { index ->
- val placeable = wrappers[index].placeable
- val offset = wrappers[index].offset
- if (orientation == Orientation.Vertical) {
- placeable.placeWithLayer(offset + visualOffset)
- } else {
- placeable.placeRelativeWithLayer(offset + visualOffset)
- }
- }
- }
-} \ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index 06356b3cc5b..4ba37d0b49f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -743,7 +743,7 @@ internal class CompositionImpl(
// Record derived state dependency mapping
if (value is DerivedState<*>) {
derivedStates.removeScope(value)
- for (dependency in value.dependencies) {
+ for (dependency in value.currentRecord.dependencies) {
// skip over empty objects from dependency array
if (dependency == null) break
derivedStates.add(dependency, value)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
index 538ada1a551..10bc8c43b4c 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt
@@ -39,17 +39,9 @@ import kotlin.math.min
*/
internal interface DerivedState<T> : State<T> {
/**
- * The value of the derived state retrieved without triggering a notification to read observers.
+ * Provides a current [Record].
*/
- val currentValue: T
-
- /**
- * A list of the dependencies used to produce [value] or [currentValue].
- *
- * The [dependencies] list can be used to determine when a [StateObject] appears in the apply
- * observer set, if the state could affect value of this derived state.
- */
- val dependencies: Array<Any?>
+ val currentRecord: Record<T>
/**
* Mutation policy that controls how changes are handled after state dependencies update.
@@ -57,6 +49,21 @@ internal interface DerivedState<T> : State<T> {
* produced and it is up to observer to invalidate it correctly.
*/
val policy: SnapshotMutationPolicy<T>?
+
+ interface Record<T> {
+ /**
+ * The value of the derived state retrieved without triggering a notification to read observers.
+ */
+ val currentValue: T
+
+ /**
+ * A list of the dependencies used to produce [value] or [currentValue].
+ *
+ * The [dependencies] list can be used to determine when a [StateObject] appears in the apply
+ * observer set, if the state could affect value of this derived state.
+ */
+ val dependencies: Array<Any?>
+ }
}
private val calculationBlockNestedLevel = SnapshotThreadLocal<Int>()
@@ -67,7 +74,7 @@ private class DerivedSnapshotState<T>(
) : StateObject, DerivedState<T> {
private var first: ResultRecord<T> = ResultRecord()
- class ResultRecord<T> : StateRecord() {
+ class ResultRecord<T> : StateRecord(), DerivedState.Record<T> {
companion object {
val Unset = Any()
}
@@ -75,14 +82,14 @@ private class DerivedSnapshotState<T>(
var validSnapshotId: Int = 0
var validSnapshotWriteCount: Int = 0
- var dependencies: IdentityArrayMap<StateObject, Int>? = null
+ var _dependencies: IdentityArrayMap<StateObject, Int>? = null
var result: Any? = Unset
var resultHash: Int = 0
override fun assign(value: StateRecord) {
@Suppress("UNCHECKED_CAST")
val other = value as ResultRecord<T>
- dependencies = other.dependencies
+ _dependencies = other._dependencies
result = other.result
resultHash = other.resultHash
}
@@ -108,7 +115,7 @@ private class DerivedSnapshotState<T>(
fun readableHash(derivedState: DerivedState<*>, snapshot: Snapshot): Int {
var hash = 7
- val dependencies = sync { dependencies }
+ val dependencies = sync { _dependencies }
if (dependencies != null) {
notifyObservers(derivedState) {
dependencies.forEach { stateObject, readLevel ->
@@ -134,6 +141,13 @@ private class DerivedSnapshotState<T>(
}
return hash
}
+
+ override val currentValue: T
+ @Suppress("UNCHECKED_CAST")
+ get() = result as T
+
+ override val dependencies: Array<Any?>
+ get() = _dependencies?.keys ?: emptyArray()
}
/**
@@ -143,7 +157,6 @@ private class DerivedSnapshotState<T>(
* @return latest state record for the derived state.
*/
fun current(snapshot: Snapshot): StateRecord =
- @Suppress("UNCHECKED_CAST")
currentRecord(current(first, snapshot), snapshot, false, calculation)
private fun currentRecord(
@@ -157,7 +170,7 @@ private class DerivedSnapshotState<T>(
// for correct invalidation later
if (forceDependencyReads) {
notifyObservers(this) {
- val dependencies = readable.dependencies
+ val dependencies = readable._dependencies
val invalidationNestedLevel = calculationBlockNestedLevel.get() ?: 0
dependencies?.forEach { dependency, nestedLevel ->
calculationBlockNestedLevel.set(nestedLevel + invalidationNestedLevel)
@@ -201,14 +214,14 @@ private class DerivedSnapshotState<T>(
@Suppress("UNCHECKED_CAST")
policy?.equivalent(result, readable.result as T) == true
) {
- readable.dependencies = newDependencies
+ readable._dependencies = newDependencies
readable.resultHash = readable.readableHash(this, currentSnapshot)
readable.validSnapshotId = snapshot.id
readable.validSnapshotWriteCount = snapshot.writeCount
readable
} else {
val writable = first.newWritableRecord(this, currentSnapshot)
- writable.dependencies = newDependencies
+ writable._dependencies = newDependencies
writable.resultHash = writable.readableHash(this, currentSnapshot)
writable.validSnapshotId = snapshot.id
writable.validSnapshotWriteCount = snapshot.writeCount
@@ -245,18 +258,11 @@ private class DerivedSnapshotState<T>(
}
}
- override val currentValue: T
- get() = first.withCurrent {
- @Suppress("UNCHECKED_CAST")
- currentRecord(it, Snapshot.current, false, calculation).result as T
- }
-
- override val dependencies: Array<Any?>
- get() = first.withCurrent {
- val record = currentRecord(it, Snapshot.current, false, calculation)
- @Suppress("UNCHECKED_CAST")
- record.dependencies?.keys ?: emptyArray()
+ override val currentRecord: DerivedState.Record<T> get() {
+ return first.withCurrent {
+ currentRecord(it, Snapshot.current, false, calculation)
}
+ }
override fun toString(): String = first.withCurrent {
"DerivedState(value=${displayValue()})@${hashCode()}"
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
index 38a8945a0f8..61f8d709135 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
@@ -269,7 +269,7 @@ internal class RecomposeScopeImpl(
val tracked = trackedDependencies ?: IdentityArrayMap<DerivedState<*>, Any?>().also {
trackedDependencies = it
}
- tracked[instance] = instance.currentValue
+ tracked[instance] = instance.currentRecord.currentValue
}
return false
@@ -299,7 +299,7 @@ internal class RecomposeScopeImpl(
@Suppress("UNCHECKED_CAST")
it as DerivedState<Any?>
val policy = it.policy ?: structuralEqualityPolicy()
- policy.equivalent(it.currentValue, trackedDependencies[it])
+ policy.equivalent(it.currentRecord.currentValue, trackedDependencies[it])
}
}
)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
index 43a64f55607..9cfcca0ed58 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateObserver.kt
@@ -380,7 +380,8 @@ class SnapshotStateObserver(private val onChangedExecutor: (callback: () -> Unit
/**
* Counter for skipping reads inside derived states. If count is > 0, read happens inside
* a derived state.
- * Reads for derived states are captured separately through [DerivedState.dependencies].
+ * Reads for derived states are captured separately through
+ * [DerivedState.Record.dependencies].
*/
private var deriveStateScopeCount = 0
@@ -423,10 +424,11 @@ class SnapshotStateObserver(private val onChangedExecutor: (callback: () -> Unit
val previousToken = recordedValues.add(value, currentToken)
if (value is DerivedState<*> && previousToken != currentToken) {
+ val record = value.currentRecord
// re-read the value before removing dependencies, in case the new value wasn't read
- recordedDerivedStateValues[value] = value.currentValue
+ recordedDerivedStateValues[value] = record.currentValue
- val dependencies = value.dependencies
+ val dependencies = record.dependencies
val dependencyToDerivedStates = dependencyToDerivedStates
dependencyToDerivedStates.removeScope(value)
@@ -542,7 +544,11 @@ class SnapshotStateObserver(private val onChangedExecutor: (callback: () -> Unit
val policy = derivedState.policy ?: structuralEqualityPolicy()
// Invalidate only if currentValue is different than observed on read
- if (!policy.equivalent(derivedState.currentValue, previousValue)) {
+ if (!policy.equivalent(
+ derivedState.currentRecord.currentValue,
+ previousValue
+ )
+ ) {
valueToScopes.forEachScopeOf(derivedState) { scope ->
invalidated.add(scope)
hasValues = true
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 57531e57a87..ebea07382b2 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.compose.ui.zIndex
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 @@ class DrawReorderingTest {
@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 @@ class DrawReorderingTest {
rule.runOnUiThread {
drawLatch = CountDownLatch(1)
reverseOrder = true
+ childRelayoutCount = 0
}
rule.validateSquareColors(
@@ -1092,6 +1102,74 @@ class DrawReorderingTest {
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 ba80c9152d8..cff91584285 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 @@ class GraphicsLayerTest {
rule.setContent {
FixedSize(
30,
- Modifier.padding(10).graphicsLayer().onGloballyPositioned {
- coords = it
- }
+ Modifier
+ .padding(10)
+ .graphicsLayer()
+ .onGloballyPositioned {
+ coords = it
+ }
) { /* no-op */ }
}
@@ -148,9 +151,11 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
Padding(10) {
FixedSize(
10,
- Modifier.scale(scale = 2f).onGloballyPositioned {
- coords = it
- }
+ Modifier
+ .scale(scale = 2f)
+ .onGloballyPositioned {
+ coords = it
+ }
) {
}
}
@@ -217,9 +226,11 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
Padding(10) {
FixedSize(
10,
- Modifier.rotate(90f).onGloballyPositioned {
- coords = it
- }
+ Modifier
+ .rotate(90f)
+ .onGloballyPositioned {
+ coords = it
+ }
) {
}
}
@@ -263,12 +276,14 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
}
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 @@ class GraphicsLayerTest {
FixedSize(10, Modifier.graphicsLayer(clip = true)) {
FixedSize(
10,
- Modifier.padding(20).onGloballyPositioned {
- coords = it
- }
+ Modifier
+ .padding(20)
+ .onGloballyPositioned {
+ coords = it
+ }
) {
}
}
@@ -493,7 +521,8 @@ class GraphicsLayerTest {
drawBlock: DrawScope.() -> Unit
) {
Box(
- Modifier.testTag(tag)
+ Modifier
+ .testTag(tag)
.size(size)
.background(Color.Black)
.graphicsLayer {
@@ -606,7 +635,11 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
drawRect(
color = Color.Red,
topLeft = Offset(-width, -height),
- size = Size(width * 2, height * 2))
+ size = Size(width * 2, height * 2)
+ )
}
)
}
@@ -738,7 +776,11 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
drawRect(
color = Color.Red,
topLeft = Offset(-width, -height),
- size = Size(width * 2, height * 2))
+ size = Size(width * 2, height * 2)
+ )
}
)
}
@@ -815,7 +858,11 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
drawRect(
color = Color.Red,
topLeft = Offset(-width, -height),
- size = Size(width * 2, height * 2))
+ size = Size(width * 2, height * 2)
+ )
}
)
}
@@ -896,7 +944,11 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
rule.setContent {
FixedSize(
5,
- Modifier.graphicsLayer().testTag("tag").background(color)
+ Modifier
+ .graphicsLayer()
+ .testTag("tag")
+ .background(color)
)
}
@@ -1092,14 +1151,18 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
rule.setContent {
Canvas(
modifier =
- Modifier.testTag(tag)
+ Modifier
+ .testTag(tag)
.size((dimen / LocalDensity.current.density).dp)
.background(Color.Black)
.graphicsLayer(
@@ -1209,7 +1273,8 @@ class GraphicsLayerTest {
rule.setContent {
Canvas(
modifier =
- Modifier.testTag(tag)
+ Modifier
+ .testTag(tag)
.size((dimen / LocalDensity.current.density).dp)
.background(Color.LightGray)
.graphicsLayer(
@@ -1244,7 +1309,8 @@ class GraphicsLayerTest {
rule.setContent {
Canvas(
modifier =
- Modifier.testTag(tag)
+ Modifier
+ .testTag(tag)
.size((dimen / LocalDensity.current.density).dp)
.background(Color.Black)
.graphicsLayer(
@@ -1360,7 +1426,10 @@ class GraphicsLayerTest {
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 @@ class GraphicsLayerTest {
}
}
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 @@ class GraphicsLayerTest {
}
}
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 @@ class GraphicsLayerTest {
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 154f1fd51b0..e91f316f69b 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.Modifier
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 @@ class LayoutCooperationTest {
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 @@ class LayoutCooperationTest {
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 378d5b729d7..66c3ac92bdb 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
@@ -1729,6 +1729,7 @@ class LookaheadScopeTest {
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) }
@@ -1739,6 +1740,7 @@ class LookaheadScopeTest {
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 f6553819a4f..ca5805c34e7 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 @@ class PlacementLayoutCoordinatesTest {
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 @@ class PlacementLayoutCoordinatesTest {
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 @@ class PlacementLayoutCoordinatesTest {
}) { 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 @@ class PlacementLayoutCoordinatesTest {
}
@Test
- fun parentCoordateChangeCausesRelayout() {
+ fun parentCoordinateChangeCausesRelayout() {
val locations = mutableStateListOf<LayoutCoordinates?>()
var offset by mutableStateOf(DpOffset(0.dp, 0.dp))
rule.setContent {
@@ -354,7 +354,7 @@ class PlacementLayoutCoordinatesTest {
.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 @@ class PlacementLayoutCoordinatesTest {
.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 @@ class PlacementLayoutCoordinatesTest {
.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 @@ class PlacementLayoutCoordinatesTest {
.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 @@ class PlacementLayoutCoordinatesTest {
.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 @@ class PlacementLayoutCoordinatesTest {
.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 @@ class PlacementLayoutCoordinatesTest {
.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 @@ class PlacementLayoutCoordinatesTest {
.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 @@ class PlacementLayoutCoordinatesTest {
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 fe78b5942a3..a0e6a00351d 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
@@ -870,20 +870,30 @@ internal class AndroidComposeView(context: Context, coroutineContext: CoroutineC
}
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 f718f7b6639..a2c822069a1 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 @@ internal class RenderNodeLayer(
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 af49d34430a..e674eed2c7a 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.node.NodeCoordinator
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 354fe564a8e..a6747907fc1 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 @@ internal class LookaheadLayoutCoordinatesImpl(val lookaheadDelegate: LookaheadDe
): 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 94893a6a42b..b20aa16c347 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 @@ abstract class Placeable : Measured {
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 @@ abstract class Placeable : Measured {
measurementConstraints.minHeight,
measurementConstraints.maxHeight
)
+ apparentToRealOffset =
+ IntOffset((width - measuredSize.width) / 2, (height - measuredSize.height) / 2)
}
/**
@@ -110,7 +112,7 @@ abstract class Placeable : Measured {
set(value) {
if (field != value) {
field = value
- recalculateWidthAndHeight()
+ onMeasuredSizeChanged()
}
}
@@ -119,8 +121,8 @@ abstract class Placeable : Measured {
* 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 @@ abstract class Placeable : Measured {
* 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 @@ abstract class Placeable : Measured {
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 fc84004a882..b682397f370 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 @@ internal sealed class AlignmentLines(val alignmentLinesOwner: AlignmentLinesOwne
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 e66dc1cb70a..9e079fe7577 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.node.LayoutNode.LayoutState
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 @@ internal class LayoutNodeLayoutDelegate(
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 class LayoutNodeLayoutDelegate(
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 @@ internal class LayoutNodeLayoutDelegate(
return _childDelegates.asMutableList()
}
+ var layingOutChildren = false
+ private set
+
override fun layoutChildren() {
+ layingOutChildren = true
alignmentLines.recalculateQueryOwner()
if (layoutPending) {
@@ -308,6 +351,7 @@ internal class LayoutNodeLayoutDelegate(
layoutPending = false
val oldLayoutState = layoutState
layoutState = LayoutState.LayingOut
+ coordinatesAccessedDuringPlacement = false
with(layoutNode) {
val owner = requireOwner()
owner.snapshotObserver.observeLayoutSnapshotReads(
@@ -341,6 +385,8 @@ internal class LayoutNodeLayoutDelegate(
alignmentLines.previousUsedDuringParentLayout = true
}
if (alignmentLines.dirty && alignmentLines.required) alignmentLines.recalculate()
+
+ layingOutChildren = false
}
private fun checkChildrenPlaceOrderForUpdates() {
@@ -463,7 +509,7 @@ internal class LayoutNodeLayoutDelegate(
}
private inline fun forEachChildDelegate(block: (MeasurePassDelegate) -> Unit) {
- layoutNode.children.fastForEach {
+ layoutNode.forEachChild {
block(it.measurePassDelegate)
}
}
@@ -581,6 +627,10 @@ internal class LayoutNodeLayoutDelegate(
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 @@ internal class LayoutNodeLayoutDelegate(
}
// Post-lookahead (if any) placement
- layoutState = LayoutState.LayingOut
placeOuterCoordinator(position, zIndex, layerBlock)
- layoutState = LayoutState.Idle
}
private fun placeOuterCoordinator(
@@ -613,26 +661,34 @@ internal class LayoutNodeLayoutDelegate(
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 @@ internal class LayoutNodeLayoutDelegate(
get() = layoutNode.parent?.layoutDelegate?.alignmentLinesOwner
override fun forEachChildAlignmentLinesOwner(block: (AlignmentLinesOwner) -> Unit) {
- layoutNode.children.fastForEach {
+ layoutNode.forEachChild {
block(it.layoutDelegate.alignmentLinesOwner)
}
}
@@ -756,11 +812,11 @@ internal class LayoutNodeLayoutDelegate(
*/
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 @@ internal class LayoutNodeLayoutDelegate(
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 @@ internal class LayoutNodeLayoutDelegate(
val oldLayoutState = layoutState
layoutState = LayoutState.LookaheadLayingOut
val owner = layoutNode.requireOwner()
+ coordinatesAccessedDuringPlacement = false
owner.snapshotObserver.observeLayoutSnapshotReads(layoutNode) {
clearPlaceOrder()
forEachChildAlignmentLinesOwner { child ->
@@ -985,6 +1046,8 @@ internal class LayoutNodeLayoutDelegate(
alignmentLines.previousUsedDuringParentLayout = true
}
if (alignmentLines.dirty && alignmentLines.required) alignmentLines.recalculate()
+
+ layingOutChildren = false
}
private fun checkChildrenPlaceOrderForUpdates() {
@@ -1030,7 +1093,7 @@ internal class LayoutNodeLayoutDelegate(
get() = layoutNode.parent?.layoutDelegate?.lookaheadAlignmentLinesOwner
override fun forEachChildAlignmentLinesOwner(block: (AlignmentLinesOwner) -> Unit) {
- layoutNode.children.fastForEach {
+ layoutNode.forEachChild {
block(it.layoutDelegate.lookaheadAlignmentLinesOwner!!)
}
}
@@ -1056,11 +1119,11 @@ internal class LayoutNodeLayoutDelegate(
*/
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 @@ internal class LayoutNodeLayoutDelegate(
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 @@ internal class LayoutNodeLayoutDelegate(
}
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 d297aeff46b..e743af77f9a 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 @@ internal class MeasureAndLayoutDelegate(private val root: LayoutNode) {
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 @@ internal class MeasureAndLayoutDelegate(private val root: LayoutNode) {
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 2a0b6e9f93b..f80a25234af 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 @@ internal abstract class NodeCoordinator(
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 @@ internal abstract class NodeCoordinator(
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 @@ internal abstract class NodeCoordinator(
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 @@ internal abstract class NodeCoordinator(
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 @@ internal abstract class NodeCoordinator(
}
val nodeCoordinator = sourceCoordinates.toCoordinator()
+ nodeCoordinator.onCoordinatesUsed()
val commonAncestor = findCommonAncestor(nodeCoordinator)
var position = relativeToSource
@@ -751,6 +774,7 @@ internal abstract class NodeCoordinator(
override fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {
val coordinator = sourceCoordinates.toCoordinator()
+ coordinator.onCoordinatesUsed()
val commonAncestor = findCommonAncestor(coordinator)
matrix.reset()
@@ -795,6 +819,7 @@ internal abstract class NodeCoordinator(
"LayoutCoordinates $sourceCoordinates is not attached!"
}
val srcCoordinator = sourceCoordinates.toCoordinator()
+ srcCoordinator.onCoordinatesUsed()
val commonAncestor = findCommonAncestor(srcCoordinator)
val bounds = rectCache
@@ -842,6 +867,7 @@ internal abstract class NodeCoordinator(
override fun localToRoot(relativeToLocal: Offset): Offset {
check(isAttached) { ExpectAttachedLayoutCoordinates }
+ onCoordinatesUsed()
var coordinator: NodeCoordinator? = this
var position = relativeToLocal
while (coordinator != null) {
@@ -1178,7 +1204,8 @@ internal abstract class NodeCoordinator(
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 07b7f50c268..3e9e6199ded 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
@@ -25,6 +25,8 @@ import androidx.compose.runtime.collection.mutableVectorOf
internal class OnPositionedDispatcher {
private val layoutNodes = mutableVectorOf<LayoutNode>()
+ 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 df004df15be..74a2ab71a60 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
@@ -521,10 +521,10 @@ class LayoutNodeTest {
@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 149cf479769..45447046951 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.B
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 @@ class LazyListBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -127,7 +130,7 @@ class LazyListBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -147,7 +150,7 @@ class LazyListBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
}
@@ -181,8 +184,7 @@ class LazyListBeyondBoundsTest(param: Param) {
}
// Act.
- rule.waitForIdle()
- val hasMoreContent = rule.runOnUiThread {
+ val hasMoreContent = rule.runOnIdle {
beyondBoundsLayoutRef.layout(beyondBoundsLayoutDirection) {
hasMoreContent
}
@@ -202,28 +204,27 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced { placedItems += index }
+ .trackPlaced(index)
)
}
item {
@@ -330,26 +327,22 @@ class LazyListBeyondBoundsTest(param: Param) {
.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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
}
}
}
- 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 @@ class LazyListBeyondBoundsTest(param: Param) {
Box(
Modifier
.size(10.toDp())
- .onPlaced {
- placedItems += index
- }
+ .trackPlaced(index)
)
}
item {
@@ -474,20 +461,17 @@ class LazyListBeyondBoundsTest(param: Param) {
.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 @@ class LazyListBeyondBoundsTest(param: Param) {
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 @@ class LazyListBeyondBoundsTest(param: Param) {
Column {
BasicText(
text = "Outer button",
- Modifier.focusRequester(buttonFocusRequester).focusable())
+ Modifier
+ .focusRequester(buttonFocusRequester)
+ .focusable())
TvLazyColumn {
items(3) {
@@ -617,4 +602,37 @@ class LazyListBeyondBoundsTest(param: Param) {
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
+ }
}