1

I want to make a composable that holds a an image and a text next to it in a way that the image will scale itself to be as high as the text next to it. I used to do it like that:

@Composable
fun Test(
) {
    Row(
        modifier = Modifier
            .height(IntrinsicSize.Min)
            .width(200.dp),
        horizontalArrangement = Arrangement.Start,
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Image(
            painterResource(id = R.drawable.some_imported_svg),
            contentDescription = null,
            contentScale = ContentScale.Fit,
            modifier = Modifier
                .padding(end = 8.dp)
        )
        Text(
            modifier = Modifier,
            text = "Yum Yum",
            color = MaterialTheme.colors.onBackground,
        )
    }
}

It worked up until I made an update to some of the dependencies in the project. After some investigation, it seems to me that the behavioural change was made in one of the following dependencies updates:

"androidx.compose.ui:ui"
"androidx.compose.material:material"
"androidx.compose.material:material-icons-core"
"androidx.compose.material:material-icons-extended"
"androidx.compose.ui:ui-tooling"
"androidx.compose.ui:ui-tooling-preview"

More specifically, when updating from a version prior to 1.2.0-alpha04 to that version or to a newer one. I.e.

With 1.1.1: With 1.1.1

With 1.4.3: With 1.4.3

Since it is a simple task to do and that change was made long time ago and there were no complains (that I found) about it, my guess is that What I did back then was wrong and just worked and now there is something else that I am missing.

Any help would be highly appreciated! Thanks a lot!

avivmg
  • 369
  • 3
  • 7
  • 1
    by the same size, you mean the same height? – Klitos G. May 17 '23 at 16:28
  • @avivmg, it seems that your text has the default sp size, why don't you just adjust your image's size accordingly ? – yuroyami May 17 '23 at 17:07
  • You are correct in that manner. I had the hope to find a solution that would be a bit more robust like I had before the update. That way I would be able to create many other types of composables with images in them that would just fit to the available space without declaring their dimensions. I.e. in the example above, instead of text, a column of texts. – avivmg May 17 '23 at 17:44

2 Answers2

2

Asking for intrinsics measurements doesn't measure the children twice. Children are queried for their intrinsic measurements before they're measured and then, based on that information the parent calculates the constraints to measure its children with.

https://developer.android.com/jetpack/compose/layouts/intrinsic-measurements#intrinsics-in-action

In the case of intrinsics, they are more of a tentative calculation in order to perform real measuring using the obtained values. Imagine a row with 3 children. In order to make its height match the height of the tallest child, it would need to get the intrinsic measures of all its children, and finally measure itself using the maximum one.

https://newsletter.jorgecastillo.dev/p/introducing-lookaheadlayout

Modifier.onSizeChanged or Modifier.globallyPositioned to get size to effect another Composable has risk of infinite recompositions. If you have Text you can use TextMeasured, as in this question if you know which one of the siblings need to be use as reference you can use Layout if you don't know which one should be bigger or lower you should use SubcomposeLayout.

https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/package-summary#(androidx.compose.ui.Modifier).onSizeChanged(kotlin.Function1)

How to get exact size without recomposition?

For the scope of this question i will post first 2.

With TextMeasurer

val textMeasurer = rememberTextMeasurer()

val text = "Yum Yum"
val textLayoutResult = remember {
    textMeasurer.measure(AnnotatedString(text))
}

val density = LocalDensity.current

val height = with(density) {
    textLayoutResult.size.height.toDp()
}

Row(
    modifier = Modifier
        .border(1.dp, Color.Red)
        .height(height)
        .width(200.dp),
    horizontalArrangement = Arrangement.Start,
    verticalAlignment = Alignment.CenterVertically,
) {
    Image(
        painterResource(id = R.drawable.placeholder),
        contentDescription = null,
        contentScale = ContentScale.Fit,
        modifier = Modifier
            .border(2.dp, Color.Black)
            .padding(end = 8.dp)
    )

    Text(
        modifier = Modifier,
        text = text,
        color = MaterialTheme.colors.onBackground,
    )
}

With Layout that measures Text first and then measures Image with Text's height

@Composable
private fun ImageTextLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        modifier = modifier,
        content = content
    ) { measurables: List<Measurable>,
        constraints: Constraints ->

        require(measurables.size == 2)

        val textPlaceable = measurables.last().measure(constraints = constraints)
        val imagePlaceable = measurables.first().measure(
            constraints.copy(
                minWidth = 0,
                minHeight = textPlaceable.height,
                maxHeight = textPlaceable.height
            )
        )

        val hasFixedWidth = constraints.hasFixedWidth

        val width = if (hasFixedWidth) constraints.maxWidth
        else imagePlaceable.width + textPlaceable.width
        val height = imagePlaceable.height.coerceAtMost(textPlaceable.height)

        layout(width, height) {
            imagePlaceable.placeRelative(0, 0)
            textPlaceable.placeRelative(imagePlaceable.width, 0)
        }
    }
}

