Jetpack Compose中的LazyColumn中如何添加固定网格?

16

Jetpack Compose目前会抛出IllegalStateException异常,因为您无法嵌套两个可垂直滚动的组件:

@ExperimentalFoundationApi
@Composable
fun MyLazyColumn() {
    LazyColumn {
        item {
            Text(text = "My LazyColumn Title")
        }
        item {
            LazyVerticalGrid(cells = GridCells.Fixed(4)) {
                items(10) {
                    Box(
                        modifier = Modifier
                            .size(50.dp)
                            .padding(5.dp)
                            .background(Color.Gray)
                    )
                }
            }
        }
    }
}

我不想让网格本身滚动,而只是显示一个我传递给它的Composable的固定网格。有没有解决办法可以在LazyColumn中显示一个不滚动的网格?

我希望在LazyColumn中嵌入一个不可滚动的网格,但不想让这个网格本身滚动。是否有解决方法?

6个回答

11
如果您不介意使用不稳定的API,可以使用LazyVerticalGrid并使用span参数使item占据整个宽度,如@Mustafa 指出的那样:
LazyVerticalGrid(
    cells = GridCells.Fixed(spanCount),
) {
    item(
        span = { GridItemSpan(spanCount) }
    ) {
        Text("Title")
    }
    items(10) {
        Text(it.toString())
    }
}

在稳定之前,建议使用稳定的组件。

使用稳定的组件,如LazyColumnRow,以达到相同的结果。

可以通过实现gridItemsLazyColumn一起使用来完成。

fun LazyListScope.gridItems(
    count: Int,
    nColumns: Int,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    itemContent: @Composable BoxScope.(Int) -> Unit,
) {
    gridItems(
        data = List(count) { it },
        nColumns = nColumns,
        horizontalArrangement = horizontalArrangement,
        itemContent = itemContent,
    )
}

fun <T> LazyListScope.gridItems(
    data: List<T>,
    nColumns: Int,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    key: ((item: T) -> Any)? = null,
    itemContent: @Composable BoxScope.(T) -> Unit,
) {
    val rows = if (data.isEmpty()) 0 else 1 + (data.count() - 1) / nColumns
    items(rows) { rowIndex ->
        Row(horizontalArrangement = horizontalArrangement) {
            for (columnIndex in 0 until nColumns) {
                val itemIndex = rowIndex * nColumns + columnIndex
                if (itemIndex < data.count()) {
                    val item = data[itemIndex]
                    androidx.compose.runtime.key(key?.invoke(item)) {
                        Box(
                            modifier = Modifier.weight(1f, fill = true),
                            propagateMinConstraints = true
                        ) {
                            itemContent.invoke(this, item)
                        }
                    }
                } else {
                    Spacer(Modifier.weight(1f, fill = true))
                }
            }
        }
    }
}

使用方法:

LazyColumn {
    item {
        Text(text = "My LazyColumn Title")
    }
    // with count
    gridItems(10, nColumns = 4) { index -> 
        Box(
            modifier = Modifier
                .size(50.dp)
                .padding(5.dp)
                .background(Color.Gray)
        )
    }
    // or with list of items
    gridItems(listOf(1,2,3), nColumns = 4) { item ->
        Box(
            modifier = Modifier
                .size(50.dp)
                .padding(5.dp)
                .background(Color.Gray)
        )
    }
}

1
这个解决方案在数据列表较小且网格项 UI 简单的情况下运行良好,但当数据量大且网格项具有复杂的 UI 和状态时,它会变得卡顿和沉重。 - Mustafa Ibrahim
@MustafaIbrahim 你尝试过运行应用程序的发布版本吗?在调试模式下,LazyColumn 的性能要低得多。此外,您是否尝试过具有相同布局的普通单元格 LazyColumn?我认为我的方法对于普通的 LazyColumn 来说并不会增加太多开销。 - Phil Dukhov
@MustafaIbrahim LazyVerticalGrid应该比我的解决方案表现更好,因为自从**1.2.*版本开始,它已经被重写,不再使用LazyColumn。但是在1.1.***版本中,我预计性能大致相同。 - Phil Dukhov

6

这个被接受的答案非常好,并且在你有少量数据和简单的网格项UI的情况下运行良好,但是当你的数据很大且网格项具有复杂的UI和状态时,它将变得卡顿和沉重。

在我的情况下,我通过将LazyVerticalGrid作为网格项和其他内容的容器来解决了这个问题,现在它看起来像这样:

