Search code examples
androidkotlinandroid-jetpack-compose

How to vertically align a composable to the first line of a text composable in jetpack compose


I have the following jetpack compose layout (I added .border so that the view bounds are visible):

@Composable
fun ExpandableCard(
    expanded: Boolean
) {
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(Dimens.commonMdSpacing),
            horizontalArrangement = Arrangement.Start
        ) {
            Icon(
                imageVector = if (expanded)
                    Icons.Default.RemoveCircleOutline
                else
                    Icons.Default.AddCircleOutline,
                contentDescription = null,
                tint = MaterialTheme.colorScheme.primary,
                modifier = Modifier
                    .size(22.dp)
                    .border(1.dp, Color.Black)
            )
            Text(
                text = "Some really really really really really long title",
                fontWeight = FontWeight.Bold,
                fontSize = 18.sp,
                modifier = Modifier
                    .padding(start = 16.dp)
                    .border(1.dp, Color.Black)
            )
        }
}

enter image description here

I want the Icon to be aligned to the first line of the Text. This looks good when the font scale of the device is 1 (the bottom preview "Standard").
But the upper preview is a font scale of 2.5x and the icon is no longer aligned to the first line of text. Can this be achieved? preferably without complex view measurements


Solution

  • There is way which involves indirect view measurement. Text composable provide a onTextLayout callback which gives the information about line top and line bottom, also there is information about first line's baseline, I have not used it in this solution but you can give it a try as well.

    Also if you need to use border then count the border height as well

    @Composable
    fun ExpandableCard(
        expanded: Boolean
    ) {
    
        val iconSize = 22f
        Row(
            modifier = Modifier
                .fillMaxWidth().wrapContentHeight()
                .padding(12.dp),
            horizontalArrangement = Arrangement.Start
        ) {
            var topPadding by remember {
                mutableStateOf(0f)
            }
            Box(modifier = Modifier.padding(top = 0f.coerceAtLeast(topPadding.toDp(LocalContext.current) - iconSize/2).dp)) {
                Icon(
                    imageVector = if (expanded)
                        Icons.Default.Refresh
                    else
                        Icons.Default.Add,
                    contentDescription = null,
                    tint = MaterialTheme.colorScheme.primary,
                    modifier = Modifier
                        .size(iconSize.dp)
                        .border(1.dp, Color.Black)
    
                )
            }
            Text (
                text = "Some really really really really really long title",
                fontWeight = FontWeight.Bold,
                fontSize = 18.sp,
                modifier = Modifier
                    .padding(start = 16.dp)
                    .border(1.dp, Color.Black),
                onTextLayout = {
                   topPadding = (it.getLineTop(0) + it.getLineBottom(0))/2
                }
            )
        }
    }
    
    fun Float.toDp(context: Context): Float = this / context.resources.displayMetrics.density
    

    enter image description here