Usage

ImageTextLayout(
    modifier = Modifier
        .border(1.dp, Color.Green)
        .width(200.dp),
) {
    Image(
        painterResource(id = R.drawable.placeholder),
        contentDescription = null,
        contentScale = ContentScale.Fit,
        modifier = Modifier
            .border(2.dp, Color.Black)
            .padding(end = 8.dp)
    )

    Text(
        modifier = Modifier,
        text = text,
        color = MaterialTheme.colors.onBackground,
    )
}

Full Demo

@OptIn(ExperimentalTextApi::class)
@Preview
@Composable
fun Test(
) {

    Column {

        Spacer(modifier = Modifier.height(40.dp))

        val textMeasurer = rememberTextMeasurer()

        val text = "Yum Yum"
        val textLayoutResult = remember {
            textMeasurer.measure(AnnotatedString(text))
        }

        val density = LocalDensity.current

        val height = with(density) {
            textLayoutResult.size.height.toDp()
        }

        Row(
            modifier = Modifier
                .border(1.dp, Color.Red)
                .height(height)
                .width(200.dp),
            horizontalArrangement = Arrangement.Start,
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Image(
                painterResource(id = R.drawable.placeholder),
                contentDescription = null,
                contentScale = ContentScale.Fit,
                modifier = Modifier
                    .border(2.dp, Color.Black)
                    .padding(end = 8.dp)
            )

            Text(
                modifier = Modifier,
                text = text,
                color = MaterialTheme.colors.onBackground,
            )
        }

        ImageTextLayout(
            modifier = Modifier
                .border(1.dp, Color.Green)
                .width(200.dp),
        ) {
            Image(
                painterResource(id = R.drawable.placeholder),
                contentDescription = null,
                contentScale = ContentScale.Fit,
                modifier = Modifier
                    .border(2.dp, Color.Black)
                    .padding(end = 8.dp)
            )

            Text(
                modifier = Modifier,
                text = text,
                color = MaterialTheme.colors.onBackground,
            )
        }
    }
}
Thracian
  • 43,021
  • 16
  • 133
  • 222
  • 1
    Thanks for the detailed answer! Not entirely sure but, I believe that the issue lies in the size of the image itself, and not the size of the Text. Pretty cool, thanks for the links and the description! Learned a lot! Those are great options. I was hoping to find some way that will be as easy as it was before the update. Sadly it seems that there isn't any. – avivmg May 17 '23 at 23:38
  • 1
    Yes, you are right it's probably due image not returning intrinsic min height correctly. – Thracian May 18 '23 at 03:48
1

One way to do this is to read the height of your text after it is rendered and update your image height.

Try this:

@Composable
fun Test(
) {
    Row(
        modifier = Modifier
            .height(IntrinsicSize.Min)
            .width(200.dp),
        horizontalArrangement = Arrangement.Start,
        verticalAlignment = Alignment.CenterVertically,
    ) {
        val wantedHeight = remember { mutableStateOf(0) }

        Image(
            painterResource(id = R.drawable.ic_stages),
            contentDescription = null,
            modifier = Modifier
                .padding(end = 8.dp)
                .height(wantedHeight.value.dp)
        )
        Text(
            modifier = Modifier.onGloballyPositioned { coordinates ->
                wantedHeight.value = coordinates.size.height.toDp()
            },
            text = "Yum Yum",
            color = MaterialTheme.colors.onBackground,
        )
    }
}

Please note, forcing recomposition is prone to creating problems, you might end up in a loop of recompositions or have performance problems. In this simple example you won't have a problem, but please consider providing a static height for your image that will fit the text size you are after (taking always into account that the text size might be changed by the user device preferences or accessibility settings)

Klitos G.
  • 806
  • 5
  • 14
  • Thanks a lot for the answer! With tiny changes it works. As you noted though, it adds some new recompositions and complexity. I hoped that I would be able to find a similar way of handling the issue like before the update, but maybe that way doesn't exist anymore. So its either calculating or 'remembering' the other siblings' dimentions or just defining fixed values. Just as a side note, I believe that on swiftui it is just the same as in this post update. – avivmg May 17 '23 at 17:34