Grid Layout with equally height rows in Jetpack Compose

Viewed 1917

Jetpack Compose:

I want to create a grid layout like UI with equally height rows, but I can't find a function for getting the current usable screen size (The application should look like that)

How the result should look like:

Have a look at the link mentioned above

What I have tried:

I tried to use Modifier.fillMaxHeight() inside of LazyVerticalGrid for generating equally sized rows, but it didn't work. Beside of that, I also had the idea of setting the height manually for every row item, but I couldn't find a function for getting the usable screen size in jetpack compose (I then would have divided the screen height by the number of rows).

val numbers = (0..34).toList()

LazyVerticalGrid(
    cells = GridCells.Fixed(7)

) {
    items(numbers.count()) {
        Column(horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(text = "#"+numbers.get(it))
        }
    }
}
3 Answers

You can use the aspectRatio modifier:

LazyVerticalGrid(
    columns = GridCells.Adaptive(100.dp),
) {
    items(100) {
        Box(
            modifier = Modifier.aspectRatio(1f)
        ) { }
    }
}

Full example:

enter image description here


@Composable
fun Body(modifier: Modifier) {
    Column (
        modifier = modifier,
    ) {
        Text(
            text = "Without aspect ratio",
            modifier = Modifier.padding(12.dp),
            style = MaterialTheme.typography.h5,
        )
        LazyVerticalGrid(
            columns = GridCells.Adaptive(100.dp),
            modifier = Modifier.weight(1f),
        ) {
            items(100) {
                Surface (
                    modifier = Modifier
                        .padding(12.dp),
                    color = Color(0xff2187bb),
                ) {
                    RandomSizeBox()
                }
            }
        }
        Divider()
        Text(
            "With aspect ratio",
            modifier = Modifier.padding(12.dp),
            style = MaterialTheme.typography.h5,
        )
        LazyVerticalGrid(
            columns = GridCells.Adaptive(100.dp),
            modifier = Modifier.weight(1f),
        ) {
            items(100) {
                Surface (
                    modifier = Modifier
                        .aspectRatio(1f)
                        .padding(12.dp),
                    color = Color(0xffbb2187),
                ) {
                    RandomSizeBox()
                }
            }
        }
    }
}

@Composable
fun RandomSizeBox() {
    Box(modifier = Modifier.height((10 + Math.random() * 90).dp))
}

You can use subcompose layout to precalculate the max height of the biggest view, and then use that height for all of the LazyVerticalGrid items.

For example:

example

How does this work?

SubcomposeLayout gives you constraints, i.e. the max width and height the view could be. You can use this to calculate how many views will fit for a given min width. I will use 128 dp:

val longestText = exampleStringList.maxBy { it.length }
val maxWidthDp = constraints.maxWidth/1.dp.toPx()
val width = maxWidthDp/floor(maxWidthDp/128)

Then you can measure the height of a test view, in my case CategoryButton passing the calculated width from above to the modifier:

val measuredHeight = subcompose("viewToMeasure") {
        CategoryButton(
            longestText,
            modifier = Modifier.width(width.dp)
        )
    }[0]
        .measure(Constraints()).height.toDp()

Next you compose your actual LazyVerticalGrid, ensuring that the items use the height you calculated above:

val contentPlaceable = subcompose("content") {
        LazyVerticalGrid(
            columns = GridCells.Adaptive(minSize = 128.dp),
            verticalArrangement = Arrangement.spacedBy(12.dp),
            horizontalArrangement = Arrangement.spacedBy(12.dp),
        ) {
            items(DataStore.categories) { category ->
                CategoryButton(
                    category,
                    onClick = { onCategorySelected(category) },
                    modifier = Modifier.height(measuredHeight)
                )
            }
        }
    }[0].measure(constraints)

And finally, layout the LazyVerticalGrid:

layout(contentPlaceable.width, contentPlaceable.height) {
        contentPlaceable.place(0, 0)
    }

Full code:

SubcomposeLayout { constraints ->
        val longestText = DataStore.categories.maxBy { it.length }
        val maxWidthDp = constraints.maxWidth/1.dp.toPx()
        val width = maxWidthDp/floor(maxWidthDp/128)

        val measuredHeight = subcompose("viewToMeasure") {
            CategoryButton(
                longestText,
                modifier = Modifier.width(width.dp)
            )
        }[0]
            .measure(Constraints()).height.toDp()

        val contentPlaceable = subcompose("content") {
            LazyVerticalGrid(
                columns = GridCells.Adaptive(minSize = 128.dp),
                verticalArrangement = Arrangement.spacedBy(12.dp),
                horizontalArrangement = Arrangement.spacedBy(12.dp),
            ) {
                items(DataStore.categories) { category ->
                    CategoryButton(
                        category,
                        onClick = { onCategorySelected(category) },
                        modifier = Modifier.height(measuredHeight)
                    )
                }
            }
        }[0].measure(constraints)
        layout(contentPlaceable.width, contentPlaceable.height) {
            contentPlaceable.place(0, 0)
        }
    }
Related