How to align items in separate rows in Compose?

Viewed 53

I want to create a Compose view that looks something like this:

|-------------------------------------------|
| "Dynamic text"    Image   "Text "         |
|-------------------------------------------|
| "Dynamic text 2"  Image   "Text"          |
|-------------------------------------------|

Logical way to do it would be to add two Rows inside a Column. Problematic part is that Images inside those Rows must always align while the first text elements length can change.

|-------------------------------------------|
| "Dynamic longer text"    Image   "Text "  |
|-------------------------------------------|
| "Text"                   Image   "Text"   |
|-------------------------------------------|

What is the best option for it? I've thought about using ConstraintLayout but that seems like an overkill for this simple scenario. I have also considered using Columns inside a Row, but that just doesn't feel natural in this case. All ideas welcome!

1 Answers

If aligning Image and Text with static text end of your Composable is okay you can set Modifier.weight(1f) for Text with dynamic text so it will cover the space that is not used by Image and Text with static text.

Column(modifier = Modifier.fillMaxSize()) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    ) {
        Text("Dynamic text", modifier = Modifier.weight(1f))
        Image(
            modifier = Modifier.size(50.dp),
            painter = painterResource(id = R.drawable.landscape1),
            contentDescription = ""
        )
        Text("text")
    }
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
    ) {
        Text("Dynamic longer text", modifier = Modifier.weight(1f))

        Image(
            modifier = Modifier.size(50.dp),
            painter = painterResource(id = R.drawable.landscape1),
            contentDescription = ""
        )
        Text("text")
    }
}

If you want a layout like in first table you should use a Layout. If you don't have any experience with Layout it might be complicated.

Steps to accomplish what's needed

1- First measure your every child Composable with Constraints and get placeable

    val placeables: List<Placeable> = measurables.map { measurable ->
        measurable.measure(constraints)
    }

2- Get maximum width of dynamic texts, this will be our threshold for each row's second element x position, i added a padding but you can add padding to Text if you want to

   // Get the maximum with of first Text on each Row
    val maxDynamicWidth = placeables.filterIndexed { index, _ ->
        index % 3 == 0
    }.maxOf { it.width } + padding

3- Get height of each row. We will use row height for placing every 3 Composables

val rowHeights = mutableListOf<Int>()
var maxHeight = 0

// Get height for each row
placeables.forEachIndexed { index, placeable ->
    maxHeight = if (index % 3 == 2) {
        rowHeights.add(maxHeight)
        0
    } else {
        placeable.height.coerceAtLeast(maxHeight)
    }
}

4- get total height of our Layout, you can use constraints.maxHeight if you want to and then layout every 3 Composables on each row

    val totalHeight = rowHeights.sum()
    var y = 0
    var x = 0

    layout(constraints.maxWidth, totalHeight) {
        placeables.forEachIndexed { index, placeable ->
            if (index % 3 == 0) {
                if (index > 0) y += rowHeights[index / 3]
                placeable.placeRelative(0, y)
                x = maxDynamicWidth
            } else {
                placeable.placeRelative(x, y)
                x += placeable.width

            }
        }
    }

Full implementation

@Composable
private fun MyLayout(
    modifier: Modifier = Modifier,
    paddingAfterDynamicText: Dp = 0.dp,
    content: @Composable () -> Unit
) {

    val padding = with(LocalDensity.current) {
        paddingAfterDynamicText.roundToPx()
    }
    Layout(
        modifier = modifier,
        content = content
    ) { measurables: List<Measurable>, constraints: Constraints ->

        require(measurables.size % 3 == 0)

        val placeables: List<Placeable> = measurables.map { measurable ->
            measurable.measure(constraints)
        }

        // Get the maximum with of first Text on each Row
        val maxDynamicWidth = placeables.filterIndexed { index, _ ->
            index % 3 == 0
        }.maxOf { it.width } + padding

        val rowHeights = mutableListOf<Int>()
        var maxHeight = 0

        // Get height for each row
        placeables.forEachIndexed { index, placeable ->
            maxHeight = if (index % 3 == 2) {
                rowHeights.add(maxHeight)
                0
            } else {
                placeable.height.coerceAtLeast(maxHeight)
            }
        }

        val totalHeight = rowHeights.sum()
        var y = 0
        var x = 0

        // i put Composable on each row to top of the Row
        // (y-placeable.height)/2 places them to center of row
        layout(constraints.maxWidth, totalHeight) {
            placeables.forEachIndexed { index, placeable ->
                if (index % 3 == 0) {
                    if (index > 0) y += rowHeights[index / 3]
                    placeable.placeRelative(0, y)
                    x = maxDynamicWidth
                } else {
                    placeable.placeRelative(x, y)
                    x += placeable.width

                }
            }
        }
    }
}

You can use it as

MyLayout(
    modifier = Modifier.border(3.dp, Color.Red),
    paddingAfterDynamicText = 15.dp
) {

    Text("Dynamic text")
    Image(
        modifier = Modifier.size(50.dp),
        painter = painterResource(id = R.drawable.landscape1),
        contentDescription = ""
    )
    Text("text")


    Text("Dynamic longer text")

    Image(
        modifier = Modifier.size(50.dp),
        painter = painterResource(id = R.drawable.landscape1),
        contentDescription = ""
    )
    Text("text")

}

Result

enter image description here

Related