I have the following code:
#Composable
fun Home(navController: NavHostController) {
val itemsList = (1..100).toList()
val itemsIndexedList = listOf("A", "B", "C")
val itemModifier = Modifier.wrapContentSize()
LazyVerticalGrid(
columns = GridCells.Fixed(5)
) {
items(itemsList) {
Column(modifier = Modifier.border(width = 1.dp, color = Color.Blue)) {
Row{
Icon(
painter = painterResource(R.drawable.ic_image),
contentDescription = null,
modifier = Modifier.fillMaxWidth()
)
}
Row{
Text("$it")
}
}
}
itemsIndexed(itemsIndexedList) { index, item ->
Text("Item at index $index is $item", itemModifier)
}
}
}
But some components are not found when I use UiAutomator.
According to the structure of my code, it should be like this:
--Column
----Row
------Icon
----Row
------Text
But Column, Row, and Icon cannot be found, only Text is found.
I expected the Icon width to be the same as the parent GridItem's width, but it isn't. I need to check the width of the GridItem and the width of the Icon, what should I do?
Related
i have a Compose LazyColumnList which shows the content (from a List) in single cards. I want to highlight the selected card only. Now when i select another card, the first card stays highlighted.
Here is the code:
...
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(
items = sql_quran,
itemContent = { SurahListItem(quran_einzeln = it, navController)
})
}
#Composable
fun SurahListItem(
quran_einzeln: ItemData_quran,
navController: NavController,
) {
var selectedCard by remember { mutableStateOf(false) }
var cardColor = if (selectedCard) Blue else White;
// var cardColor= if (selectedCard) LightGray else White
Card(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp).fillMaxWidth().fillMaxSize(),
elevation = 2.dp,
shape = RoundedCornerShape(corner = CornerSize(16.dp)),
backgroundColor = cardColor,
onClick = {
selectedCard=!selectedCard
}
)
All state changes are happening locally within your SurahListItem and nothing tells your LazyColumn to perform any updates to its item children every time you click one of them.
I can't compile your entire code, so I just assumed some parts of it resulting to the codes below. I hoisted the "selection state" that you want one level above from your SurahListItem and have the composable that contains the LazyColumn do the updates.
You can copy-and-paste all of these in a single .kt file and you'll be able to run it separately from some root composable screen/content.
data class ItemData_quran(val content: String)
#Composable
fun MyCards( sql_quran: List<ItemData_quran>) {
var selectedItem by remember {
mutableStateOf<ItemData_quran?>(null)
}
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(
items = sql_quran,
itemContent = { surah ->
SurahListItem(
quran_einzeln = surah,
setSelected = selectedItem?.let {
surah.content == it.content
} ?: false
) {
selectedItem = it
}
}
)
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun SurahListItem(
quran_einzeln: ItemData_quran,
setSelected: Boolean,
isSelected: (ItemData_quran?) -> Unit
) {
val cardColor = if (setSelected) Blue else White
Card(
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 8.dp)
.wrapContentSize(),
elevation = 2.dp,
shape = RoundedCornerShape(corner = CornerSize(16.dp)),
backgroundColor = cardColor,
onClick = {
isSelected(if(!setSelected) quran_einzeln else null)
}
) {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(text = quran_einzeln.content)
}
}
}
Usage:
MyCards(
listOf(
ItemData_quran("Item 1"),
ItemData_quran("Item 2"),
ItemData_quran("Item 3"),
ItemData_quran("Item 4"),
ItemData_quran("Item 5"),
ItemData_quran("Item 6"),
ItemData_quran("Item 7"),
)
)
Goal
I want to fit as many items as possible to available space and add "+{num}" badge to the end to indicate if there are more items left. Something like below image.
Problem
Since number and size of the items (i.e chips in this case) and also available space are not known beforehand it is difficult to know exactly how many chips would fit. Additionally, compose measures children only once and lays out right away.
What I've tried
I tried the following approach but it is not quite there yet.
#Composable
fun MainScreen() {
Column {
val states = arrayOf(
"NY", "CA", "NV", "PA", "AZ", "AK", "NE", "CT", "CO", "FL", "IL", "KS", "WA"
)
var chipCount by remember { mutableStateOf(0) }
Row(
modifier = Modifier
.padding(horizontal = 16.dp)
.wrapContentHeight(),
verticalAlignment = Alignment.CenterVertically
) {
ChipRow(
modifier = Modifier
.padding(end = 4.dp)
.weight(1f, fill = false),
onPlacementComplete = { chipCount = it }
) {
for (state in states) {
Chip(
modifier = Modifier
.padding(4.dp)
.wrapContentSize(), text = state
)
}
}
Text(text = "+${states.size - chipCount}", style = MaterialTheme.typography.h6)
}
}
}
#Composable
fun Chip(
text: String,
modifier: Modifier = Modifier
) {
Surface(
color = Color.LightGray,
shape = CircleShape,
modifier = modifier
) {
Text(
text = text,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.body1,
modifier = Modifier.padding(6.dp)
)
}
}
#Composable
fun ChipRow(
modifier: Modifier = Modifier,
onPlacementComplete: (Int) -> Unit,
content: #Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) };
var counter = 0
layout(constraints.maxWidth, constraints.maxHeight) {
var xPosition = 0
for (placeable in placeables) {
if (xPosition + placeable.width > constraints.maxWidth) break
placeable.placeRelative(x = xPosition, 0)
xPosition += placeable.width
counter++
}
onPlacementComplete(counter)
}
}
}
First of all it looks hacky rather than a legit solution. Secondly, it doesn't work as intended. The output is like below.
Request
I went through official guides for custom layouts and did many google searches. But couldn't come up with anything better. Could you help with this situation? Reference links, pointing errors, etc., anything is appreciated.
Thanks in advance!
You've passed your max constraint values (layout(constraints.maxWidth, constraints.maxHeight)), that's why your view took whole screen.
You need to pass the needed view size instead, for example like this:
data class Item(val placeable: Placeable, val xPosition: Int)
val items = mutableListOf<Item>()
var xPosition = 0
for (placeable in placeables) {
if (xPosition + placeable.width > constraints.maxWidth) break
items.add(Item(placeable, xPosition))
xPosition += placeable.width
}
layout(
width = items.last().let { it.xPosition + it.placeable.width },
height = items.maxOf { it.placeable.height }
) {
items.forEach {
it.placeable.place(it.xPosition, 0)
}
onPlacementComplete(items.count())
}
Result:
i am not too sure either but i think you need to state the column and row width first to make it centervertically.
and i dont think u need 1f in weight, instead 0.8f would be better i believe.
Column (
modifier = Modifier.fillMaxWidth()
){
val states = arrayOf(
"NY", "CA", "NV", "PA", "AZ", "AK", "NE", "CT", "CO", "FL", "IL", "KS", "WA"
)
var chipCount by remember { mutableStateOf(0) }
Row(
modifier = Modifier
.padding(horizontal = 16.dp)
.wrapContentHeight()
.modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
ChipRow(
modifier = Modifier
.padding(end = 4.dp)
.weight(0.8f, fill = false),
onPlacementComplete = { chipCount = it }
) {
for (state in states) {
Chip(
modifier = Modifier
.padding(4.dp)
.wrapContentSize(), text = state
)
}
}
Text(text = "+${states.size - chipCount}", style = MaterialTheme.typography.h6)
}
}
or i would prefer you use constraintlayout instead. here
I need to implement multiselect for LazyList, which will also change appBar content when list items are long clicked.
For ListView we could do that with just setting choiceMode to CHOICE_MODE_MULTIPLE_MODAL and setting MultiChoiceModeListener.
Is there a way to do this using Compose?
Since you're in control of the whole state in jetpack compose, you can easily create your own multi-select mode. This is an example.
First I created a ViewModel
val dirs: LiveData<DirViewState> = {
DirViewState.Content(dirRepository.targetFolders)}.asFlow().asLiveData()
val all: LiveData<DirViewState> = {
DirViewState.Content(dirRepository.allFolders)
}.asFlow().asLiveData()
val createFolder = mutableStateOf(false)
val refresh = mutableStateOf(false)
val enterSelectMode = mutableStateOf(false)
val selectedAll = mutableStateOf(false)
val selectedList = mutableStateOf(mutableListOf<String>())
fun updateList(path: String){
if(path in selectedList.value){
selectedList.value.remove(path)
}else{
selectedList.value.add(path)
}
}
}
Usage
Card(modifier = Modifier
.width(100.dp)
.height(120.dp)
.padding(8.dp)
.pointerInput(Unit) {
detectTapGestures(
onLongPress = { **viewModel.enterSelectMode.value = true** },
onTap = {
if (viewModel.enterSelectMode.value) {
viewModel.enterSelectMode.value = false
}
}
)
}
,
shape = MaterialTheme.shapes.medium
) {
Image(painter = if (dirModel.dirCover != "") painter
else painterResource(id = R.drawable.thumbnail),
contentDescription = "dirThumbnail",
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(),
contentScale = ContentScale.FillHeight)
**AnimatedVisibility(
visible = viewModel.enterSelectMode.value,
enter = expandIn(),
exit = shrinkOut()
){
Row(verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.Start) {
Checkbox(checked = isSelected.value, onCheckedChange = {
isSelected.value = !isSelected.value
viewModel.updateList(dirModel.dirPath)
})
}
}**
}
Add selected field to some class representing the item. Then just compose proper code based on that field. In compose you don't have to look for some LazyColumn flag or anything like that. You are in control of the whole state of the list.
The same can be said about the AppBar, you can do a simple if there, like if (items.any { it.selected }) // display button
You can simply handle this by the below code:
Fore example This is your list:
LazyColumn {
items(items = filters) { item ->
FilterItem(item)
}
}
and this is your list item View:
#Composable
fun FilterItem(filter: FilterModel) {
val (selected, onSelected) = remember { mutableStateOf(false) }
Card(
shape = RoundedCornerShape(8.dp),
modifier = Modifier
.padding(horizontal = 8.dp)
.border(
width = 1.dp,
colorResource(
if (selected)
R.color.black
else
R.color.white
),
RoundedCornerShape(8.dp)
)) {
Text(
modifier = Modifier
.toggleable(
value =
selected,
onValueChange =
onSelected
)
.padding(8.dp),
text = filter.text)
}
}
and that's it use tooggleable on your click component modifier like here I did on text.
I am trying to work with the compose lazy column, i want each item of my lazy column to have the maximum height , I tried the below code but it is not giving the expected result
Code
val itemsList: List by mainScreenViewModel.portfolioItems.observeAsState(listOf())
Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.matchParentSize()) {
LazyColumn(
content = {
itemsIndexed(itemsList) { index, item ->
Text(text = "Hello", modifier = Modifier.fillMaxHeight())
}
},
)
}
}
fillMaxHeight()/fillMaxWidth() do not work correctly for vertical and horizontal scrolling list respectively. Instead we can use fillParentHeight() and fillParentWidth() respectively provided in LazyItemScope.
Sample Code:
LazyColumn(
modifier = modifier.fillMaxHeight()) {
items(count = 3) {
Column(modifier = Modifier.fillParentMaxHeight()) {
Text(
text = "Item $it",
modifier = Modifier.fillMaxWidth()
)
Divider(Modifier.height(2.dp))
}
}
}
Refer for more details: https://developer.android.com/reference/kotlin/androidx/compose/foundation/lazy/LazyItemScope
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