android-jetpack-composerowelement

FlowRow make elements in row stretch to available width


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

[Before1 After

@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)
                )
            }
        }

    }

}

Solution

  • 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.