How to align items in separate rows in Compose? - android

I want to create a Compose view that looks something like this:
|-------------------------------------------|
| "Dynamic text" Image "Text " |
|-------------------------------------------|
| "Dynamic text 2" Image "Text" |
|-------------------------------------------|
Logical way to do it would be to add two Rows inside a Column. Problematic part is that Images inside those Rows must always align while the first text elements length can change.
|-------------------------------------------|
| "Dynamic longer text" Image "Text " |
|-------------------------------------------|
| "Text" Image "Text" |
|-------------------------------------------|
What is the best option for it? I've thought about using ConstraintLayout but that seems like an overkill for this simple scenario. I have also considered using Columns inside a Row, but that just doesn't feel natural in this case. All ideas welcome!

If aligning Image and Text with static text end of your Composable is okay you can set Modifier.weight(1f) for Text with dynamic text so it will cover the space that is not used by Image and Text with static text.
Column(modifier = Modifier.fillMaxSize()) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
) {
Text("Dynamic text", modifier = Modifier.weight(1f))
Image(
modifier = Modifier.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = ""
)
Text("text")
}
Row(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
) {
Text("Dynamic longer text", modifier = Modifier.weight(1f))
Image(
modifier = Modifier.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = ""
)
Text("text")
}
}
If you want a layout like in first table you should use a Layout. If you don't have any experience with Layout it might be complicated.
Steps to accomplish what's needed
1- First measure your every child Composable with Constraints and get placeable
val placeables: List<Placeable> = measurables.map { measurable ->
measurable.measure(constraints)
}
2- Get maximum width of dynamic texts, this will be our threshold for each row's second element x position, i added a padding but you can add padding to Text if you want to
// Get the maximum with of first Text on each Row
val maxDynamicWidth = placeables.filterIndexed { index, _ ->
index % 3 == 0
}.maxOf { it.width } + padding
3- Get height of each row. We will use row height for placing every 3 Composables
val rowHeights = mutableListOf<Int>()
var maxHeight = 0
// Get height for each row
placeables.forEachIndexed { index, placeable ->
maxHeight = if (index % 3 == 2) {
rowHeights.add(maxHeight)
0
} else {
placeable.height.coerceAtLeast(maxHeight)
}
}
4- get total height of our Layout, you can use constraints.maxHeight if you want to and then layout every 3 Composables on each row
val totalHeight = rowHeights.sum()
var y = 0
var x = 0
layout(constraints.maxWidth, totalHeight) {
placeables.forEachIndexed { index, placeable ->
if (index % 3 == 0) {
if (index > 0) y += rowHeights[index / 3]
placeable.placeRelative(0, y)
x = maxDynamicWidth
} else {
placeable.placeRelative(x, y)
x += placeable.width
}
}
}
Full implementation
#Composable
private fun MyLayout(
modifier: Modifier = Modifier,
paddingAfterDynamicText: Dp = 0.dp,
content: #Composable () -> Unit
) {
val padding = with(LocalDensity.current) {
paddingAfterDynamicText.roundToPx()
}
Layout(
modifier = modifier,
content = content
) { measurables: List<Measurable>, constraints: Constraints ->
require(measurables.size % 3 == 0)
val placeables: List<Placeable> = measurables.map { measurable ->
measurable.measure(constraints)
}
// Get the maximum with of first Text on each Row
val maxDynamicWidth = placeables.filterIndexed { index, _ ->
index % 3 == 0
}.maxOf { it.width } + padding
val rowHeights = mutableListOf<Int>()
var maxHeight = 0
// Get height for each row
placeables.forEachIndexed { index, placeable ->
maxHeight = if (index % 3 == 2) {
rowHeights.add(maxHeight)
0
} else {
placeable.height.coerceAtLeast(maxHeight)
}
}
val totalHeight = rowHeights.sum()
var y = 0
var x = 0
// i put Composable on each row to top of the Row
// (y-placeable.height)/2 places them to center of row
layout(constraints.maxWidth, totalHeight) {
placeables.forEachIndexed { index, placeable ->
if (index % 3 == 0) {
if (index > 0) y += rowHeights[index / 3]
placeable.placeRelative(0, y)
x = maxDynamicWidth
} else {
placeable.placeRelative(x, y)
x += placeable.width
}
}
}
}
}
You can use it as
MyLayout(
modifier = Modifier.border(3.dp, Color.Red),
paddingAfterDynamicText = 15.dp
) {
Text("Dynamic text")
Image(
modifier = Modifier.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = ""
)
Text("text")
Text("Dynamic longer text")
Image(
modifier = Modifier.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentDescription = ""
)
Text("text")
}
Result

