https://kotlinlang.org logo
#compose
Title
# compose
j

Johan Reitan

03/28/2022, 11:35 AM
I have a
Row
component that has two slots for text. Both texts can be of varying length, and I’m having a hard time getting the component to balance the text’s width so that the longest text takes the rest of the available space, and also weigh them equally if both texts are long. The closest I’ve been able to get is using equal values of
Modifier.weight()
, but that does not solve the case where one text is longer than the other. The first image is using
weight()
, and the second is what I’m trying to achieve. Code and more examples in thread 🧵
The following are examples when using
weight
with this code:
Copy code
Row(
    Modifier
        .fillMaxWidth()
        .padding(8.dp),
) {
    Text(
        modifier = Modifier.weight(1f),
        text = text1,
    )

    Spacer(Modifier.width(8.dp))

    Text(
        modifier = Modifier.weight(1f),
        text = text2,
        textAlign = TextAlign.End,
    )
}
Only the case where both texts are long produce what I’m trying to achieve.
Also played around with
IntrinsicSize
, and that produces the desired result if the first text is short, otherwise the first text takes all the available space:
Copy code
Row(
    Modifier
        .fillMaxWidth()
        .padding(8.dp),
) {
    Text(
        modifier = Modifier.width(IntrinsicSize.Max),
        text = text1,
    )

    Spacer(Modifier.weight(1f))

    Spacer(Modifier.width(8.dp))

    Text(
        modifier = Modifier.width(IntrinsicSize.Max),
        text = text2,
        textAlign = TextAlign.End,
    )
}
This seems like such a simple case, so I’m sure I’ve missed something obvious 🙂
s

Saiedmomen

03/28/2022, 11:47 AM
what does "long" mean?
j

Johan Reitan

03/28/2022, 11:49 AM
Not much longer than what you see in these previews. The point is they are long enough so that they need to break into 2 or 3 lines
s

Saiedmomen

03/28/2022, 12:07 PM
Easiest way would be to calc the weights based on text lengths. There might be a more clever way though
a

Albert Chang

03/28/2022, 12:10 PM
This is definitely not a simple case but you can achieve it using a custom layout in which you query the intrinsic sizes of the texts and then dicide how to allocate space.
2
j

Johan Reitan

