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

4

9

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

Hobart answered 30/12, 2021 at 12:51 Comment(1)
I created a bug about this missing feature to Google. issuetracker.google.com/issues/212634248Hobart
E
12

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

Ermina answered 6/1, 2022 at 4:7 Comment(6)
Thanks Philip for the answer! I really appreciate you taking the time and making a working sample out of this! This indeed seems to work fine - I will adopt your solution :) This is a working solution - which is nice - but hopefully the Jetpack Compose team can provide this out of the box. And I didn't know how to get the ascent, so thanks for that also!Hobart
@Pylyp Hi sir! Hope all going well!! I read several answers from you in SO about this topic (onTextLayout / paddingFromBaseLine etc.) I was wondering if there is any stuff can you please share? I don't think I saw them from Android developer site. Thank you!Langan
@Langan I don't have, I learn everything by inspecting the API. You can check some other answers of mine using this searchErmina
@PylypDukhov Awesome!! Thank you! Have a nice day (and stay safe!!)!!!Langan
Sorry but your code doesn't work for me Maybe because of the library updatesGoodman
Update: The issue is in my custom font. Each font has their own baselines. That's why it doesn't workGoodman
H
1

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
        }
    }
)
Hind answered 23/9, 2022 at 10:20 Comment(0)
D
0

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.

Demilitarize answered 30/12, 2021 at 13:49 Comment(2)
Yes, that would be one option. But feels hackish to me and the value would depend on the font used. This would be the last resort and workaround that I would implement if there is no proper solution for this in Compose. Which to be honest would be a surprise if it does not exist. And I would probably file a bug for it.Hobart
If your first Text component will be another size then offset won't be correctGoodman
S
-1

Another option is to use ConstraintLayout. You can simply constrain the tops of the two texts https://developer.android.com/jetpack/compose/layouts/constraintlayout

Spangler answered 19/7, 2022 at 10:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.