Related

Custom top app bar with children drawn outside of parent bounds - Compose

I am trying to create a custom top app bar in Compose and Material3 (using TopAppBarLayout as base), that has a row of buttons running along the bottom edge. I want the row to spill past the bottom boundary and appear over content body of the Scaffold. Within the layout inspector, it seems that the row of buttons is placed in the correct spot, however the content body is covering the bottom part of the row:
I've looked at the Scaffold source to see if the placement issue is there, but it looks correct to me (content body is placed first, followed by top bar):
// Placing to control drawing order to match default elevation of each placeable
bodyContentPlaceables.forEach {
it.place(0, 0)
}
topBarPlaceables.forEach {
it.place(0, 0)
}
Swapping those around had no effect in any case. I also tried adding Modifier.wrapContentHeight(unbounded = true) in various spots, but none of them did what I was after.
This code is essentially TopAppBarLayout with the button row added to it:
#Composable
private fun TopAppBarLayout(
modifier: Modifier,
heightPx: Float,
navigationIconContentColor: Color,
titleContentColor: Color,
actionIconContentColor: Color,
title: #Composable () -> Unit,
titleTextStyle: TextStyle,
titleAlpha: Float,
titleVerticalArrangement: Arrangement.Vertical,
titleHorizontalArrangement: Arrangement.Horizontal,
titleBottomPadding: Int,
hideTitleSemantics: Boolean,
navigationIcon: #Composable () -> Unit,
actions: #Composable () -> Unit,
buttons: #Composable () -> Unit
) {
Layout(
{
Box(
Modifier
.layoutId("navigationIcon")
.padding(start = TopAppBarHorizontalPadding)
) {
CompositionLocalProvider(
LocalContentColor provides navigationIconContentColor,
content = navigationIcon
)
}
Box(
Modifier
.layoutId("title")
.padding(horizontal = TopAppBarHorizontalPadding)
.then(if (hideTitleSemantics) Modifier.clearAndSetSemantics { } else Modifier)
) {
ProvideTextStyle(value = titleTextStyle) {
CompositionLocalProvider(
LocalContentColor provides titleContentColor.copy(alpha = titleAlpha),
content = title
)
}
}
Box(
Modifier
.layoutId("actionIcons")
.padding(end = TopAppBarHorizontalPadding)
) {
CompositionLocalProvider(
LocalContentColor provides actionIconContentColor,
content = actions
)
}
Box(
Modifier
.layoutId("buttons")
.wrapContentHeight(unbounded = true)
) {
CompositionLocalProvider(
LocalContentColor provides Color.Red,
content = buttons
)
}
},
modifier = modifier
) { measurables, constraints ->
val navigationIconPlaceable =
measurables.first { it.layoutId == "navigationIcon" }
.measure(constraints.copy(minWidth = 0))
val actionIconsPlaceable =
measurables.first { it.layoutId == "actionIcons" }
.measure(constraints.copy(minWidth = 0))
val maxTitleWidth = if (constraints.maxWidth == Constraints.Infinity) {
constraints.maxWidth
} else {
(constraints.maxWidth - navigationIconPlaceable.width - actionIconsPlaceable.width)
.coerceAtLeast(0)
}
val titlePlaceable =
measurables.first { it.layoutId == "title" }
.measure(constraints.copy(minWidth = 0, maxWidth = maxTitleWidth))
// Locate the title's baseline.
val titleBaseline =
if (titlePlaceable[LastBaseline] != AlignmentLine.Unspecified) {
titlePlaceable[LastBaseline]
} else {
0
}
val buttonRowPlaceable =
measurables.first { it.layoutId == "buttons" }
.measure(constraints.copy(minWidth = 0))
val layoutHeight = heightPx.roundToInt()
layout(constraints.maxWidth, layoutHeight) {
// Navigation icon
navigationIconPlaceable.placeRelative(
x = 0,
y = (layoutHeight - navigationIconPlaceable.height) / 2
)
// Title
titlePlaceable.placeRelative(
x = when (titleHorizontalArrangement) {
Arrangement.Center -> (constraints.maxWidth - titlePlaceable.width) / 2
Arrangement.End ->
constraints.maxWidth - titlePlaceable.width - actionIconsPlaceable.width
// Arrangement.Start.
// An TopAppBarTitleInset will make sure the title is offset in case the
// navigation icon is missing.
else -> max(TopAppBarTitleInset.roundToPx(), navigationIconPlaceable.width)
},
y = when (titleVerticalArrangement) {
Arrangement.Center -> (layoutHeight - titlePlaceable.height) / 2
// Apply bottom padding from the title's baseline only when the Arrangement is
// "Bottom".
Arrangement.Bottom ->
if (titleBottomPadding == 0) layoutHeight - titlePlaceable.height
else layoutHeight - titlePlaceable.height - max(
0,
titleBottomPadding - titlePlaceable.height + titleBaseline
)
// Arrangement.Top
else -> 0
}
)
// Action icons
actionIconsPlaceable.placeRelative(
x = constraints.maxWidth - actionIconsPlaceable.width,
y = (layoutHeight - actionIconsPlaceable.height) / 2
)
// Button row
buttonRowPlaceable.placeRelative(
x = 0,
y = layoutHeight - buttonRowPlaceable.height / 2,
)
}
}
}

