Implementing a Custom Layout in Jetpack Compose for Dynamic Room Display
This article explains how to create a custom layout in Jetpack Compose that dynamically arranges a grid of fixed‑size room items, centers the content horizontally and vertically when needed, and adds vertical scrolling for overflow, while detailing the measurement and placement logic with full Kotlin code examples.
Goal
The layout should display a list of rooms in a building, with each room having a fixed width and height. The requirements are:
Horizontally calculate how many rooms fit per row and center the row.
Vertically center the whole grid when the total height is less than the screen height.
Allow vertical scrolling when the content exceeds the screen height.
Compose Custom Layout Overview
Compose uses the @Composable Layout function to implement custom layouts. The function receives a content lambda, a modifier , and a measurePolicy . Inside the measure block you measure children, calculate positions, and finally call layout(width, height) { … } to place them.
Key Parameters
modifier : Modifier passed from the caller to affect constraints and appearance.
content : The composable children that will be measured and placed.
measurePolicy : A MeasurePolicy that defines how to measure children and compute the layout size.
Measuring Children
Each child is measured once using the constraints provided by the parent layout. The code creates a new Constraints instance with a minimum of 0 to avoid the child being forced to the parent size when modifiers like fillMaxSize() are used.
@Composable
inline fun Layout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {}Calculating Layout
The algorithm works as follows:
Measure the first child to obtain childWidth and childHeight .
Determine how many children fit in a row ( columns ) by repeatedly subtracting childWidth + space from the maximum width.
Compute the total row width and the horizontal start offset ( edgeStart ) to center the row.
Calculate the number of rows ( rows ) and the total content height ( contentHeight ).
If contentHeight is smaller than the layout height, compute a vertical offset ( edgeTop ) to center the content.
Store the X and Y coordinates for each child in two 2‑D arrays.
Finally call layout(layoutWidth, layoutHeight) { … } and place each child with placeable.placeRelative(x, y) (or place() for LTR).
Adding Vertical Scrolling
When the content height exceeds the available height, the Modifier.verticalScroll() modifier is applied to the custom layout. Because the scrolling modifier makes the maximum height Infinity , the layout height is calculated as the maximum of contentHeight and constraints.minHeight to avoid an infinite layout size.
Full Implementation
@Composable
fun CustomScreen() {
Surface(color = MaterialTheme.colors.background) {
CustomLayout(
modifier = Modifier
.background(Color.Gray)
.fillMaxSize()
.padding(12.dp)
.verticalScroll(rememberScrollState())
) {
for (i in 1..100) {
Box(
modifier = Modifier
.size(67.dp, 36.dp)
.background(color = Color(0xFFFF6633), shape = RoundedCornerShape(2.dp)),
contentAlignment = Alignment.Center
) {
Text(text = "10${i}", fontSize = 16.sp, color = Color.White)
}
}
}
}
}
@Composable
fun CustomLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
Layout(content = content, modifier = modifier) { measurables, constraints ->
val space = 5.dp.roundToPx()
var rows = 0
var columns = 0
var edgeStart = 0
var edgeTop = 0
var contentHeight = 0
var isCalculated = false
val childConstraints = Constraints(0, constraints.maxWidth, 0, constraints.maxWidth)
val placeables = measurables.mapIndexed { index, measurable ->
val placeable = measurable.measure(childConstraints)
if (!isCalculated) {
isCalculated = true
var rowWidth = constraints.maxWidth
val childWidth = placeable.width
val childHeight = placeable.height
while (rowWidth >= childWidth) {
rowWidth -= (childWidth + space)
columns++
}
val lineWidth = columns * childWidth + (columns - 1) * space
edgeStart = (constraints.maxWidth - lineWidth) / 2
rows = (measurables.size + columns - 1) / columns
contentHeight = rows * childHeight + (rows - 1) * space
// allocate position arrays
childX = Array(rows) { IntArray(columns) }
childY = Array(rows) { IntArray(columns) }
}
val row = index / columns
val column = index % columns
childX[row][column] = column * (placeable.width + space) + edgeStart
childY[row][column] = row * (placeable.height + space)
placeable
}
val layoutWidth = constraints.maxWidth
var layoutHeight = max(contentHeight, constraints.minHeight)
if (contentHeight < layoutHeight) {
edgeTop = (layoutHeight - contentHeight) / 2
}
layout(layoutWidth, layoutHeight) {
placeables.forEachIndexed { index, placeable ->
val row = index / columns
val column = index % columns
val x = childX[row][column]
val y = childY[row][column] + edgeTop
placeable.placeRelative(x, y)
}
}
}
}Summary
Compose’s Layout composable enables custom UI arrangements similar to Android ViewGroup, requiring a single measurement pass and explicit placement of children. By calculating column count, row count, and offsets, you can achieve horizontal and vertical centering, and by using Modifier.verticalScroll you can add scrolling when the content exceeds the available space.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.