Abhriya Roy
Abhriya Roy

Reputation: 1438

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 ?

Upvotes: 27

Views: 11392

Answers (8)

AskNilesh
AskNilesh

Reputation: 69709

Better to use LazyVerticalStaggeredGrid

Follow this steps

Step 1 Add the below dependency in your build.gradle file

implementation "androidx.compose.foundation:foundation:1.3.0-rc01"

Step 2 import the below classes in your activity file

import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells

Step 3 Add LazyVerticalStaggeredGrid like this

  LazyVerticalStaggeredGrid(
    columns = StaggeredGridCells.Fixed(2),
    state = state,
    modifier = Modifier.fillMaxSize(),
    content = {
      val list = listOf(1,2,4,3,5,6,8,8,9)
      items(list.size) { position ->
        Box(
          Modifier.padding(5.dp)
        ) {
          // create your own layout here
          NotesItem(list[position])
        }
      }
    })

OUTPUT

enter image description here

Upvotes: 2

Gabriele Mariotti
Gabriele Mariotti

Reputation: 364451

Starting from 1.3.0-beta02 you can use the LazyVerticalStaggeredGrid.

Something like:

    val state = rememberLazyStaggeredGridState()

    LazyVerticalStaggeredGrid(
        columns = StaggeredGridCells.Fixed(2),
        modifier = Modifier.fillMaxSize(),
        state = state,
        content = {

            items(count) {
                //item content
            }
        }
    )

enter image description here

Upvotes: 4

Unes
Unes

Reputation: 80

It's now available in version 1.3.0-beta02. You can implement it like this:

LazyVerticalStaggeredGrid(
    columns = StaggeredGridCells.Fixed(2),
) {
    itemsIndexed((0..50).toList()) { i, item ->
        Box(
            Modifier
                .padding(2.dp)
                .fillMaxWidth()
                .height(20.dp * i)
                .background(Color.Cyan),
        )
    }
}

Or you can use horizontal view LazyHorizontalStaggeredGrid

Upvotes: 10

rishi kumar
rishi kumar

Reputation: 111

Really saved a lot of time thanks guys(author of answers). I tried all 3 ways.

This is not an answer rather an observation. For me order of items were not maintained for answer#11. For sample list it did , but with actual list in office work it did not. ordering was altered by one position. I tried even with array list, input list were ordered but views were displaced still.

However, answer#22 did maintained order. And works correctly. I am using this one. answer#33 did worked as expected as both columns have their individual and independent scroll behaviour

Note: Pagination is still not supported in any of the custom implementation. Manual observation on last item is required to trigger fetching new data. (we can't use pager from pager library, there's no way to make call on pager obj. However, there is manual paging in 'start' code of advance paging codelab (manual paging works there in sample)) https://developer.android.com/codelabs/android-paging#0
Cheers folks.!!

UPDATE with working answer

Please go thorough Android jetpack compose pagination : Pagination not working with staggered layout jetpack compose , Where I have working sample of staggered layout in compose and also with supporting pagination.

Solution : https://github.com/rishikumr/stackoverflow_code_sharing/tree/main/staggered-layout-compose-with_manual_pagination

Working video : https://drive.google.com/file/d/1IsKy0wzbyqI3dme3x7rzrZ6uHZZE9jrL/view?usp=sharing

Upvotes: 0

Unes
Unes

Reputation: 80

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:

Upvotes: 2

Hashem Mousavi
Hashem Mousavi

Reputation: 440

I wrote custom staggered column feel free to use it:

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

Using side:

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

Upvotes: 1

Louis Duboscq
Louis Duboscq

Reputation: 497

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]

Upvotes: 4

Saurabh Thorat
Saurabh Thorat

Reputation: 20714

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

Upvotes: 16

Related Questions