val spanCount = 2
LazyVerticalGrid(
    modifier = modifier
        .fillMaxWidth(),
    cells = GridCells.Fixed(spanCount),
    state = rememberLazyGridState(),
) {
    item(
        span = {
            /** Take a full row */
            GridItemSpan(currentLineSpan = spanCount)
        }
    ) {
        Column(
            modifier = Modifier.fillMaxWidth()
        ) {
            items.forEach {
                /** Iterate over your items here */
            }
        }
    }
    items(
        items = gridItems
    ){gridItem->
        /** Grid items go here */
    }
}

2

对我来说,我已经显式地添加了高度,在懒惰列中使用LazyverticalGrid时它可以正常工作。

 LazyVerticalGrid(
        cells = GridCells.Fixed(3),
        contentPadding = PaddingValues(5.dp),
        modifier = Modifier
            .layoutId("lazy_list")
            .height(200.dp)

0

如果您的网格较小且不太复杂(例如需要惰性加载),则可以使用Jetpack Compose ConstraintLayout。当我有一个无法修改的可滚动父级时,我遇到了这个问题。

它的工作方式与基于视图的ConstraintLayout几乎相同,但略微受限。

例如,这将是一个项目符号网格,使文本行保持与最长项目符号宽度对齐:

val spaceAfterBullet = 8.dp
val spaceBetweenRows = 4.dp

ConstraintLayout(modifier = Modifier.fillMaxWidth()) {

    val (bullet1, bullet2, bullet3, text1, text2, text3) = createRefs()
    val bulletBarrier = createEndBarrier(bullet1, bullet2, bullet3)
    val row1Barrier = createBottomBarrier(bullet1, text1)
    val row2Barrier = createBottomBarrier(bullet2, text2)

    Text(text = "1.", textAlign = TextAlign.End, modifier = Modifier.constrainAs(bullet1) {
    })
    Text(text = "First line of text", modifier = Modifier
        .padding(start = spaceAfterBullet)
        .constrainAs(text1) {
            start.linkTo(bulletBarrier)
        })

    Text(text = "2.", textAlign = TextAlign.End, modifier = Modifier.constrainAs(bullet2) {
        top.linkTo(row1Barrier, margin = spaceBetweenRows)
    })
    Text(
        text = "Second line of text which will overflow into two lines of text :)",
        modifier = Modifier
            .padding(start = spaceAfterBullet)
            .constrainAs(text2) {
                start.linkTo(bulletBarrier)
                top.linkTo(row1Barrier, margin = spaceBetweenRows)
            })

    Text(text = "99.", textAlign = TextAlign.End, modifier = Modifier.constrainAs(bullet3) {
        top.linkTo(row2Barrier, margin = spaceBetweenRows)
    })
    Text(
        text = "\"Every man has two lives, and the second starts when he realizes he has just one\" — Confucius",
        modifier = Modifier
            .padding(start = spaceAfterBullet)
            .constrainAs(text3) {
                start.linkTo(bulletBarrier)
                top.linkTo(row2Barrier, margin = spaceBetweenRows)
            })

}

enter image description here

我还创建了一个通用的 这里的代码片段,可以实现一些重复使用。


0
目前在Jetpack Compose中,这段代码会抛出IllegalStateException,因为你不能嵌套两个垂直滚动的组件。
这并不完全正确,它所说的是你不能将Constraints.Infinity传递给另一个可滚动的组合。带有滚动修饰符的LazyLists组合也依赖于不允许使用无限的maxWidth/Height来测量自身。如果你将maxHeight更改为有限的值,就不会遇到这个问题。约束意味着在放置组合之前测量它们的范围。如果你传递一个0-有限的尺寸,它将以该尺寸进行测量。
如果你的垂直网格内容比父级高度减去其他内容高度的总和小,那么你也不需要指定固定的高度。
使用Modifier.heightIn(max)将其传递,你可以将网格的高度测量约束为0且位于父级之间。
@Preview
@Composable
private fun LazyTest() {
    BoxWithConstraints {
        val parentHeight = maxHeight
        LazyColumn {
            item {
                Text(text = "My LazyColumn Title")
            }
            item {
                LazyVerticalGrid(
                    modifier = Modifier.heightIn(max = parentHeight),
                    columns = GridCells.Fixed(4)
                ) {
                    items(10) {
                        Box(
                            modifier = Modifier
                                .size(50.dp)
                                .padding(5.dp)
                                .background(Color.Gray)
                        )
                    }
                }
            }
        }
    }
}

如果在您的情况下,如果您希望允许网格覆盖剩余的LazyColumn高度,您可以测量其他元素的高度,并使用Modifier.height(parent height - 其他内容的高度之和),这实际上非常容易通过自定义的Layout来实现。
如果您好奇为什么会出现此异常,那是因为滚动代码中在设置具有滚动修饰符的组合之前进行了检查。
private class ScrollingLayoutNode(
    var scrollerState: ScrollState,
    var isReversed: Boolean,
    var isVertical: Boolean
) : LayoutModifierNode, Modifier.Node() {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        // And here in LazyVerticalGrid there is a check if there is Constraints.Infinity
        checkScrollableContainerConstraints(
            constraints,
            if (isVertical) Orientation.Vertical else Orientation.Horizontal
        )

        //  this is how LazyColumn passes Constraints.Infinity content or child Composables
        val childConstraints = constraints.copy(
            maxHeight = if (isVertical) Constraints.Infinity else constraints.maxHeight,
            maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity
        )
        val placeable = measurable.measure(childConstraints)
        val width = placeable.width.coerceAtMost(constraints.maxWidth)
        val height = placeable.height.coerceAtMost(constraints.maxHeight)
        val scrollHeight = placeable.height - height
        val scrollWidth = placeable.width - width
        val side = if (isVertical) scrollHeight else scrollWidth
        // The max value must be updated before returning from the measure block so that any other
        // chained RemeasurementModifiers that try to perform scrolling based on the new
        // measurements inside onRemeasured are able to scroll to the new max based on the newly-
        // measured size.

}

而且,checkScrollableContainerConstraints函数是什么。
fun checkScrollableContainerConstraints(
    constraints: Constraints,
    orientation: Orientation
) {
    if (orientation == Orientation.Vertical) {
        check(constraints.maxHeight != Constraints.Infinity) {
            "Vertically scrollable component was measured with an infinity maximum height " +
                "constraints, which is disallowed. One of the common reasons is nesting layouts " +
                "like LazyColumn and Column(Modifier.verticalScroll()). If you want to add a " +
                "header before the list of items please add a header as a separate item() before " +
                "the main items() inside the LazyColumn scope. There are could be other reasons " +
                "for this to happen: your ComposeView was added into a LinearLayout with some " +
                "weight, you applied Modifier.wrapContentSize(unbounded = true) or wrote a " +
                "custom layout. Please try to remove the source of infinite constraints in the " +
                "hierarchy above the scrolling container."
        }
    } else {
        check(constraints.maxWidth != Constraints.Infinity) {
            "Horizontally scrollable component was measured with an infinity maximum width " +
                "constraints, which is disallowed. One of the common reasons is nesting layouts " +
                "like LazyRow and Row(Modifier.horizontalScroll()). If you want to add a " +
                "header before the list of items please add a header as a separate item() before " +
                "the main items() inside the LazyRow scope. There are could be other reasons " +
                "for this to happen: your ComposeView was added into a LinearLayout with some " +
                "weight, you applied Modifier.wrapContentSize(unbounded = true) or wrote a " +
                "custom layout. Please try to remove the source of infinite constraints in the " +
                "hierarchy above the scrolling container."
        }
    }
}

0
@Composable
fun <T> VerticalGrid(
modifier: Modifier = Modifier,
items: List<T>,
columns: Int,
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
    if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,//You can set Arrangement.spacedBy to set the spacing between columns
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,//You can set Arrangement.spacedBy to set the spacing between rows.
content: @Composable (item: T) -> Unit
) {

val rows = items.chunked(columns)
Column(modifier = modifier, verticalArrangement = verticalArrangement) {
    rows.forEachIndexed { rowindex, rowItems ->
        Row(Modifier.fillMaxWidth(), horizontalArrangement = horizontalArrangement) {
            rowItems.forEachIndexed { index, item ->
                Box(
                    modifier = Modifier
                        .weight(1f)
                ) {
                    content(item)
                }
                if (index == rowItems.lastIndex && rowindex == rows.lastIndex) {
                    // Add a placeholder empty view
                    for (i in 0 until (columns - rowItems.size)) {
                        Spacer(
                            modifier = Modifier
                                .weight(1f)
                        )
                    }
                }

            }

        }

    }
}

}


你的回答可以通过提供更多的支持性信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的回答是否正确。你可以在帮助中心找到关于如何撰写好回答的更多信息。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接