Hello everyone, I’m implementing a chat bubble (li...
# compose
d
Hello everyone, I’m implementing a chat bubble (like WhatsApp and other messengers) and want to place a status on the same line as the last line of text if there’s enough space. Last line content can be arbitrary, e.g., Markdown, a
Column
with attachments, a
Flow
with reactions, etc. Taking the case of a
Flow
with reactions: I use
onGlobalPositioned
on the last reaction to get its coordinates, store them in a
MutableState
, and compare them with the status width. This works somehow but triggers a double layout pass because
onGlobalPositioned
fires only after placement. Is there a way to get the coordinates of non-direct children in a single layout pass without rewriting standard Composables?
j
Maybe a custom
Layout
? You’d be able to directly query the sizes of all children/grand-children and allows you to perform Pixel perfect placement.
a
Definitely try a custom
Layout
, and maybe with some additional tools like alignment lines.

Self-promotion

that goes into some examples including the badge use case at 11:05 - full code is here
d
Thank you. I’m using a custom Layout, but how can I get the coordinates of indirect child during the first layout pass? Suppose the last composable in the bubble is:
Copy code
FlowRow {
    reactions.forEachIndexed { index, item ->
        Reaction(
            reaction = item,
        )
    }
}
To determine whether there is enough space on the last line of reactions for the status I need coordinates of the last
Reaction
. However, this information only becomes available only after
place
is called in the custom Layout.
j
Why did you need the coordinates of the indirect children? You know the coordinates & bounds of the parent holding those children. You can use that information to determine the X, Y coordinate status line.
d
Well, because I need to know the width of the last line. For example, consider the following layout shown in the image. The
FlowRow
is depicted in green and the
Reactions
in blue. The bubble layout only knows the bounds of the
FlowRow
, but to place the status correctly I need the coordinates of
Reaction #6
.
j
The only way I can think of without a second "layout" pass would be to also manually implement the "Flow" layout.
🙏 1
a
I'd take a look at alignment lines - they'r
d
Thanks for help, I will take a look at alignment lines
I have checked alignment lines and it looks like it still requires to rewrite standard composables like
FlowRow
a
There shouldn't need to be any need to rewrite
FlowRow
- alignment lines can propagate up through other layouts. Something like this seems close to what you're looking for:
Copy code
private val LastItemTop = HorizontalAlignmentLine(::minOf)
private val LastItemBottom = HorizontalAlignmentLine(::maxOf)
private val LastItemEnd = VerticalAlignmentLine(::maxOf)

@Preview(widthDp = 100)
@Preview(widthDp = 120)
@Preview(widthDp = 140)
@Preview(widthDp = 160)
@Preview(widthDp = 180)
@Preview(widthDp = 200)
@Composable
internal fun FlowRowWithBadge(modifier: Modifier = Modifier) {
    Layout(
        modifier = modifier.fillMaxWidth(),
        content = {
            FlowRow(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.spacedBy(8.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp),
            ) {
                repeat(10) { index ->
                    val isLast = index == 9
                    Button(
                        onClick = {},
                        contentPadding = PaddingValues(0.dp),
                        modifier = Modifier
                            .size(32.dp)
                            .then(
                                if (isLast) {
                                    Modifier.layout { measurable, constraints ->
                                        val placeable = measurable.measure(constraints)
                                        layout(
                                            placeable.width,
                                            placeable.height,
                                            alignmentLines = mapOf(
                                                LastItemTop to 0,
                                                LastItemBottom to placeable.height,
                                                LastItemEnd to placeable.width,
                                            ),
                                        ) {
                                            placeable.place(0, 0)
                                        }
                                    }
                                } else {
                                    Modifier
                                }
                            ),
                    ) {
                        Text(text = "$index", style = MaterialTheme.typography.bodySmall)
                    }
                }
            }
            Text(
                text = "Status: Online",
                modifier = Modifier
                    .background(Color.Green)
                    .padding(horizontal = 8.dp, vertical = 4.dp),
                style = MaterialTheme.typography.labelLarge,
            )
        },
    ) { measurables, constraints ->
        val flowRowMeasurable = measurables[0]
        val badgeMeasurable = measurables[1]

        val flowRowPlaceable = flowRowMeasurable.measure(constraints)
        val badgePlaceable = badgeMeasurable.measure(constraints.copy(minWidth = 0))

        val lastItemTop = flowRowPlaceable[LastItemTop]
        val lastItemBottom = flowRowPlaceable[LastItemBottom]
        val lastItemEnd = flowRowPlaceable[LastItemEnd]

        val spacing = 8.dp.roundToPx()
        val canFitOnLastLine = if (lastItemBottom != AlignmentLine.Unspecified && lastItemEnd != AlignmentLine.Unspecified) {
            val remainingWidth = constraints.maxWidth - lastItemEnd - spacing
            remainingWidth >= badgePlaceable.width
        } else {
            false
        }

        val width = constraints.maxWidth
        val height = if (canFitOnLastLine) {
            max(flowRowPlaceable.height, lastItemTop + badgePlaceable.height)
        } else {
            flowRowPlaceable.height + spacing + badgePlaceable.height
        }

        layout(width, height) {
            flowRowPlaceable.place(0, 0)
            if (canFitOnLastLine) {
                badgePlaceable.place(
                    x = width - badgePlaceable.width,
                    y = max(lastItemTop, lastItemBottom - badgePlaceable.height),
                )
            } else {
                badgePlaceable.place(
                    x = width - badgePlaceable.width,
                    y = flowRowPlaceable.height + spacing,
                )
            }
        }
    }
}