Pager indcater in Android Compose

I am new with android Compose
Can Anyone told me if there any way to implement Page indcater in compose without third library ?
I am using material design 3
I have tried many solutions
But its Dublicated
I try a way with state pager but now its Dublicated 🙂
After many attempts to find an effective solution
I found two ways
The first:
Jetpack Compose animated pager dots indicator?
The Second :
Pager indicator for compose
but you will face some issue
about PagerState Class which now is duplicated
so you Can use rememberPagerState
fun PageIndicatorSample() {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(Modifier.height(40.dp))
val pagerState1 = rememberPagerState(initialPage = 0)
val coroutineScope = rememberCoroutineScope()
PagerIndicatornew(pagerState = pagerState1, indicatorCount = getAllData.size) {
coroutineScope.launch {
pagerState1.scrollToPage(it)
}
}
HorizontalPager(
pageCount = getAllData.size,
state = pagerState1,
) {
// Here You Add what compose You Want... this is just example
Card(getAllData[it])
}
}
}
For Pager indicator
#OptIn(ExperimentalFoundationApi::class)
#Composable
fun PagerIndicatornew(
modifier: Modifier = Modifier,
pagerState: PagerState,
indicatorCount: Int = 5,
indicatorSize: Dp = 16.dp,
indicatorShape: Shape = CircleShape,
space: Dp = 8.dp,
activeColor: Color = Color(0xffEC407A),
inActiveColor: Color = Color.LightGray,
onClick: ((Int) -> Unit)? = null) {
val listState = rememberLazyListState()
val totalWidth: Dp = indicatorSize * indicatorCount + space * (indicatorCount - 1)
val widthInPx = LocalDensity.current.run { indicatorSize.toPx() }
val currentItem by remember {
derivedStateOf {
pagerState.currentPage
}
}
val itemCount = indicatorCount
LaunchedEffect(key1 = currentItem) {
val viewportSize = listState.layoutInfo.viewportSize
listState.animateScrollToItem(
currentItem,
(widthInPx / 2 - viewportSize.width / 2).toInt()
)
}
LazyRow(
modifier = modifier.width(totalWidth),
state = listState,
contentPadding = PaddingValues(vertical = space),
horizontalArrangement = Arrangement.spacedBy(space),
userScrollEnabled = false
) {
items(itemCount) { index ->
val isSelected = (index == currentItem)
// Index of item in center when odd number of indicators are set
// for 5 indicators this is 2nd indicator place
val centerItemIndex = indicatorCount / 2
val right1 =
(currentItem < centerItemIndex &&
index >= indicatorCount - 1)
val right2 =
(currentItem >= centerItemIndex &&
index >= currentItem + centerItemIndex &&
index <= itemCount - centerItemIndex + 1)
val isRightEdgeItem = right1 || right2
// Check if this item's distance to center item is smaller than half size of
// the indicator count when current indicator at the center or
// when we reach the end of list. End of the list only one item is on edge
// with 10 items and 7 indicators
// 7-3= 4th item can be the first valid left edge item and
val isLeftEdgeItem =
index <= currentItem - centerItemIndex &&
currentItem > centerItemIndex &&
index < itemCount - indicatorCount + 1
Box(
modifier = Modifier
.graphicsLayer {
val scale = if (isSelected) {
1f
} else if (isLeftEdgeItem || isRightEdgeItem) {
.5f
} else {
.8f
}
scaleX = scale
scaleY = scale
}
.clip(indicatorShape)
.size(indicatorSize)
.background(
if (isSelected) activeColor else inActiveColor,
indicatorShape
)
.then(
if (onClick != null) {
Modifier
.clickable {
onClick.invoke(index)
}
} else Modifier
)
)
}
}
}
NOTE: the first solution has some issues with indexing
so I prefer the Second one
Look at accompanist source code and customize it if you need or better use third-party lib

