Related
I want that my screen adapts the layout based on the screen size. I use a basic list and want that on a phone there is one list item for each row and on a tablet in landscape there should be two list items each row.
i got this code of a example from google, but this does not work for list sizes (size % 2 != 0) because the last item would be stretched to the screen width
#Composable
fun PlanScreen(modifier: Modifier = Modifier, widthSizeClass: WindowWidthSizeClass, items: List<String>) {
val numColumns = if(widthSizeClass == WindowWidthSizeClass.Expanded) 2 else 1
Column(modifier = modifier.fillMaxSize()) {
LazyColumn(modifier = modifier
.fillMaxSize()
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally) {
items(plan.chunked(numColumns)) {
Row {
it.forEach {
ListItemTwoLine(modifier = Modifier.weight(1f), title = it, supportingText = it, icon = R.drawable.baseline_edit_24)
}
}
}
}
}
}
You might try dividing the list items into chunks based on the number of columns.The idea is to chunk the list into smaller sublists of numColumns length, that can be achieved by plan.chunked(numColumns).
However, if the number of items in the list is not evenly divisible by numColumns , the last chunk may have fewer items. In this case, you can add a condition to handle the case where the last chunk has only one item, and display it in a separate column to prevent stretching.
The following code can be used to achieve this:
#Composable
fun PlanScreen(modifier: Modifier = Modifier, widthSizeClass: WindowWidthSizeClass, items: List<String>) {
val numColumns = if(widthSizeClass == WindowWidthSizeClass.Expanded) 2 else 1
Column(modifier = modifier.fillMaxSize()) {
items.chunked(numColumns).forEachIndexed { index, chunk ->
Row {
chunk.forEach {
ListItemTwoLine(modifier = Modifier.weight(1f), title = it, supportingText = it, icon = R.drawable.baseline_edit_24)
}
if (index == items.size / numColumns && items.size % numColumns != 0) {
Spacer(Modifier.weight(1f))
}
}
}
}
}
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
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
}
}
}
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()
}
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