在Compose 1.2版本中,惰性网格布局相关的API去除了实验性的标记,升级为稳定版。
如果你需要惰性(Lazy)的网格布局,除了考虑使用LazyLayout
实现一个之外,还可以试试开箱即用的LazyVerticalGrid
和LazyHorizontalGrid
。顾名思义,LazyVerticalGrid
允许纵向滚动,依次横向地布局子项;LazyHorizontalGrid
允许横向滚动,依次纵向地布局子项。它们的实际布局效果类似于下图:
下面以LazyVerticalGrid
为例,介绍如何使用相关的API。
如果你希望简单得到一个列数固定为两列的网格布局,可以在columns
(注意,这个参数在低于1.2版本的时候称作cells
)参数中传入GridCells.Fixed(2)
:
@Composable
fun LazyVerticalGridSample(data: List<Int>) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
) {
items(data) { item ->
SampleItem(item)
}
}
}
如果希望控制每个子项的间隔,我们还可以添加参数horizontalArrangement
和verticalArrangement
,下述代码为每个子项之间增加了10dp的纵向间隔和横向间隔:
@Composable
fun LazyVerticalGridSample(data: List<Int>) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(data) { item ->
SampleItem(item)
}
}
}
固定列数的垂直Grid在布局上的实现思路并不复杂:首先,如果有设置horizontalArrangement
,则先从Grid的可用宽度中减去子项的间隔总共需要占用的宽度。以上面的代码为例,固定两列、子项间距为10dp的Grid,可用宽度为width - 10
。然后,两列的子项平分剩余的所有宽度,每个子项的可用宽度为(width - 10) / 2
。
有没有察觉到这种布局方式可能在哪些场景中出问题?在Fixed
模式中,子项的具体宽度是不确定的,依赖于Grid的具体宽度,而若是Grid被设置为占满整个屏幕的宽度的话,就有可能出现子项的宽度过度拉伸的问题,比如在旋转屏幕或者在平板等屏幕宽度更大的设备运行App时。
如果想避免这个问题,可以考虑使用另一种布局模式GridCells.Adaptive
:
@Composable
fun LazyVerticalGridSample(data: List<Int>) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 20.dp),
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(data) { item ->
SampleItem(item)
}
}
}
GridCells.Adaptive
是一种自适应的策略,它会尽量生成较多的列,每一列的最小宽度为minSize
。上述例子中,传入的minSize
为20dp,假设Grid的可用宽度为88dp,那么GridCells.Adaptive(minSize = 20.dp)
意味着LazyVerticalGrid
最多能够放下4列,占用80dp。剩下的88-80=8dp怎么办?它们会平均分配到每一列中,换句话说,现在每一列的宽度其实是22dp。
如果固定列数或者自适应都不能满足你的需求,例如虽然Grid固定为两列,但希望第二列获得的宽度是第一列的一半,那么可以试试自定义新的GridCells
实现。GridCells
实际上是一个接口,前面提到的Fixed
和Adaptive
是其实现类。
@Stable
interface GridCells {
fun Density.calculateCrossAxisCellSizes(availableSize: Int, spacing: Int): List<Int>
}
calculateCrossAxisCellSizes
传入Grid需要布局的轴上可用的宽度以及由verticalArrangement
或horizontalArrangement
传入的分隔的大小,要求返回指定每一列的宽度的列表。传入的参数和要求返回的列表都以px为单位。
如果你在用Compose 1.2以下的版本,那么很遗憾,GridCells
是一个sealed class
,无法自定义。
在上述讨论中,我们都默认单个子项的列跨度(即占用多少列)为1,但是有很多的场景希望能够自定义列跨度。比如我们希望在子项前添加一个类别的卡片Title:
LazyVerticalGrid(...) {
...
item(span = {
// this上下文为LazyGridItemSpanScope
GridItemSpan(this.maxLineSpan)
}) {
CategoryTitle(text = "Vegetables")
}
items(data) { item ->
SampleItem(item)
}
}
放置子项的item
、items
等函数可以传入一个创建GridItemSpan
的lambda,描述子项会跨越多少列。
lambda上下文中提供的LazyGridItemSpanScope
类包含两个数据,其中一个就是本例使用的maxLineSpan
,这个数据描述了Grid现在最多有多少列,在GridCells.Adaptive
的布局模式下非常实用。