03/28/2022, 12:15 PM
@Saiedmomen Thanks for the suggestion, but the text length doesn’t take into account line breaks, so the smaller text will still be squished… @Albert Chang I’m starting to realise this now. I’ll look into using a custom layout for this 👍
👍 1
This is as far as I got today, but it’s not much of an improvement:
Copy code
Layout(
    modifier = Modifier
        .fillMaxWidth()
        .padding(8.dp),
    content = {
        Text(
            text = text1,
        )

        Text(
            text = text2,
            textAlign = TextAlign.End,
        )
    }
) { measurables, constraints ->
    val minIntrinsicWidths = measurables.map { measurable ->
        measurable.minIntrinsicWidth(constraints.minHeight)
    }
    val totalWidth = minIntrinsicWidths.sum().toFloat()
    val spacing = 8.dp.roundToPx()
    val spacingCompensation = (measurables.size - 1) * spacing / measurables.size
    val placeables = measurables.mapIndexed { index, measurable ->
        measurable.measure(
            Constraints.fixedWidth((minIntrinsicWidths[index] / totalWidth * constraints.maxWidth).roundToInt() - spacingCompensation)
        )
    }

    layout(constraints.maxWidth, placeables.maxOf { it.height }) {
        var xPosition = 0
        placeables.forEachIndexed { index, placeable ->
            if (index > 0) {
                xPosition += spacing
            }
            placeable.placeRelative(x = xPosition, y = 0)
            xPosition += placeable.width
        }
    }
}
The closest I’ve been able to get is using ConstraintLayout:
Copy code
ConstraintLayout(
    modifier = Modifier
        .fillMaxWidth()
        .padding(8.dp),
) {
    val (title, value) = createRefs()
    val chain =
        createHorizontalChain(title, value, chainStyle = ChainStyle.SpreadInside)
    constrain(chain) {
        start.linkTo(parent.start)
        end.linkTo(parent.end)
    }
    Text(
        modifier = Modifier.constrainAs(title) {
            top.linkTo(<http://parent.top|parent.top>)
            width = Dimension.preferredWrapContent.atLeast(100.dp)
        },
        text = text1,
    )
    Text(
        modifier = Modifier
            .constrainAs(value) {
                top.linkTo(<http://parent.top|parent.top>)
                width = Dimension.preferredWrapContent
            }
            .padding(start = 8.dp),
        text = text2,
    )
}
The only problem with this solution is that
Dimension.preferredWrapContent
doesn’t seem to work as intended. If both texts are long, the second text seems to take precedence, squeezing the first text away, instead of weighing them equally. I added
.atLeast(100.dp)
so that it doesn’t completely disappear.
a

Albert Chang

03/29/2022, 10:53 AM
Wrote a quick sample. Is this what you want?
Copy code
Layout(content = {
    Text(text = "Text1")
    Text(text = "Text2", textAlign = TextAlign.Right)
}) { measurables, constraints ->
    val first = measurables[0]
    val second = measurables[1]
    val firstWidth = first.maxIntrinsicWidth(constraints.maxHeight)
    val secondWidth = second.maxIntrinsicWidth(constraints.maxHeight)
    val totalWidth = constraints.maxWidth - 8.dp.roundToPx()
    val halfWidth = totalWidth / 2
    val firstConstraints: Constraints
    val secondConstraints: Constraints
    if ((firstWidth <= halfWidth && secondWidth <= halfWidth) ||
        (firstWidth > halfWidth && secondWidth > halfWidth)
    ) {
        firstConstraints = constraints.copy(minWidth = halfWidth, maxWidth = halfWidth)
        secondConstraints = firstConstraints
    } else if (firstWidth > halfWidth) {
        firstConstraints = constraints.copy(
            minWidth = totalWidth - secondWidth,
            maxWidth = totalWidth - secondWidth
        )
        secondConstraints = constraints.copy(
            minWidth = secondWidth,
            maxWidth = secondWidth
        )
    } else {
        firstConstraints = constraints.copy(
            minWidth = firstWidth,
            maxWidth = firstWidth
        )
        secondConstraints = constraints.copy(
            minWidth = totalWidth - firstWidth,
            maxWidth = totalWidth - firstWidth
        )
    }
    val firstPlaceable = first.measure(firstConstraints)
    val secondPlaceable = second.measure(secondConstraints)
    layout(constraints.maxWidth, max(firstPlaceable.height, secondPlaceable.height)) {
        firstPlaceable.placeRelative(0, 0)
        secondPlaceable.placeRelative(constraints.maxWidth - secondPlaceable.width, 0)
    }
}
j

Johan Reitan

03/29/2022, 11:02 AM
Yes, this seems to solve the problem perfectly! Thank you so much @Albert Chang!
👍 1
s

Stylianos Gakis

04/21/2022, 7:10 AM
Thanks a lot for this Albert! I had a use case exactly like this, only with the extra need of not having the two texts go super close to each other, so I made a couple of changes. Thanks for sharing this here, served as a nice snippet to start from. I also changed the inputs to composables as my use case needed them to sometimes for example have different alpha, or boldness etc, and I thought it’d make sense to just pass them as slots. With the only a bit less convenient parts being that I now have to pass the
TextAlign
into the second one and have to remember to use it on the call site which would look something like this:
Copy code
HorizontalTextsWithMaximumSpaceTaken(
    startText = {
        Text(
            text = startText,
            style = MaterialTheme.typography.h5,
        )
    },
    endText = { textAlign ->
        CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
            Text(
                text = endText,
                style = MaterialTheme.typography.h6,
                textAlign = textAlign,
            )
        }
    },
    spaceBetween = 10.dp,
)
Also I can’t for the life of me think of a nice name to call this function 😂
Copy code
@Composable
fun HorizontalTextsWithMaximumSpaceTaken(
    startText: @Composable () -> Unit,
    endText: @Composable (textAlign: TextAlign) -> Unit,
    modifier: Modifier = Modifier,
    spaceBetween: Dp = 0.dp,
) {
    Layout(
        content = {
            startText()
            endText(textAlign = TextAlign.End)
        },
        modifier = modifier,
    ) { measurables, constraints ->
        val first = measurables[0]
        val second = measurables[1]
        val firstWidth = first.maxIntrinsicWidth(constraints.maxHeight)
        val secondWidth = second.maxIntrinsicWidth(constraints.maxHeight)

        val totalWidth = constraints.maxWidth
        val halfWidth = totalWidth / 2

        val centerSpace = spaceBetween.roundToPx()
        val halfCenterSpace = (spaceBetween / 2).roundToPx()

        val halfWidthMinusSpace = halfWidth - halfCenterSpace

        val firstConstraints: Constraints
        val secondConstraints: Constraints
        val textsShouldShareEqualSpace =
            (firstWidth <= halfWidthMinusSpace && secondWidth <= halfWidthMinusSpace) ||
                (firstWidth > halfWidthMinusSpace && secondWidth > halfWidthMinusSpace)
        if (textsShouldShareEqualSpace) {
            firstConstraints = constraints.copy(minWidth = halfWidthMinusSpace, maxWidth = halfWidthMinusSpace)
            secondConstraints = firstConstraints
        } else if (firstWidth > halfWidthMinusSpace) {
            firstConstraints = constraints.copy(
                minWidth = totalWidth - secondWidth - halfCenterSpace,
                maxWidth = totalWidth - secondWidth - halfCenterSpace,
            )
            secondConstraints = constraints.copy(
                minWidth = secondWidth,
                maxWidth = secondWidth,
            )
        } else {
            firstConstraints = constraints.copy(
                minWidth = firstWidth,
                maxWidth = firstWidth,
            )
            secondConstraints = constraints.copy(
                minWidth = totalWidth - firstWidth - halfCenterSpace,
                maxWidth = totalWidth - firstWidth - halfCenterSpace,
            )
        }
        val firstPlaceable = first.measure(firstConstraints)
        val secondPlaceable = second.measure(secondConstraints)
        layout(constraints.maxWidth, max(firstPlaceable.height, secondPlaceable.height)) {
            firstPlaceable.placeRelative(0, 0)
            secondPlaceable.placeRelative(constraints.maxWidth - secondPlaceable.width, 0)
        }
    }
}
👍 2
5 Views