Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to achieve a staggered grid layout using Jetpack compose?

As far as I can see we can only use Rows and Columns in Jetpack Compose to show lists. How can I achieve a staggered grid layout like the image below? The normal implementation of it using a Recyclerview and a staggered grid layout manager is pretty easy. But how to do the same in Jetpack Compose ?

like image 450
Abhriya Roy Avatar asked Oct 07 '20 04:10

Abhriya Roy


People also ask

What is staggered layout?

A LayoutManager that lays out children in a staggered grid formation. It supports horizontal & vertical layout as well as an ability to layout children in reverse. Staggered grids are likely to have gaps at the edges of the layout.

What is LazyColumn in jetpack compose?

A LazyColumn is a vertically scrolling list that only composes and lays out the currently visible items. It's similar to a Recyclerview in the classic Android View system.

How do you make a Column scrollable in jetpack compose?

We can make the Column scrollable by using the verticalScroll() modifier.


Video Answer


3 Answers

One of Google's Compose sample Owl shows how to do a staggered grid layout. This is the code snippet that is used to compose this:

@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
}

And then you can pass in your item composable in it:

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

Link to snippet in the sample: https://github.com/android/compose-samples/blob/1630f6b35ac9e25fb3cd3a64208d7c9afaaaedc5/Owl/app/src/main/java/com/example/owl/ui/courses/FeaturedCourses.kt#L161

like image 164
Saurabh Thorat Avatar answered Oct 09 '22 03:10

Saurabh Thorat


Your layout is a scrollable layout with rows of multiple cards (2 or 4)

The row with 2 items :

@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)
  } 
}

The row with 4 items :

@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)
    }
 }
}

The final grid layout :

@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)
        }
    }
} 

enter image description here

Then, you can customize with the card layout you want . I set static values for small and large cards (120, 270 for height and 170 for width)

@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)
) {
 ...
}

 

I transformed the datas in :

data class RowData(val datas: List<Item>)
data class Item(val text: String, val imgRes: Int)

You simply have to call it with

 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))

Sure you need to manage carefully your data transformation because you can have an ArrayIndexOutOfBoundsException with data[index]

like image 4
LDQ Avatar answered Oct 09 '22 02:10

LDQ


This library will help you LazyStaggeredGrid

Usage:

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
          )
      }
  }

Result:

like image 2
Nesyou Avatar answered Oct 09 '22 02:10

Nesyou