如何使用Jetpack compose实现交错网格布局?

How to achieve a staggered grid layout using Jetpack compose?

据我所知,我们只能在 Jetpack Compose 中使用 RowsColumns 来显示列表。如何实现如下图所示的交错网格布局?使用 Recyclerview 和交错网格布局管理器的正常实现非常简单。但是如何在 Jetpack Compose 中做同样的事情呢?

Google 的一个 Compose 示例 Owl 展示了如何进行交错网格布局。这是用于编写此代码的代码片段:

@Composable
fun StaggeredVerticalGrid(
    modifier: Modifier = Modifier,
    maxColumnWidth: Dp,
    children: @Composable () -> Unit
) {
    Layout(
        children = children,
        modifier = modifier
    ) { measurables, constraints ->
        check(constraints.hasBoundedWidth) {
            "Unbounded width not supported"
        }
        val columns = ceil(constraints.maxWidth / maxColumnWidth.toPx()).toInt()
        val columnWidth = constraints.maxWidth / columns
        val itemConstraints = constraints.copy(maxWidth = columnWidth)
        val colHeights = IntArray(columns) { 0 } // track each column's height
        val placeables = measurables.map { measurable ->
            val column = shortestColumn(colHeights)
            val placeable = measurable.measure(itemConstraints)
            colHeights[column] += placeable.height
            placeable
        }

        val height = colHeights.maxOrNull()?.coerceIn(constraints.minHeight, constraints.maxHeight)
                ?: constraints.minHeight
        layout(
                width = constraints.maxWidth,
                height = height
        ) {
            val colY = IntArray(columns) { 0 }
            placeables.forEach { placeable ->
                val column = shortestColumn(colY)
                placeable.place(
                        x = columnWidth * column,
                        y = colY[column]
                )
                colY[column] += placeable.height
            }
        }
    }
}

private fun shortestColumn(colHeights: IntArray): Int {
    var minHeight = Int.MAX_VALUE
    var column = 0
    colHeights.forEachIndexed { index, height ->
        if (height < minHeight) {
            minHeight = height
            column = index
        }
    }
    return column
}

然后你可以在其中传递你的项目组合:

StaggeredVerticalGrid(
    maxColumnWidth = 220.dp,
    modifier = Modifier.padding(4.dp)
) {
    // Use your item composable here
}

Link 示例中的片段:https://github.com/android/compose-samples/blob/1630f6b35ac9e25fb3cd3a64208d7c9afaaaedc5/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt#L161

您的布局是一个可滚动的布局,包含多行卡片(2 或 4)

有 2 个项目的行:

@Composable
fun GridRow2Elements(row: RowData) {
  Row(
    modifier = Modifier
        .fillMaxWidth()
        .fillMaxHeight(),
    horizontalArrangement = Arrangement.SpaceEvenly
  ) {
    GridCard(row.datas[0], small = true, endPadding = 0.dp)
    GridCard(row.datas[1], small = true, startPadding = 0.dp)
  } 
}

有 4 个项目的行:

@Composable
fun GridRow4Elements(row: RowData) {
 Row(
    modifier = Modifier
        .fillMaxWidth()
        .fillMaxHeight(),
    horizontalArrangement = Arrangement.SpaceEvenly
 ) {
    Column {
        GridCard(row.datas[0], small = true, endPadding = 0.dp)
        GridCard(row.datas[1], small = false, endPadding = 0.dp)
    }
    Column {
        GridCard(row.datas[2], small = false, startPadding = 0.dp)
        GridCard(row.datas[3], small = true, startPadding = 0.dp)
    }
 }
}

最终的网格布局:

