Crashed when set a “span” param to item in LazyVerticalGrid

Viewed 539

I'm using a 'LazyVerticalGrid' with 2 cells to show list items in a compose project. What I want is leting the last item occupy 2 cells, like this:

enter image description here

My approach is set the last item's 'span' param to 'GridItemSpan(2)', but when items are not exceeding one page, it is normal.However, when the items exceeds one page, I slide the list, it will crash.

//My code:

    val list: List<Int> = ArrayList<Int>().apply {
        repeat(1000) {
            add(it)
        }
    }
    LazyVerticalGrid(
        cells = GridCells.Fixed(2),
        state = state
    ) {
        items(list) {
            Text(text = "$it")
        }
        // both GridItemSpan(2) or GridItemSpan(1) here will crashed
        item(span = {
            GridItemSpan(2)
        }) {
            Text(text = "End")
        }
    }

// The Error Log:

java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
    at java.util.ArrayList.get(ArrayList.java:437)
    at androidx.compose.foundation.lazy.grid.LazyGridSpanLayoutProvider.getLineConfiguration(LazyGridSpanLayoutProvider.kt:96)
    at androidx.compose.foundation.lazy.grid.LazyMeasuredLineProvider.getAndMeasure-bKFJvoY(LazyMeasuredLineProvider.kt:60)
    at androidx.compose.foundation.lazy.grid.LazyGridMeasureKt.measureLazyGrid-0cYbdkg(LazyGridMeasure.kt:140)
    at androidx.compose.foundation.lazy.grid.LazyGridKt$rememberLazyGridMeasurePolicy$1$1.measure-3p2s80s(LazyGrid.kt:324)
    at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$2$1.invoke-0kLqBqw(LazyLayout.kt:68)
    at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$2$1.invoke(LazyLayout.kt:61)
    at androidx.compose.ui.layout.LayoutNodeSubcompositionsState$createMeasurePolicy$1.measure-3p2s80s(SubcomposeLayout.kt:514)
    at androidx.compose.ui.node.InnerPlaceable.measure-BRTryo0(InnerPlaceable.kt:54)
    at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:96)
    at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:96)
    at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:96)
    at androidx.compose.ui.node.DelegatingLayoutNodeWrapper.measure-BRTryo0(DelegatingLayoutNodeWrapper.kt:96)
    at androidx.compose.ui.graphics.SimpleGraphicsLayerModifier.measure-3p2s80s(GraphicsLayerModifier.kt:306)
    at androidx.compose.ui.node.ModifiedLayoutNode.measure-BRTryo0(ModifiedLayoutNode.kt:40)
    at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$2.invoke(OuterMeasurablePlaceable.kt:99)
    at androidx.compose.ui.node.OuterMeasurablePlaceable$remeasure$2.invoke(OuterMeasurablePlaceable.kt:98)
    at androidx.compose.runtime.snapshots.Snapshot$Companion.observe(Snapshot.kt:1918)
    at androidx.compose.runtime.snapshots.SnapshotStateObserver.observeReads(SnapshotStateObserver.kt:121)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeReads$ui_release(OwnerSnapshotObserver.kt:88)
    at androidx.compose.ui.node.OwnerSnapshotObserver.observeMeasureSnapshotReads$ui_release(OwnerSnapshotObserver.kt:76)
    at androidx.compose.ui.node.OuterMeasurablePlaceable.remeasure-BRTryo0(OuterMeasurablePlaceable.kt:98)
    at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui_release(LayoutNode.kt:1289)
    at androidx.compose.ui.node.LayoutNode.remeasure-_Sx5XlM$ui_release$default(LayoutNode.kt:1285)
    at androidx.compose.ui.node.MeasureAndLayoutDelegate.doRemeasure-sdFAvZA(MeasureAndLayoutDelegate.kt:170)
    at androidx.compose.ui.node.MeasureAndLayoutDelegate.remeasureAndRelayoutIfNeeded(MeasureAndLayoutDelegate.kt:246)
    at androidx.compose.ui.node.MeasureAndLayoutDelegate.access$remeasureAndRelayoutIfNeeded(MeasureAndLayoutDelegate.kt:38)
    at androidx.compose.ui.node.MeasureAndLayoutDelegate.measureAndLayout(MeasureAndLayoutDelegate.kt:191)
    at androidx.compose.ui.platform.AndroidComposeView.measureAndLayout(AndroidComposeView.android.kt:693)
    at androidx.compose.ui.node.Owner$DefaultImpls.measureAndLayout$default(Owner.kt:191)
    at androidx.compose.ui.platform.AndroidComposeView.dispatchDraw(AndroidComposeView.android.kt:882)
    at android.view.View.draw(View.java:23019)
    at android.view.View.updateDisplayListIfDirty(View.java:21883)
    at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4526)
    at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4499)
    at android.view.View.updateDisplayListIfDirty(View.java:21836)
    at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4526)
    at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4499)
    at android.view.View.updateDisplayListIfDirty(View.java:21836)
    at android.view.ViewGroup.recreateChildDisplayList(ViewGroup.java:4526)
    at android.view.ViewGroup.dispatchGetDisplayList(ViewGroup.java:4499)

