Fix placement of 0-sized items at the start of the staggered grid
The 0-sized items were ignored, as their lower boundary is exactly on the first visible pixel. This change updates visibility check for placement to only discard items where both lower and higher bounds are outside of the visible range.
Fixes: 321784348
Test: LazyStaggeredGridTest
(cherry picked from https://android-review.googlesource.com/q/commit:e153db4b0f42479af2d7d57d5f51308f057db2cc)
Merged-In: I5191c993b52df4136eaf46a7f15019383d9ef2db
Change-Id: I5191c993b52df4136eaf46a7f15019383d9ef2db
diff --git a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
index 1cf1414..3dd5ec6 100644
--- a/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
+++ b/compose/foundation/foundation/src/androidInstrumentedTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
@@ -25,6 +25,7 @@
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.list.assertIsPlaced
import androidx.compose.foundation.lazy.list.setContentWithTestViewConfiguration
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.DisposableEffect
@@ -50,6 +51,7 @@
import androidx.compose.ui.unit.dp
import androidx.test.filters.MediumTest
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Assert.assertTrue
@@ -2170,4 +2172,53 @@
assertThat(composedItems).isEqualTo(setOf(0, 1, 2, 3))
}
}
+
+ @Test
+ fun zeroSizeItemIsPlacedWhenItIsAtTheTop() {
+ lateinit var state: LazyStaggeredGridState
+
+ rule.setContent {
+ state = rememberLazyStaggeredGridState(initialFirstVisibleItemIndex = 0)
+ LazyStaggeredGrid(
+ lanes = 2,
+ state = state,
+ modifier = Modifier
+ .mainAxisSize(itemSizeDp * 2)
+ .crossAxisSize(itemSizeDp * 2)
+ ) {
+ repeat(10) { index ->
+ items(2) {
+ Spacer(Modifier.testTag("${index * 10 + it}"))
+ }
+ items(8) {
+ Spacer(Modifier.mainAxisSize(itemSizeDp))
+ }
+ }
+ }
+ }
+
+ rule.onNodeWithTag("0")
+ .assertIsPlaced()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertMainAxisSizeIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("1")
+ .assertIsPlaced()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertMainAxisSizeIsEqualTo(0.dp)
+
+ runBlocking(Dispatchers.Main + AutoTestFrameClock()) {
+ state.scrollToItem(10, 0)
+ }
+
+ rule.onNodeWithTag("10")
+ .assertIsPlaced()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertMainAxisSizeIsEqualTo(0.dp)
+
+ rule.onNodeWithTag("11")
+ .assertIsPlaced()
+ .assertMainAxisStartPositionInRootIsEqualTo(0.dp)
+ .assertMainAxisSizeIsEqualTo(0.dp)
+ }
}
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 8287261..e810b3b 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
@@ -444,6 +444,7 @@
-firstItemOffsets[it]
}
+ val minVisibleOffset = minOffset + mainAxisSpacing
val maxOffset = (mainAxisAvailableSize + afterContentPadding).coerceAtLeast(0)
debugLog {
@@ -473,14 +474,20 @@
)
laneInfo.setLane(itemIndex, spanRange.laneInfo)
- val offset = currentItemOffsets.maxInRange(spanRange) + measuredItem.sizeWithSpacings
+ val offset = currentItemOffsets.maxInRange(spanRange)
spanRange.forEach { lane ->
- currentItemOffsets[lane] = offset
+ currentItemOffsets[lane] = offset + measuredItem.sizeWithSpacings
currentItemIndices[lane] = itemIndex
measuredItems[lane].addLast(measuredItem)
}
- if (currentItemOffsets[spanRange.start] <= minOffset + mainAxisSpacing) {
+ // item is not visible if both start and end bounds are outside of the visible range.
+ if (
+ offset < minVisibleOffset && currentItemOffsets[spanRange.start] <= minVisibleOffset
+ ) {
+ // We scrolled past measuredItem, and it is not visible anymore. We measured it
+ // for correct positioning of other items, but there's no need to place it.
+ // Mark it as not visible and filter below.
measuredItem.isVisible = false
remeasureNeeded = true
}
@@ -538,7 +545,10 @@
}
laneInfo.setGaps(itemIndex, gaps)
- if (currentItemOffsets[spanRange.start] <= minOffset + mainAxisSpacing) {
+ // item is not visible if both start and end bounds are outside of the visible range.
+ if (
+ offset < minVisibleOffset && currentItemOffsets[spanRange.start] <= minVisibleOffset
+ ) {
// We scrolled past measuredItem, and it is not visible anymore. We measured it
// for correct positioning of other items, but there's no need to place it.
// Mark it as not visible and filter below.