@Composable
fun Grid(rows: List<RowData>) {
ScrollableColumn(modifier = Modifier.fillMaxWidth()) {
    rows.mapIndexed { index, rowData ->
        if (rowData.datas.size == 2) {
            GridRow2Elements(rowData)
        } else if (rowData.datas.size == 4) {
            GridRow4Elements(rowData)
        }
    }
} 

然后,您可以自定义您想要的卡片布局。我为小型和大型卡片设置了静态值(120、270 高度和 170 宽度)

@Composable
fun GridCard(
 item: Item,
 small: Boolean,
 startPadding: Dp = 8.dp,
 endPadding: Dp = 8.dp,
) {
 Card(
    modifier = Modifier.preferredWidth(170.dp)
        .preferredHeight(if (small) 120.dp else 270.dp)
        .padding(start = startPadding, end = endPadding, top = 8.dp, bottom = 8.dp)
) {
 ...
}

 

我在 :

中转换了数据
data class RowData(val datas: List<Item>)
data class Item(val text: String, val imgRes: Int)

你只需用

调用它
 val listOf2Elements = RowData(
    listOf(
        Item("Zesty Chicken", xx),
        Item("Spring Rolls", xx),
    )
)

val listOf4Elements = RowData(
    listOf(
        Item("Apple Pie", xx),
        Item("Hot Dogs", xx),
        Item("Burger", xx),
        Item("Pizza", xx),
    )
)

Grid(listOf(listOf2Elements, listOf4Elements))

当然你需要仔细管理你的数据转换,因为你可以有一个 ArrayIndexOutOfBoundsException with data[index]

我写了自定义交错列 欢迎使用:

@Composable
fun StaggerdGridColumn(
    modifier: Modifier = Modifier,
    columns: Int = 3,
    content: @Composable () -> Unit,
) {
    Layout(content = content, modifier = modifier) { measurables, constraints ->
        val columnWidths = IntArray(columns) { 0 }
        val columnHeights = IntArray(columns) { 0 }

        val placables = measurables.mapIndexed { index, measurable ->
            val placable = measurable.measure(constraints)

            val col = index % columns
            columnHeights[col] += placable.height
            columnWidths[col] = max(columnWidths[col], placable.width)
            placable
        }

        val height = columnHeights.maxOrNull()
            ?.coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
            ?: constraints.minHeight

        val width =
            columnWidths.sumOf { it }.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth))

        val colX = IntArray(columns) { 0 }
        for (i in 1 until columns) {
            colX[i] = colX[i - 1] + columnWidths[i - 1]
        }

        layout(width, height) {
            val colY = IntArray(columns) { 0 }
            placables.forEachIndexed { index, placeable ->
                val col = index % columns
                placeable.placeRelative(
                    x = colX[col],
                    y = colY[col]
                )
                colY[col] += placeable.height
            }
        }
    }
}

使用方:

Surface(color = MaterialTheme.colors.background) {

    val size = remember {
        mutableStateOf(IntSize.Zero)
    }
    Box(
        modifier = Modifier
            .fillMaxSize()
            .verticalScroll(rememberScrollState())
            .onGloballyPositioned {
                size.value = it.size
            },
        contentAlignment = Alignment.TopCenter
    ) {
        val columns = 3
        StaggerdGridColumn(
            columns = columns
        ) {
            topics.forEach {
                Chip(
                    text = it,
                    modifier = Modifier
                        .width(with(LocalDensity.current) { (size.value.width / columns).toDp() })
                        .padding(8.dp),
                )
            }
        }
    }
}


@Composable
fun Chip(modifier: Modifier = Modifier, text: String) {
    Card(
        modifier = modifier,
        border = BorderStroke(color = Color.Black, width = 1.dp),
        shape = RoundedCornerShape(8.dp),
        elevation = 10.dp
    ) {
        Column(
            modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Box(
                modifier = Modifier
                    .size(16.dp, 16.dp)
                    .background(color = MaterialTheme.colors.secondary)
            )
            Spacer(Modifier.height(4.dp))
            Text(
                text = text,
                style = TextStyle(color = Color.DarkGray, textAlign = TextAlign.Center)
            )
        }
    }
}

这个图书馆会帮助你LazyStaggeredGrid

用法:

LazyStaggeredGrid(cells = StaggeredCells.Adaptive(minSize = 180.dp)) {
     items(60) {
        val randomHeight: Double = 100 + Math.random() * (500 - 100)
          Image(
             painter = painterResource(id = R.drawable.image),
             contentDescription = null,
             modifier = Modifier.height(randomHeight.dp).padding(10.dp),
             contentScale = ContentScale.Crop
          )
      }
  }

结果: