Scroll Multiple LazyRows together - LazyHorizontalGrid alike? - android

How to assign the same scroll state to two LazyRows, so that both row scrolls together?
Jetpack compose lists currently doesn't have LazyHorizontalGrid, So any alternative solution?
Column{
LazyRow(
modifier = Modifier.fillMaxWidth()
) {
// My sublist1
}
LazyRow(
modifier = Modifier.fillMaxWidth()
) {
// My sublist2
}
}
Trying to implement below:

Update: Google has added the component officially - LazyHorizontalGrid.
I modified the LazyVerticalGrid class, and made it work towards only GridCells.Fixed(n) horizontal grid.
Here is the complete gist code: LazyHorizontalGrid.kt
Main changes
#Composable
#ExperimentalFoundationApi
private fun FixedLazyGrid(
nRows: Int,
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
scope: LazyGridScopeImpl
) {
val columns = (scope.totalSize + nRows - 1) / nRows
LazyRow(
modifier = modifier,
state = state,
contentPadding = contentPadding,
) {
items(columns) { columnIndex ->
Column {
for (rowIndex in 0 until nRows) {
val itemIndex = columnIndex * nRows + rowIndex
if (itemIndex < scope.totalSize) {
Box(
modifier = Modifier.wrapContentSize(),
propagateMinConstraints = true
) {
scope.contentFor(itemIndex, this#items).invoke()
}
} else {
Spacer(Modifier.weight(1f, fill = true))
}
}
}
}
}
}
Code Usage
LazyHorizontalGrid(
cells = GridCells.Fixed(2)
) {
items(items = restaurantsList){
RestaurantItem(r = it, modifier = Modifier.fillParentMaxWidth(0.8f))
}
}

Related

Auto scrolling pager not working properly in Android Jetpack Compose

I am learning jetpack compose.I am trying to implement a viewpager in jetpack compose where 5 image will be auto scrolled after 3 sec just like a carousel banner.Everything is alright before last index item image.After auto scroll to last index ,page should be scrolled to 0 index and will repeat.That's where the problem begain.The pager not working perfectly here .It's reapeting 3-4 index and sometimes stuck between to image/page after first auto scroll.
This is the img
My Code
#OptIn(ExperimentalPagerApi::class)
#Composable
fun HorizontalPagerScreen() {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(0.dp, 5.dp)
) {
val items = createItems()
val pagerState = rememberPagerState()
HorizontalPager(
modifier = Modifier
.fillMaxWidth()
.height(250.dp),
count = items.size,
state = pagerState,
verticalAlignment = Alignment.Top,
) { currentPage ->
Image(
painter = rememberAsyncImagePainter(items[currentPage].Image),
contentDescription = items[currentPage].title,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth(),
)
//Here's the code for auto scrolling
LaunchedEffect(key1= Unit, key2= pagerState.currentPage) {
while (true) {
yield()
delay(3000)
var newPage = pagerState.currentPage + 1
if (newPage > items.lastIndex) newPage = 0
pagerState.animateScrollToPage(newPage)
}
}
}
}
}
**How to make it auto scroll for infinite times **
You can create a loopingCount variable that you increment every few seconds using a LaunchedEffect and then mod it with the max amount of pages, you also need to take into account if the user is dragging on the pager or not.
The full code sample can be found here, but added below too:
#Composable
fun HorizontalPagerLoopingIndicatorSample() {
Scaffold(
modifier = Modifier.fillMaxSize()
) { padding ->
Column(
Modifier
.fillMaxSize()
.padding(padding)
) {
// Display 10 items
val pageCount = 10
// We start the pager in the middle of the raw number of pages
val loopingCount = Int.MAX_VALUE
val startIndex = loopingCount / 2
val pagerState = rememberPagerState(initialPage = startIndex)
fun pageMapper(index: Int): Int {
return (index - startIndex).floorMod(pageCount)
}
HorizontalPager(
// Set the raw page count to a really large number
pageCount = loopingCount,
state = pagerState,
// Add 32.dp horizontal padding to 'center' the pages
contentPadding = PaddingValues(horizontal = 32.dp),
// Add some horizontal spacing between items
pageSpacing = 4.dp,
modifier = Modifier
.weight(1f)
.fillMaxWidth()
) { index ->
// We calculate the page from the given index
val page = pageMapper(index)
PagerSampleItem(
page = page,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
)
}
HorizontalPagerIndicator(
pagerState = pagerState,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(16.dp),
pageCount = pageCount,
pageIndexMapping = ::pageMapper
)
val loopState = remember {
mutableStateOf(true)
}
LoopControl(loopState, Modifier.align(Alignment.CenterHorizontally))
ActionsRow(
pagerState = pagerState,
modifier = Modifier.align(Alignment.CenterHorizontally),
infiniteLoop = true
)
var underDragging by remember {
mutableStateOf(false)
}
LaunchedEffect(key1 = Unit) {
pagerState.interactionSource.interactions.collect { interaction ->
when (interaction) {
is PressInteraction.Press -> underDragging = true
is PressInteraction.Release -> underDragging = false
is PressInteraction.Cancel -> underDragging = false
is DragInteraction.Start -> underDragging = true
is DragInteraction.Stop -> underDragging = false
is DragInteraction.Cancel -> underDragging = false
}
}
}
val looping = loopState.value
if (underDragging.not() && looping) {
LaunchedEffect(key1 = underDragging) {
try {
while (true) {
delay(1000L)
val current = pagerState.currentPage
val currentPos = pageMapper(current)
val nextPage = current + 1
if (underDragging.not()) {
val toPage = nextPage.takeIf { nextPage < pageCount } ?: (currentPos + startIndex + 1)
if (toPage > current) {
pagerState.animateScrollToPage(toPage)
} else {
pagerState.scrollToPage(toPage)
}
}
}
} catch (e: CancellationException) {
Log.i("page", "Launched paging cancelled")
}
}
}
}
}
}
#Composable
fun LoopControl(
loopState: MutableState<Boolean>,
modifier: Modifier = Modifier,
) {
IconButton(
onClick = { loopState.value = loopState.value.not() },
modifier = modifier
) {
val icon = if (loopState.value) {
Icons.Default.PauseCircle
} else {
Icons.Default.PlayCircle
}
Icon(imageVector = icon, contentDescription = null)
}
}
private fun Int.floorMod(other: Int): Int = when (other) {
0 -> this
else -> this - floorDiv(other) * other
}

