In xml you can use GridLayoutManager.SpanSizeLookup in GridLayoutManager to set the span size on single items (How many columns the item will use in the row, like for example, in a grid of 3 columns I can set the first item to be span size 3 so it will use all the width of the grid), but in Compose I can't find a way to do it, the vertical grid only have a way set the global span count and add items, but not set the span size of an individual item, is there a way to do it?
Jetpack Compose version 1.1.0-beta03 introduced horizontal spans to LazyVerticalGrid.
Example code:
val list by remember { mutableStateOf(listOf("A", "E", "I", "O", "U")) }
LazyVerticalGrid(
cells = GridCells.Fixed(2)
) {
// Spanned Item:
item(
span = {
// Replace "maxCurrentLineSpan" with the number of spans this item should take.
// Use "maxCurrentLineSpan" if you want to take full width.
GridItemSpan(maxCurrentLineSpan)
}
) {
Text("Vowels")
}
// Other items:
items(list) { item ->
Text(item)
}
}
There is no support for this out of the box at present. The way I have solved this for now is to use a LazyColumn then the items are Rows and in each Row you can decide how wide an item is, using weight.
I have implemented and in my case I have headers (full width), and cells of items of equal width (based on how wide the screen is, there could be 1, 2 or 3 cells per row). It's a workaround, but until there is native support from VerticalGrid this is an option.
My solution is here - look for the LazyListScope extensions.
Edit: this is no longer necessary as LazyVerticalGrid supports spans now, here's an example
LazyVerticalGrid(
columns = GridCells.Adaptive(
minSize = WeatherCardWidth,
),
modifier = modifier,
contentPadding = PaddingValues(all = MarginDouble),
horizontalArrangement = Arrangement.spacedBy(MarginDouble),
verticalArrangement = Arrangement.spacedBy(MarginDouble),
) {
state.forecastItems.forEach { dayForecast ->
item(
key = dayForecast.header.id,
span = { GridItemSpan(maxLineSpan) }
) {
ForecastHeader(
state = dayForecast.header,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = MarginDouble),
)
}
items(
items = dayForecast.forecast,
key = { hourForecast -> hourForecast.id }
) { hourForecast ->
ForecastWeatherCard(
state = hourForecast,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
Adapting the code from the answer, I created a more "general" purpose method, It can be used with Adaptive and Fixed, I'm very new with Compose so I accept suggestions
#Composable
fun HeaderGrid(cells: GridCells, content: HeaderGridScope.() -> Unit) {
var columns = 1
var minColumnWidth = 0.dp
when (cells) {
is GridCells.Fixed -> {
columns = cells.count
minColumnWidth = cells.minSize
}
is GridCells.Adaptive -> {
val width = LocalContext.current.resources.displayMetrics.widthPixels
val columnWidthPx = with(LocalDensity.current) { cells.minSize.toPx() }
minColumnWidth = cells.minSize
columns = ((width / columnWidthPx).toInt()).coerceAtLeast(1)
}
}
LazyColumn(modifier = Modifier.fillMaxWidth()){
content(HeaderGridScope(columns, minColumnWidth, this))
}
}
fun <T>HeaderGridScope.gridItems(items: List<T>, content: #Composable (T) -> Unit) {
items.chunked(numColumn).forEach {
listScope.item {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
it.forEach {
content(it)
}
if (it.size < numColumn) {
repeat(numColumn - it.size) {
Spacer(modifier = Modifier.width(columnWidth))
}
}
}
}
}
}
fun HeaderGridScope.header(content: #Composable BoxScope.() -> Unit) {
listScope.item {
Box(
modifier = Modifier
.fillMaxWidth(),
content = content
)
}
}
data class HeaderGridScope(val numColumn: Int, val columnWidth: Dp, val listScope: LazyListScope)
sealed class GridCells {
class Fixed(val count: Int, val minSize: Dp) : GridCells()
class Adaptive(val minSize: Dp) : GridCells()
}
Related
I have implemented pagination in my app using jetpack compose. You can find full code here in case you need more details : https://github.com/alirezaeiii/TMDb-Compose
Here is my code :
LazyVerticalGrid(
columns = GridCells.Fixed(COLUMN_COUNT),
contentPadding = PaddingValues(
start = Dimens.GridSpacing,
end = Dimens.GridSpacing,
bottom = WindowInsets.navigationBars.getBottom(LocalDensity.current)
.toDp().dp.plus(
Dimens.GridSpacing
)
),
horizontalArrangement = Arrangement.spacedBy(
Dimens.GridSpacing,
Alignment.CenterHorizontally
),
content = {
items(lazyTMDbItems.itemCount) { index ->
val tmdbItem = lazyTMDbItems[index]
tmdbItem?.let {
TMDbItemContent(
it,
Modifier
.height(320.dp)
.padding(vertical = Dimens.GridSpacing),
onClick
)
}
}
lazyTMDbItems.apply {
when (loadState.append) {
is LoadState.Loading -> {
item(span = span) {
LoadingRow(modifier = Modifier.padding(vertical = Dimens.GridSpacing))
}
}
is LoadState.Error -> {
val message =
(loadState.append as? LoadState.Error)?.error?.message ?: return#apply
item(span = span) {
ErrorScreen(
message = message,
modifier = Modifier.padding(vertical = Dimens.GridSpacing),
refresh = { retry() })
}
}
else -> {}
}
}
})
As you see I set columns as a fix size :
columns = GridCells.Fixed(COLUMN_COUNT)
And in order to set loading indicator and error view at the bottom of Grid in the middle of screen, I have :
private val span: (LazyGridItemSpanScope) -> GridItemSpan = { GridItemSpan(COLUMN_COUNT) }
So when I use it as bellow using span for items, it will be centered :
item(span = span) {
LoadingRow(modifier = Modifier.padding(vertical = Dimens.GridSpacing))
}
My question is when columns is not a fix size. So we have :
columns = GridCells.Adaptive(minSize = 120.dp)
How can I center loading indicator and error view at the bottom of Grid in the screen? since as I understand span is working with a fix size.
You can use the LazyGridItemSpanScope.maxLineSpan value.
As reported in the doc:
The max line span (horizontal for vertical grids) an item can occupy. This will be the number of columns in vertical grids or the number of rows in horizontal grids.
For example if LazyVerticalGrid has 3 columns this value will be 3 for each cell.
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 30.dp),
) {
items(
listgrid,
span = { index ->
GridItemSpan(maxLineSpan)
}
) {
//...
}
}
I need to implement next grid:
Size of red boxes should depend on screen width. I tried use Column and Rows:
#Composable
fun Component() {
Column(modifier = Modifier.fillMaxWidth()) {
Row {
repeat(5) {
if (it > 0) {
Spacer(modifier = Modifier.width(20.dp))
}
Box(modifier = Modifier
.aspectRatio(1f)
.weight(1f).background(Color.Red))
}
}
Spacer(modifier = Modifier.height(20.dp))
Row {
repeat(4) {
if (it > 0) {
Spacer(modifier = Modifier.width(20.dp))
}
val weight = if (it < 3) 1f else 2f
Box(modifier = Modifier
.aspectRatio(weight)
.weight(weight).background(Color.Red))
}
}
}
}
But since I have one less space in second row, it doesn't looks perfect.
How I can create pixel perfect grid view with merged cells?
I know about LazyGrid, but I'm not sure if it's appropriate since my grid needs to be full screen.
You can specify the column span with the span parameter of the LazyGridScope DSL item and items methods.
Something like:
val listgrid = (0 until 9).toList()
LazyVerticalGrid(
columns = GridCells.Fixed(5),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(listgrid,
span = { index ->
val spanCount = if (index == 8) 2 else 1
GridItemSpan(spanCount)
}
) {
Box(Modifier.size(50.dp).background(Red, RoundedCornerShape(4.dp)))
}
}
I want to have two tables below each other in my app and I used this code as a template for a table. The problem is that when I call the table component, everything after it won't be executed. It doesn't show any error or anything in logcat so I have no idea what is causing it.
Main:
Column {
MyTable(someData = mylist) // will show
Text(text = "Hello") // won't show
MyTable(someData = mylist) // won't show
}
MyTable.kt
data class Cell(
val name: String,
val weight: Float
)
#Composable
fun RowScope.TableCell(...) { /* same as in the linked code */ }
#Composable
fun MyTable(data: List<Something>) {
val columns = listOf(
Cell("a", .25f),
Cell("b", .25f),
Cell("c", .25f),
Cell("d", .25f)
)
LazyColumn(
Modifier
.fillMaxSize()
.padding(16.dp)
) {
item {
Row {
columns.forEach {
TableCell(
text = it.name,
weight = it.weight
)
}
}
}
itemsIndexed(data) { ind, item ->
Row(Modifier.fillMaxWidth()) {
TableCell(
text = item.somenumber.toString(),
weight = columns[0].weight
)
TableCell(
text = "Abc",
weight = columns[1].weight
)
TableCell(
text = item.somestring1,
weight = columns[2].weight
)
TableCell(
text = item.somestring2,
weight = columns[3].weight
)
}
}
}
}
As #Jakoss correctly pointed out, the first view with Modifier.fillMaxHeight(1f /* default value */) inside Column will take up all available space, so other views are not visible.
The solution depends on how you expect your final layout to work.
If you want to see both tables on the screen at the same time, and scroll each independently of the other, you can apply the Modifier.weight(1f) to each of them - this will make them the same size and fill height. This modifier is part of ColumnScope, so you need to pass it as a parameter of your MyTable:
MyTable:
#Composable
fun MyTable(
data: List<Something>
modifier: Modifier,
) {
// ..
LazyColumn(
modifier
.padding(16.dp)
) {
// ..
Main:
Column {
MyTable(
someData = mylist,
modifier = Modifier
.weight(1f)
)
Text(text = "Hello")
MyTable(
someData = mylist,
modifier = Modifier
.weight(1f)
)
}
The other case is if you want to have one scrollable table with two "subtables". In that case, you should have only one `LazyColumn', and both subtables as elements inside. Something like this:
Main:
LazyColumn {
myTableItems(data = mylist)
item {
Text(text = "Hello")
}
myTableItems(data = mylist)
}
myTableItems:
fun LazyListScope.myTableItems(data: List<Something>) {
val columns = listOf(
Cell("a", .25f),
Cell("b", .25f),
Cell("c", .25f),
Cell("d", .25f)
)
item {
Row {
columns.forEach {
TableCell(
text = it.name,
weight = it.weight
)
}
}
}
itemsIndexed(data) { ind, item ->
Row(Modifier.fillMaxWidth()) {
TableCell(
text = item.somenumber.toString(),
weight = columns[0].weight
)
// other cells
}
}
}
Lazy column in your table have fillMaxSize modifier so it will always claim all space the parent can provide. To display one table under the other you would have to set it to wrapContentSize which basically defeats the whole purpose of lazy column. To do all of this properly you should nest everything in single lazy column. Display header as a simple item and then rows as items underneath the header. It will work, but in my experience lazy column is not there yet in terms of performance (in case of lazy column with different children type). So maybe for that particular case I would stick with the good old recyclerview
You should remove thefillMaxSize() from your LazyColumn()
#Composable
fun MyTable(data: List<Something>) {
val columns = listOf(
Cell("a", .25f),
Cell("b", .25f),
Cell("c", .25f),
Cell("d", .25f)
)
LazyColumn(
Modifier //Removed
.padding(16.dp)
) {
I want to create table views, like the one below, to show the data I have.
A header
Another header
First
row
Second
row
I tried using LazyVerticalGrid to achieve it but Jetpack Compose doesn’t allow me to put LazyVerticalGrid inside a vertically scrollable Column.
It’s been two days and I’m really out of idea. Please help.
As far as I know, there's no built-in component to that. But it's actually easy to do it with LazyColumn and using the same weight for all lines of the same column.
See this example:
First, you can define a cell for your table:
#Composable
fun RowScope.TableCell(
text: String,
weight: Float
) {
Text(
text = text,
Modifier
.border(1.dp, Color.Black)
.weight(weight)
.padding(8.dp)
)
}
Then you can use it to build your table:
#Composable
fun TableScreen() {
// Just a fake data... a Pair of Int and String
val tableData = (1..100).mapIndexed { index, item ->
index to "Item $index"
}
// Each cell of a column must have the same weight.
val column1Weight = .3f // 30%
val column2Weight = .7f // 70%
// The LazyColumn will be our table. Notice the use of the weights below
LazyColumn(Modifier.fillMaxSize().padding(16.dp)) {
// Here is the header
item {
Row(Modifier.background(Color.Gray)) {
TableCell(text = "Column 1", weight = column1Weight)
TableCell(text = "Column 2", weight = column2Weight)
}
}
// Here are all the lines of your table.
items(tableData) {
val (id, text) = it
Row(Modifier.fillMaxWidth()) {
TableCell(text = id.toString(), weight = column1Weight)
TableCell(text = text, weight = column2Weight)
}
}
}
}
Here is the result:
An implementation that supports both fixed and variable column widths, and is horizontally and vertically scrollable would look like the following:
#Composable
fun Table(
modifier: Modifier = Modifier,
rowModifier: Modifier = Modifier,
verticalLazyListState: LazyListState = rememberLazyListState(),
horizontalScrollState: ScrollState = rememberScrollState(),
columnCount: Int,
rowCount: Int,
beforeRow: (#Composable (rowIndex: Int) -> Unit)? = null,
afterRow: (#Composable (rowIndex: Int) -> Unit)? = null,
cellContent: #Composable (columnIndex: Int, rowIndex: Int) -> Unit
) {
val columnWidths = remember { mutableStateMapOf<Int, Int>() }
Box(modifier = modifier.then(Modifier.horizontalScroll(horizontalScrollState))) {
LazyColumn(state = verticalLazyListState) {
items(rowCount) { rowIndex ->
Column {
beforeRow?.invoke(rowIndex)
Row(modifier = rowModifier) {
(0 until columnCount).forEach { columnIndex ->
Box(modifier = Modifier.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val existingWidth = columnWidths[columnIndex] ?: 0
val maxWidth = maxOf(existingWidth, placeable.width)
if (maxWidth > existingWidth) {
columnWidths[columnIndex] = maxWidth
}
layout(width = maxWidth, height = placeable.height) {
placeable.placeRelative(0, 0)
}
}) {
cellContent(columnIndex, rowIndex)
}
}
}
afterRow?.invoke(rowIndex)
}
}
}
}
}
The benefit of this implementation is that offers greater flexibility and a relatively simple API. However, there are known caveats, including: variable width columns are less performant and column dividers would have to be done on the cell level.
For variable width columns, it is not as performant as it would require multiple "layout passes". Essentially, this implementation lays out each Row in a LazyColumn, measuring each column cell in each row and storing the largest width value in a MutableState. When a larger width value is encountered for a column, it would trigger a recompose, adjusting all of the other cells in that column to be the larger width. This way, every cell in a column has the same width (and every cell in a row has the same height). For fixed width columns, the performance should be equivalent to other implementations as it doesn't require multiple "layout passes".
Usage:
Table(
modifier = Modifier.matchParentSize(),
columnCount = 3,
rowCount = 10,
cellContent = { columnIndex, rowIndex ->
Text("Column: $columnIndex; Row: $rowIndex")
})
Overloaded composable functions that use the above implementation should be fairly trivial to create, such as having a "header row" be the first row in the table.
My solution is not perfect, but at least the horizontal scroll works. The best I have not found and did not come up with. I hope Google releases its own implementation of DataTable. At the moment DataTable is not implemented for Android.
To set the width of the columns, you have to calculate their weights, but this calculation is not accurate.
private fun calcWeights(columns: List<String>, rows: List<List<String>>): List<Float> {
val weights = MutableList(columns.size) { 0 }
val fullList = rows.toMutableList()
fullList.add(columns)
fullList.forEach { list ->
list.forEachIndexed { columnIndex, value ->
weights[columnIndex] = weights[columnIndex].coerceAtLeast(value.length)
}
}
return weights
.map { it.toFloat() }
}
#Composable
fun SimpleTable(columnHeaders: List<String>, rows: List<List<String>>) {
val weights = remember { mutableStateOf(calcWeights(columnHeaders, rows)) }
Column(
modifier = Modifier
.horizontalScroll(rememberScrollState())
) {
/* HEADER */
Row(modifier = Modifier.fillMaxWidth()) {
columnHeaders.forEachIndexed { rowIndex, cell ->
val weight = weights.value[rowIndex]
SimpleCell(text = cell, weight = weight)
}
}
/* ROWS */
LazyColumn(modifier = Modifier) {
itemsIndexed(rows) { rowIndex, row ->
Row(
modifier = Modifier
.fillMaxWidth()
) {
row.forEachIndexed { columnIndex, cell ->
val weight = weights.value[columnIndex]
SimpleCell(text = cell, weight = weight)
}
}
}
}
}
}
#Composable
private fun SimpleCell(
text: String,
weight: Float = 1f
) {
val textStyle = MaterialTheme.typography.body1
val fontWidth = textStyle.fontSize.value / 2.2f // depends of font used(
val width = (fontWidth * weight).coerceAtMost(500f)
val textColor = MaterialTheme.colors.onBackground
Text(
text = text,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
color = textColor,
modifier = Modifier
.border(0.dp, textColor.copy(alpha = 0.5f))
.fillMaxWidth()
.width(width.dp + Size.marginS.times(2))
.padding(horizontal = 4.dp, vertical = 2.dp)
)
}
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 ?
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
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]
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
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
}
}
)
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:
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
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)
)
}
}
}
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