Align two texts by font's ascent instead of baseline in Jetpack Compose

Viewed 517

I know how to align text in Jetpack Compose to the baseline.

But now I would need to align two differently sized texts that follow each other in a Row by the ascent of the larger of these two fonts. I would like to think of this as aligning two texts "by the top baseline" if that makes sense. Modifier.align(Alignment.Top) does not work as it will not align by the ascent but by the layout's top and then the texts are not aligned correctly at the top.

I have tried to look how to do this, but apparently there's no ready made function or modifier for this? I didn't even find a way to access Text's ascent property etc in Compose. So not sure how this would be possible?

Thanks for any hints! :)

Edit: This is what it looks when Alignment.Top is used. But I would like the two texts to align at the top.

enter image description here

4 Answers

All information about text layout can be retrieved with onTextLayout Text argument. In this case you need a line size, which can be retrieved with getLineBottom, and an actual font size, which can be found in layoutInput.style.fontSize.

I agree that it'd be easier if you could use some simple way to do that, so I've starred your feature request, but for now here's how you can calculate it:

onTextLayout = { textLayoutResult ->
    val ascent = textLayoutResult.getLineBottom(0) - textLayoutResult.layoutInput.run {
        with(density) {
            style.fontSize.toPx()
        }
    }
},

Full example of aligning two texts:

val ascents = remember { mutableStateMapOf<Int, Float>() }
val texts = remember {
    listOf(
        "Big text" to 80.sp,
        "Small text" to 20.sp,
    )
}
Row(
    Modifier
        .drawBehind {
            ascents.maxOfOrNull { it.value }?.let {
                drawLine(Color.Red, Offset(0f, it), Offset(size.width, it))
            }
        }
) {
    texts.forEachIndexed { i, info ->
        Text(
            info.first,
            fontSize = info.second,
            onTextLayout = { textLayoutResult ->
                ascents[i] = textLayoutResult.getLineBottom(0) - textLayoutResult.layoutInput.run {
                    with(density) {
                        style.fontSize.toPx()
                    }
                }
            },
            modifier = Modifier
                .alpha(if (ascents.count() == texts.count()) 1f else 0f)
                .layout { measurable, constraints ->
                    val placeable = measurable.measure(constraints)
                    val maxAscent = ascents.maxOfOrNull { it.value } ?: 0f
                    val ascent = ascents[i] ?: 0f
                    val yOffset = if (maxAscent == ascent) 0 else (maxAscent - ascent).toInt()
                    layout(placeable.width, placeable.height - yOffset) {
                        placeable.place(0, yOffset)
                    }
                }
        )
    }
}

Result:

enter image description here

One workaround would be you can adjust y-axis offset modifier according to your need.

Text(text = "Second", modifier = Modifier.offset(x = 0.dp, y = 5.dp))

you can have negative value for offset as well if you like to up your first text according to your need.

in addition to one of the previous answers
ascent and descent

enter image description here

Text(
    modifier = modifier,
    text = text,
    onTextLayout = { result ->
        val layoutInput = result.layoutInput
        val fontSize = with(layoutInput.density) { layoutInput.style.fontSize.toPx() }
        val lineHeight = with(layoutInput.density) { layoutInput.style.lineHeight.toPx() }
        var baseline = result.firstBaseline
        (0 until result.lineCount).forEach { index ->
            val top = result.getLineTop(index)
            val bottom = result.getLineBottom(index)
            val ascent = bottom - fontSize
            val descent = bottom - (baseline - fontSize - top)
            baseline += lineHeight
        }
    }
)
Related