animation o a lazycolumn android

I have a list with 10 items one of them have this elements "rankingCurrentPlace", "rankingPastPlace" and "isUser:true".
What i need to do its an animation on the lazycolumn if the api esponse is like this
"isUser:true", "rankingPastPlace:3" , "rankingCurrentPlace:7"
i need to show an animation in the list where the row starts in the third place and descend to the seventh place
is there a way to do this?
this is what I actually have
LazyColumn(
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
) {
items(
items = leaderboard,
key = { leaderBoard ->
leaderBoard.rankingPlace
}
) { leaderBoard ->
RowComposable( modifier = Modifier
.fillMaxWidth(),
topicsItem = leaderBoard,)
}
This answer works except when swapping first item with any item even with basic swap function without animation. I think it would be better to ask a new question about why swapping first item doesn't work or if it is bug. Other than that works as expected. If you need to move to items that are not in screen you can lazyListState.layoutInfo.visibleItemsInfo and compare with initial item and scroll to it before animation
1.Have a SnapshotStateList of data to trigger recomposition when we swap 2 items
class MyData(val uuid: String, val value: String)
val items: SnapshotStateList<MyData> = remember {
mutableStateListOf<MyData>().apply {
repeat(20) {
add(MyData( uuid = UUID.randomUUID().toString(), "Row $it"))
}
}
}
2.Function to swap items
private fun swap(list: SnapshotStateList<MyData>, from: Int, to: Int) {
val size = list.size
if (from in 0 until size && to in 0 until size) {
val temp = list[from]
list[from] = list[to]
list[to] = temp
}
}
3.Function to swap items one by one. There is a bug with swapping first item. Even if it's with function above when swapping first item other one moves up without showing animation via Modififer.animateItemPlacement().
#Composable
private fun animatedSwap(
lazyListState: LazyListState,
items: SnapshotStateList<MyData>,
from: Int,
to: Int,
onFinish: () -> Unit
) {
LaunchedEffect(key1 = Unit) {
val difference = from - to
val increasing = difference < 0
var currentValue: Int = from
repeat(abs(difference)) {
val temp = currentValue
if (increasing) {
currentValue++
} else {
currentValue--
}
swap(items, temp, currentValue)
if (!increasing && currentValue == 0) {
delay(300)
lazyListState.scrollToItem(0)
}
delay(350)
}
onFinish()
}
}
4.List with items that have Modifier.animateItemPlacement()
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
state = lazyListState,
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(
items = items,
key = {
it.uuid
}
) {
Row(
modifier = Modifier
.animateItemPlacement(
tween(durationMillis = 200)
)
.shadow(1.dp, RoundedCornerShape(8.dp))
.background(Color.White)
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentScale = ContentScale.FillBounds,
contentDescription = null
)
Spacer(modifier = Modifier.width(10.dp))
Text(it.value, fontSize = 18.sp)
}
}
}
Demo
#OptIn(ExperimentalFoundationApi::class)
#Composable
private fun AnimatedList() {
Column(modifier = Modifier.fillMaxSize()) {
val items: SnapshotStateList<MyData> = remember {
mutableStateListOf<MyData>().apply {
repeat(20) {
add(MyData(uuid = UUID.randomUUID().toString(), "Row $it"))
}
}
}
val lazyListState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
state = lazyListState,
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
items(
items = items,
key = {
it.uuid
}
) {
Row(
modifier = Modifier
.animateItemPlacement(
tween(durationMillis = 200)
)
.shadow(1.dp, RoundedCornerShape(8.dp))
.background(Color.White)
.fillMaxWidth()
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.size(50.dp),
painter = painterResource(id = R.drawable.landscape1),
contentScale = ContentScale.FillBounds,
contentDescription = null
)
Spacer(modifier = Modifier.width(10.dp))
Text(it.value, fontSize = 18.sp)
}
}
}
var fromString by remember {
mutableStateOf("7")
}
var toString by remember {
mutableStateOf("3")
}
var animate by remember { mutableStateOf(false) }
if (animate) {
val from = try {
Integer.parseInt(fromString)
} catch (e: Exception) {
0
}
val to = try {
Integer.parseInt(toString)
} catch (e: Exception) {
0
}
animatedSwap(
lazyListState = lazyListState,
items = items,
from = from,
to = to
) {
animate = false
}
}
Row(modifier = Modifier.fillMaxWidth()) {
TextField(
value = fromString,
onValueChange = {
fromString = it
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
TextField(
value = toString,
onValueChange = {
toString = it
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
)
}
Button(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
onClick = {
animate = true
}
) {
Text("Swap")
}
}
}
Edit: Animating with Animatable
Another method for animating is using Animatable with Integer vector.
val IntToVector: TwoWayConverter<Int, AnimationVector1D> =
TwoWayConverter({ AnimationVector1D(it.toFloat()) }, { it.value.toInt() })
val coroutineScope = rememberCoroutineScope()
val animatable = remember { Animatable(0, IntToVector) }
And can be used as
private fun alternativeAnimate(
from: Int,
to: Int,
coroutineScope: CoroutineScope,
animatable: Animatable<Int, AnimationVector1D>,
items: SnapshotStateList<MyData>
) {
val difference = from - to
var currentValue: Int = from
coroutineScope.launch {
animatable.snapTo(from)
animatable.animateTo(to,
tween(350 * abs(difference), easing = LinearEasing),
block = {
val nextValue = this.value
if (abs(currentValue -nextValue) ==1) {
swap(items, currentValue, nextValue)
currentValue = nextValue
}
}
)
}
}
on button click, i'm getting values from TextField fo i convert from String
Button(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth(),
onClick = {
val from = try {
Integer.parseInt(fromString)
} catch (e: Exception) {
0
}
val to = try {
Integer.parseInt(toString)
} catch (e: Exception) {
0
}
alternativeAnimate(from, to, coroutineScope, animatable, items)
}
) {
Text("Swap")
}
Result
I suggest you to get your items from a data class. If your other items does not contain the variables you mentioned you can make them nullable in data class and put a condition checker in your lazycolumn
Like this
data class Items(
val otherItems: Other,
val rankingCurrentPlace: Int?,
val rankingLastPlace: Int?,
val isUser: Boolean?
)
Then you can make a list from this data class and pass it to lazycolumn
LazyColumn{
items(list){
(elements with condition)
}
}

Make last Item of the Compose LazyColumn fill rest of the screen

There is a regular list with some data and LazyColumn for showing it.
LazyColumn {
items (list) { item ->
ListItem(item)
}
}
Simplified ListItem looks like:
#Composable
fun ListItem(item: SomeItem) {
Row(
modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min)
) {
//Some widgets
}
}
I need to set the footer's item (last in list) height to fill screen if there is not enough items for that. It's easy to find last item and provide some flag to ListItem(item: SomeItem, isLast: Boolean), but I don't know how to set the item's height to achieve my goal. Did anyone faced this problem?
It's almost the same question as was asked here about RecyclerView. And the image from that question illustatrates it.
If your items are all same height and you have have info of how tall they are via static height or using Modifier.onSizeChanged{} you can do this by getting height of the LazyColumn using BoxWithConstraints maxHeight or rememberLazyListState().layoutInfo.viewportSize
#Composable
private fun ListComposable() {
val myItems = mutableListOf<String>()
repeat(4) {
myItems.add("Item $it")
}
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.border(2.dp, Color.Cyan)
) {
itemsIndexed(myItems) { index: Int, item: String ->
if (index == myItems.size - 1) {
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.height((maxHeight - 50.dp * (myItems.size - 1)).coerceAtLeast(50.dp))
.background(Color.Green)
)
} else {
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Red)
)
}
}
}
}
}
If you don't know total height until last item you will need to use SubcomposeLayout with list that doesn't contain last item and pass LazyColumn height- (items.size-1) total height as your last item's height.
#Composable
fun DimensionMeasureSubcomposeLayout(
modifier: Modifier = Modifier,
mainContent: #Composable () -> Unit,
dependentContent: #Composable (IntSize, Constraints) -> Unit
) {
SubcomposeLayout(modifier = modifier) { constraints ->
// Subcompose(compose only a section) main content and get Placeable
val mainPlaceables: List<Placeable> = subcompose(SlotsEnum.Main, mainContent)
.map {
it.measure(constraints)
}
// Get max width and height of main component
var maxWidth = 0
var maxHeight = 0
mainPlaceables.forEach { placeable: Placeable ->
maxWidth += placeable.width
maxHeight = placeable.height
}
val maxSize = IntSize(maxWidth, maxHeight)
val dependentPlaceables = subcompose(SlotsEnum.Dependent) {
dependentContent(maxSize, constraints)
}.map {
it.measure(constraints)
}
layout(constraints.maxWidth, constraints.maxHeight) {
dependentPlaceables.forEach { placeable: Placeable ->
placeable.placeRelative(0, 0)
}
}
}
}
enum class SlotsEnum { Main, Dependent }
And use it as
#Composable
private fun SubcomposeExample() {
val myItems = mutableListOf<String>()
repeat(15) {
myItems.add("Item $it")
}
val subList = myItems.subList(0, myItems.size - 1)
val lasItem = myItems.last()
val density = LocalDensity.current
DimensionMeasureSubcomposeLayout(
modifier = Modifier,
mainContent = {
LazyColumn(
modifier = Modifier.fillMaxWidth()
) {
items(subList) { item: String ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Red)
)
}
}
},
dependentContent = { intSize, constraints ->
val lastItemHeight = with(density) {
(constraints.maxHeight - intSize.height).toDp().coerceAtLeast(50.dp)
}
LazyColumn(modifier = Modifier.fillMaxWidth()) {
if (myItems.size > 1) {
items(subList) { item: String ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(Color.Red)
)
}
}
item {
Text(
text = lasItem,
modifier = Modifier
.fillMaxWidth()
.height(lastItemHeight)
.background(Color.Green)
)
}
}
}
)
}

