Reputation: 1438
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
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
Upvotes: 2
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
}
}
)
Upvotes: 4
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
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.
Working video : https://drive.google.com/file/d/1IsKy0wzbyqI3dme3x7rzrZ6uHZZE9jrL/view?usp=sharing
Upvotes: 0
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
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
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)
}
}
}
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
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