Fix LazyColumn/Row cross axis wrap content
Sometimes it was ignoring the items which would be visible and sometimes was using the size of item which is in fact outside of the viewport
Test: new tests
Change-Id: I1c0b08ea37ccec71b5156793ee8fe3541c8fcdbd
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt
index 4d6ba65..bbd257b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt
@@ -18,6 +18,7 @@
import androidx.compose.animation.core.ExponentialDecay
import androidx.compose.animation.core.ManualAnimationClock
+import androidx.compose.animation.core.snap
import androidx.compose.foundation.animation.FlingConfig
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
@@ -1040,9 +1041,72 @@
}
}
+ @Test
+ fun notVisibleAnymoreItemNotAffectingCrossAxisSize() {
+ val items = (0..1).toList()
+ val itemSize = with(rule.density) { 30.toDp() }
+ val itemSizeMinusOne = with(rule.density) { 29.toDp() }
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumnFor(
+ items = items,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.height(itemSizeMinusOne).testTag(LazyColumnForTag)
+ ) {
+ Spacer(
+ if (it == 0) {
+ Modifier.width(30.dp).height(itemSizeMinusOne)
+ } else {
+ Modifier.width(20.dp).height(itemSize)
+ }
+ )
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag(LazyColumnForTag)
+ .assertWidthIsEqualTo(20.dp)
+ }
+
+ @Test
+ fun itemStillVisibleAfterOverscrollIsAffectingCrossAxisSize() {
+ val items = (0..2).toList()
+ val itemSize = with(rule.density) { 30.toDp() }
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyColumnFor(
+ items = items,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.height(itemSize * 1.75f).testTag(LazyColumnForTag)
+ ) {
+ Spacer(
+ if (it == 0) {
+ Modifier.width(30.dp).height(itemSize / 2)
+ } else if (it == 1) {
+ Modifier.width(20.dp).height(itemSize / 2)
+ } else {
+ Modifier.width(20.dp).height(itemSize)
+ }
+ )
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag(LazyColumnForTag)
+ .assertWidthIsEqualTo(30.dp)
+ }
+
private fun SemanticsNodeInteraction.assertTopPositionIsAlmost(expected: Dp) {
getUnclippedBoundsInRoot().top.assertIsEqualTo(expected, tolerance = 1.dp)
}
+
+ private fun LazyListState.scrollBy(offset: Dp) {
+ runBlocking {
+ smoothScrollBy(with(rule.density) { offset.toIntPx().toFloat() }, snap())
+ }
+ }
}
data class NotStable(val count: Int)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt
index 6540372..f4d3fd4 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.lazy
+import androidx.compose.animation.core.snap
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -836,4 +837,67 @@
.that(redrawCount[1]).isEqualTo(2)
}
}
+
+ @Test
+ fun notVisibleAnymoreItemNotAffectingCrossAxisSize() {
+ val items = (0..1).toList()
+ val itemSize = with(rule.density) { 30.toDp() }
+ val itemSizeMinusOne = with(rule.density) { 29.toDp() }
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyRowFor(
+ items = items,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.width(itemSizeMinusOne).testTag(LazyRowForTag)
+ ) {
+ Spacer(
+ if (it == 0) {
+ Modifier.height(30.dp).width(itemSizeMinusOne)
+ } else {
+ Modifier.height(20.dp).width(itemSize)
+ }
+ )
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag(LazyRowForTag)
+ .assertHeightIsEqualTo(20.dp)
+ }
+
+ @Test
+ fun itemStillVisibleAfterOverscrollIsAffectingCrossAxisSize() {
+ val items = (0..2).toList()
+ val itemSize = with(rule.density) { 30.toDp() }
+ lateinit var state: LazyListState
+ rule.setContent {
+ LazyRowFor(
+ items = items,
+ state = rememberLazyListState().also { state = it },
+ modifier = Modifier.width(itemSize * 1.75f).testTag(LazyRowForTag)
+ ) {
+ Spacer(
+ if (it == 0) {
+ Modifier.height(30.dp).width(itemSize / 2)
+ } else if (it == 1) {
+ Modifier.height(20.dp).width(itemSize / 2)
+ } else {
+ Modifier.height(20.dp).width(itemSize)
+ }
+ )
+ }
+ }
+
+ state.scrollBy(itemSize)
+
+ rule.onNodeWithTag(LazyRowForTag)
+ .assertHeightIsEqualTo(30.dp)
+ }
+
+ private fun LazyListState.scrollBy(offset: Dp) {
+ runBlocking {
+ smoothScrollBy(with(rule.density) { offset.toIntPx().toFloat() }, snap())
+ }
+ }
}
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 484d23b..45ea9be 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
@@ -323,6 +323,9 @@
val minOffset = -startContentPadding
val maxOffset = mainAxisMaxSize
+ // max of cross axis sizes of all visible items
+ var maxCrossAxis = 0
+
// we had scrolled backward or we compose items in the start padding area, which means
// items before current firstItemScrollOffset should be visible. compose them and update
// firstItemScrollOffset
@@ -330,6 +333,7 @@
val previous = DataIndex(currentFirstItemIndex.value - 1)
val measuredItem = itemProvider.getAndMeasure(previous)
visibleItems.add(0, measuredItem)
+ maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
currentFirstItemScrollOffset += measuredItem.mainAxisSize
currentFirstItemIndex = previous
}
@@ -351,11 +355,9 @@
var index = goingForwardInitialIndex
val maxMainAxis = maxOffset + endContentPadding
var mainAxisUsed = -goingForwardInitialScrollOffset
- var maxCrossAxis = 0
while (mainAxisUsed <= maxMainAxis && index.value < itemsCount) {
val measuredItem = itemProvider.getAndMeasure(index)
mainAxisUsed += measuredItem.mainAxisSize
- maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
if (mainAxisUsed < minOffset) {
// this item is offscreen and will not be placed. advance firstVisibleItemIndex
@@ -368,6 +370,7 @@
}
notUsedButComposedItems.add(measuredItem)
} else {
+ maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
visibleItems.add(measuredItem)
}
@@ -389,6 +392,7 @@
itemProvider.getAndMeasure(previous)
}
visibleItems.add(0, measuredItem)
+ maxCrossAxis = maxOf(maxCrossAxis, measuredItem.crossAxisSize)
currentFirstItemScrollOffset += measuredItem.mainAxisSize
currentFirstItemIndex = previous
}