Itemdecoration in Jetpack compose

I am currently in the process of evaluating whether or not we can migrate our rather complex UI to jetpack compose at this stage and I am struggling with the following problem.
I am having an infinite scrolling vertical List of various different conceptual components. Some of them are headers, then there can be some text, some horizontally scrolling (infinite) lists and then there are some grouped components that are also stacked vertically but conceptionally belong to a group.
#Compose
fun MyComplexList() {
LazyColumn {
item {
// some header
}
item {
// some horizontal content
LazyRow {
item {}
}
}
item {
// some other header
}
items(x) {
// Some text for each item
}
}
}
As one can see this thing is rather trivial to do using compose and a lot less code than writing this complex RecyclerView + Adapter...
with one exception: that background gradient, spanning (grouping) the Some infinite list of things component. (the tilted gradient in the image)
In the past (:D) I would use an ItemDecoration on the RecyclerView to draw something across multiple items, but I can't find anything similar to that in Compose.
Does anyone have any idea on how one would achieve this with compose?
After your answer, this is what I understood...
#Composable
fun ListWithGradientBgScreen() {
val lazyListState = rememberLazyListState()
val firstVisibleIndex by remember {
derivedStateOf {
lazyListState.firstVisibleItemIndex
}
}
val totalVisibleItems by remember {
derivedStateOf {
lazyListState.layoutInfo.visibleItemsInfo.size
}
}
val itemsCount = 50
BoxWithConstraints(Modifier.fillMaxSize()) {
ListBg(firstVisibleIndex, totalVisibleItems, itemsCount, maxHeight)
LazyColumn(state = lazyListState, modifier = Modifier.fillMaxSize()) {
item {
Column(
Modifier
.fillMaxWidth()
.background(Color.White)
) {
Text(
text = "Some header",
style = MaterialTheme.typography.h5,
modifier = Modifier.padding(16.dp)
)
}
}
item {
Text(
text = "Some infinite list of things",
style = MaterialTheme.typography.h5,
modifier = Modifier.padding(16.dp)
)
}
items(itemsCount) {
Text(
text = "Item $it",
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 6.dp)
.background(Color.LightGray)
.padding(8.dp)
)
}
}
}
}
and to change the background in according to the background, you can define something like the following:
#Composable
private fun ListBg(
firstVisibleIndex: Int,
totalVisibleItems: Int,
itemsCount: Int,
maxHeight: Dp
) {
val hasNoScroll = itemsCount <= totalVisibleItems
val totalHeight = if (hasNoScroll) maxHeight else maxHeight * 3
val scrollableBgHeight = if (hasNoScroll) maxHeight else totalHeight - maxHeight
val scrollStep = scrollableBgHeight / (itemsCount + 2 - totalVisibleItems)
val yOffset = if (hasNoScroll) 0.dp else -(scrollStep * firstVisibleIndex)
Box(
Modifier
.wrapContentHeight(unbounded = true, align = Alignment.Top)
.background(Color.Yellow)
.offset { IntOffset(x = 0, y = yOffset.roundToPx()) }
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(totalHeight)
.drawBehind {
drawRoundRect(
Brush.linearGradient(
0f to Color.Red,
0.6f to Color.DarkGray,
1.0f to Color.Green,
),
)
}
)
}
}
Here is the result:
One of the options:
Replace:
items(x) {
// Some text for each item
}
with:
item {
Column(modifier = Modifier.border(...).background(...)) { //Shape, color etc...
x.forEach {
// Some text for each item
}
}
}

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