In Jetpack Compose FlowRow, how can I make the elements within the rows stretch the full width of the screen.
If this code is run on a Pixel 8, the first 4 names are on one row which is what I want, but there is white space after the last name (Peter). I basically want the view to dynamically add even spacing around all 4 pieces of text so that the full view width is used.
The 1st image shows what the code is currently doing. The 2nd image shows what I want the code to do (I have manually created this view by specifying the padding for each element in nameOptions
), but want a way for it to be done automatically based on the available width
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun Names() {
var nameOptions = arrayOf("Luke", "Christopher", "Samuel", "Peter", "Johnathan", "James", "Aydan", "Matthew", "Andrew")
Column (
modifier = Modifier.padding(10.dp)
) {
Spacer(modifier = Modifier.height(50.dp))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
for (name in nameOptions) {
Text(
text = name,
softWrap = false,
modifier = Modifier
.border(
width = 1.dp,
color = Color.Black,
shape = RoundedCornerShape(15.dp)
)
.padding(10.dp)
)
}
}
}
}
You could use a custom Layout
. Following I tested and it seemed to work quite well:
@Composable
fun Names() {
var nameOptions = arrayOf(
"Luke",
"Christopher",
"Samuel",
"Peter",
"Johnathan",
"James",
"Aydan",
"Matthew",
"Andrew"
)
Column(
modifier = Modifier.padding(10.dp)
) {
val horizontalItemSpacing = with(LocalDensity.current) { 10.dp.roundToPx() }
val verticalItemSpacing = with(LocalDensity.current) { 10.dp.roundToPx() }
Layout(
content = {
for (name in nameOptions) {
Text(
text = name,
modifier = Modifier
.border(
width = 1.dp,
color = Color.Black,
shape = RoundedCornerShape(15.dp)
)
.padding(10.dp),
// Because default is left aligned
textAlign = TextAlign.Center,
softWrap = false,
)
}
},
modifier = Modifier.fillMaxWidth(),
) { measurables, constraints ->
if (measurables.isEmpty()) return@Layout layout(0, 0) { }
// Firstly get the widths of the elements
val widths = measurables.map { it.minIntrinsicWidth(Int.MAX_VALUE) }
// Fit them in the rows
val fittingWidths = mutableListOf(mutableListOf<Int>()).apply {
// Overflow in next row
var currentRow = first()
for (width in widths) {
val availableWidth =
constraints.maxWidth - currentRow.sum() - currentRow.size * horizontalItemSpacing
if (width > availableWidth && currentRow.isNotEmpty())
currentRow = mutableListOf<Int>().also(::add)
currentRow.add(width.coerceAtMost(constraints.maxWidth))
}
}
val layoutWidth = max(
constraints.minWidth,
fittingWidths.maxOf { row ->
row.sum() + (row.size - 1) * horizontalItemSpacing
},
)
// Distribute remaining space
for (row in fittingWidths) {
val distributeWidth =
(layoutWidth - (row.sum() + (row.size - 1) * horizontalItemSpacing)) / row.size
row.replaceAll { width -> width + distributeWidth }
}
var i = 0
val placeables = fittingWidths
.dropLast(1).map { row ->
row.map { width ->
measurables[i++].measure(constraints.copy(minWidth = width))
}
}
// Last row
.plusElement(fittingWidths.last().map {
measurables[i++].measure(constraints.copy(minWidth = 0))
})
val layoutHeight = placeables.sumOf { row ->
row.maxOf { placeable -> placeable.height }
} + (placeables.size - 1) * verticalItemSpacing
layout(width = layoutWidth, height = layoutHeight) {
var posX = 0
var posY = 0
for (row in placeables) {
val rowHeight = row.maxOf { placeable ->
placeable.placeRelative(posX, posY)
posX += placeable.width + horizontalItemSpacing
placeable.height
}
posX = 0
posY += rowHeight + verticalItemSpacing
}
}
}
}
}
Note that the text has to be manually center aligned.
I'd suggest you to put the layout in a separate composable with horizontal and vertical item spacing as parameters together with modifier
and content
that you can apply to the Layout
.
edit: Added a check at the top of the measure policy to avoid division by zero.