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.