How create grid view with merged cells in Compose?

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

How to build grid images that change position and size and display number when image count is above threshold?

This is a share your knowledge, Q&A-style question about using Layout, different Constraints and placing based on size and other Composables' positions to achieve a grid that changes Composable size, position and adds a number if number of Composables are greater than 4.
To layout Composables based on their count Layout is required and need to select Constraints, a detailed answer about Constraints is available in this link, based on number of items and place them accordingly.
For Constraints selection when there are 2 items we need to pick half width and full height to have result in question. When there are 4 items we need to pick half width and half height.
When item count is 3 we need to use 2 constraints, 1 for measuring first 2 items, another one measuring the one that covers whole width
#Composable
private fun ImageDrawLayout(
modifier: Modifier = Modifier,
itemCount: Int,
divider: Dp,
content: #Composable () -> Unit
) {
val spacePx = LocalDensity.current.run { (divider).roundToPx() }
val measurePolicy = remember(itemCount, spacePx) {
MeasurePolicy { measurables, constraints ->
val newConstraints = when (itemCount) {
1 -> constraints
2 -> Constraints.fixed(
width = constraints.maxWidth / 2 - spacePx / 2,
height = constraints.maxHeight
)
else -> Constraints.fixed(
width = constraints.maxWidth / 2 - spacePx / 2,
height = constraints.maxHeight / 2 - spacePx / 2
)
}
val gridMeasurables = if (itemCount < 5) {
measurables
} else {
measurables.take(3) + measurables.first { it.layoutId == "Text" }
}
val placeables: List<Placeable> = if (measurables.size != 3) {
gridMeasurables.map { measurable: Measurable ->
measurable.measure(constraints = newConstraints)
}
} else {
gridMeasurables
.take(2)
.map { measurable: Measurable ->
measurable.measure(constraints = newConstraints)
} +
gridMeasurables
.last()
.measure(
constraints = Constraints.fixed(
constraints.maxWidth,
constraints.maxHeight / 2 - spacePx
)
)
}
layout(constraints.maxWidth, constraints.maxHeight) {
when (itemCount) {
1 -> {
placeables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
2 -> {
var xPos = 0
placeables.forEach { placeable: Placeable ->
placeable.placeRelative(xPos, 0)
xPos += placeable.width + spacePx
}
}
else -> {
var xPos = 0
var yPos = 0
placeables.forEachIndexed { index: Int, placeable: Placeable ->
placeable.placeRelative(xPos, yPos)
if (index % 2 == 0) {
xPos += placeable.width + spacePx
} else {
xPos = 0
}
if (index % 2 == 1) {
yPos += placeable.height + spacePx
}
}
}
}
}
}
}
Layout(
modifier = modifier,
content = content,
measurePolicy = measurePolicy
)
}
Another thing to note here is we need to find find Composable that contains Text. It's possible to find it from index since it's 4th item but i used Modifier.layoutId() for demonstration. This Modifier helps finding Composables when you don't know in which order they are placed inside a Composaable.
val gridMeasurables = if (size < 5) {
measurables
} else {
measurables.take(3) + measurables.first { it.layoutId == "Text" }
}
And place items based on item count and we add space only after first item on each row.
Usage
#Composable
fun GridImageLayout(
modifier: Modifier = Modifier,
thumbnails: List<Int>,
divider: Dp = 2.dp,
onClick: ((List<Int>) -> Unit)? = null
) {
if (thumbnails.isNotEmpty()) {
ImageDrawLayout(
modifier = modifier
.clickable {
onClick?.invoke(thumbnails)
},
divider = divider,
itemCount = thumbnails.size
) {
thumbnails.forEach {
Image(
modifier = Modifier.layoutId("Icon"),
painter = painterResource(id = it),
contentDescription = "Icon",
contentScale = ContentScale.Crop,
)
}
if (thumbnails.size > 4) {
val carry = thumbnails.size - 3
Box(
modifier = Modifier.layoutId("Text"),
contentAlignment = Alignment.Center
) {
Text(text = "+$carry", fontSize = 20.sp)
}
}
}
}
}
And using GridImageLayout
Column {
val icons5 = listOf(
R.drawable.landscape1,
R.drawable.landscape2,
R.drawable.landscape3,
R.drawable.landscape4,
R.drawable.landscape5,
R.drawable.landscape6,
R.drawable.landscape7,
)
GridImageLayout(
modifier = Modifier
.border(1.dp, Color.Red, RoundedCornerShape(10))
.clip(RoundedCornerShape(10))
.size(150.dp),
thumbnails = icons5
)
}

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

Categories

Resources