// code where crashed:

// code in LazyGridSpanLayoutProvider.kt file
fun getLineConfiguration(lineIndex: Int): LineConfiguration {
        if (!itemsProvider.hasCustomSpans) {
            // Quick return when all spans are 1x1 - in this case we can easily calculate positions.
            val firstItemIndex = lineIndex * slotsPerLine
            return LineConfiguration(
                firstItemIndex,
                getDefaultSpans(slotsPerLine.coerceAtMost(totalSize - firstItemIndex)
                    .coerceAtLeast(0))
            )
        }

        val bucket = lineIndex / bucketSize
        // We can calculate the items on the line from the closest cached bucket start item.
        var currentLine = min(bucket, bucketStartItemIndex.size - 1) * bucketSize
        var currentItemIndex = bucketStartItemIndex[min(bucket, bucketStartItemIndex.size - 1)]




        /** Here crashed!!!
        var knownCurrentItemSpan = bucketStartKnownSpan[min(bucket, bucketStartItemIndex.size - 1)]
        **/




        // ... but try using the more localised cached values.
        if (lastLineIndex in currentLine..lineIndex) {
            // The last calculated value is a better start point. Common when scrolling main axis.
            currentLine = lastLineIndex
            currentItemIndex = lastLineStartItemIndex
            knownCurrentItemSpan = lastLineStartKnownSpan
        } else if (bucket == cachedBucketIndex && lineIndex - currentLine < cachedBucket.size) {
            // It happens that the needed line start is fully cached. Common when scrolling in
            // reverse main axis, as we decided to cacheThisBucket previously.
            currentItemIndex = cachedBucket[lineIndex - currentLine]
            currentLine = lineIndex
            knownCurrentItemSpan = 0
        }

        val cacheThisBucket = currentLine % bucketSize == 0 &&
            lineIndex - currentLine in 2 until bucketSize
        if (cacheThisBucket) {
            cachedBucketIndex = bucket
            cachedBucket.clear()
        }

        check(currentLine <= lineIndex)

        while (currentLine < lineIndex && currentItemIndex < totalSize) {
            if (cacheThisBucket) {
                cachedBucket.add(currentItemIndex)
            }

            var spansUsed = 0
            while (spansUsed < slotsPerLine && currentItemIndex < totalSize) {
                val span = if (knownCurrentItemSpan == 0) {
                    spanOf(currentItemIndex, slotsPerLine - spansUsed)
                } else {
                    knownCurrentItemSpan.also { knownCurrentItemSpan = 0 }
                }
                if (spansUsed + span > slotsPerLine) {
                    knownCurrentItemSpan = span
                    break
                }

                currentItemIndex++
                spansUsed += span
            }
            ++currentLine
            if (currentLine % bucketSize == 0 && currentItemIndex < totalSize) {
                val currentLineBucket = currentLine / bucketSize
                // This should happen, as otherwise this should have been used as starting point.
                check(bucketStartItemIndex.size == currentLineBucket)
                bucketStartItemIndex.add(currentItemIndex)
                bucketStartKnownSpan.add(knownCurrentItemSpan)
            }
        }

        lastLineIndex = lineIndex
        lastLineStartItemIndex = currentItemIndex
        lastLineStartKnownSpan = knownCurrentItemSpan

        val firstItemIndex = currentItemIndex
        val spans = mutableListOf<GridItemSpan>()

        var spansUsed = 0
        while (spansUsed < slotsPerLine && currentItemIndex < totalSize) {
            val span = if (knownCurrentItemSpan == 0) {
                spanOf(currentItemIndex, slotsPerLine - spansUsed)
            } else {
                knownCurrentItemSpan.also { knownCurrentItemSpan = 0 }
            }
            if (spansUsed + span > slotsPerLine) break

            currentItemIndex++
            spans.add(GridItemSpan(span))
            spansUsed += span
        }
        return LineConfiguration(firstItemIndex, spans)
    }

Here is a copy of the official sample: LazyVerticalGrid

import androidx.compose.foundation.border
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.GridItemSpan
import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Text

val sections = (0 until 25).toList().chunked(5)
LazyVerticalGrid(
    cells = GridCells.Fixed(3),
    horizontalArrangement = Arrangement.spacedBy(16.dp),
    verticalArrangement = Arrangement.spacedBy(16.dp)
) {
    sections.forEachIndexed { index, items ->
        item(span = { GridItemSpan(maxLineSpan) }) {
            Text(
                "This is section $index",
                Modifier.border(1.dp, Color.Gray).height(80.dp).wrapContentSize()
            )
        }
        items(
            items,
            // not required as it is the default
            span = { GridItemSpan(1) }
        ) {
            Text(
                "Item $it",
                Modifier.border(1.dp, Color.Blue).height(80.dp).wrapContentSize()
            )
        }
    }
}

It work well, but when I increase the list "sections" ,

from:

val sections = (0 until 25).toList().chunked(5)

to:

val sections = (0 until 200).toList().chunked(5)

It crashed like my code.

0 Answers
Related