I am trying to implement a SegmentedControl composable, but allow for segments to be of different sizes if one of them needs more space. So far I've achieved basic implementation, where all segments are equal in width:
But as you can see, Foo
and Bar
segments can easily occupy less space to make room for Some very long string
.
So my requirements are:
When trying to implement the first requirement I quickly remembered that it is not possible with default Layout
composable since only one measurement per measurable per layout pass is allowed, and for good reasons.
Layout(
content = {
// Segments
}
) { segmentsMeasurables, constraints ->
var placeables = segmentsMeasurables.map {
it.measure(constraints)
}
// In case every placeable has enough space in the layout,
// we divide the space evenly between them
if (placeables.sumOf { it.measuredWidth } <= constraints.maxWidth) {
placeables = segmentsMeasurables.map {
it.measure( // <- NOT ALLOWED!
Constraints.fixed(
width = constraints.maxWidth / state.segmentCount,
height = placeables[0].height
)
)
}
}
layout(
width = placeables.sumOf { it.width },
height = placeables[0].height
) {
var xOffset = 0
placeables.forEachIndexed { index, placeable ->
xOffset += placeables.getOrNull(index - 1)?.width ?: 0
placeable.placeRelative(
x = xOffset,
y = 0
)
}
}
}
I also looked into SubcomposeLayout
, but it doesn't seem to do what I need (my use-case doesn't need subcomposition).
I can imagine a hacky solution in which I force at least two layout passes to collect children`s sizes and only after that perform layout logic, but it will be unstable, not performant, and will generate a frame with poorly layed-out children.
So how is it properly done? Am I missing something?
You have to use intrinsic measurements,
@Composable
fun Tiles(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Layout(
modifier = modifier,
content = content,
) { measurables, constraints ->
val widths = measurables.map { measurable -> measurable.maxIntrinsicWidth(constraints.maxHeight) }
val totalWidth = widths.sum()
val placeables: List<Placeable>
if (totalWidth > constraints.maxWidth) {
// do not fit, set all to same width
val width = constraints.maxWidth / measurables.size
val itemConstraints = constraints.copy(
minWidth = width,
maxWidth = width,
)
placeables = measurables.map { measurable -> measurable.measure(itemConstraints) }
} else {
// set each to its required width, and split the remainder evenly
val remainder = (constraints.maxWidth - totalWidth) / measurables.size
placeables = measurables.mapIndexed { index, measurable ->
val width = widths[index] + remainder
measurable.measure(
constraints = constraints.copy(
minWidth = width,
maxWidth = width,
)
)
}
}
layout(
width = constraints.maxWidth,
height = constraints.maxHeight,
) {
var x = 0
placeables.forEach { placeable ->
placeable.placeRelative(
x = x,
y = 0
)
x += placeable.width
}
}
}
}
@Preview(widthDp = 360)
@Composable
fun PreviewTiles() {
PlaygroundTheme {
Surface(
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(all = 16.dp),
) {
Tiles(
modifier = Modifier
.fillMaxWidth()
.height(40.dp)
) {
Text(
text = "Foo",
textAlign = TextAlign.Center,
modifier = Modifier.background(Color.Red.copy(alpha = .3f))
)
Text(
text = "Bar",
textAlign = TextAlign.Center,
modifier = Modifier.background(Color.Blue.copy(alpha = .3f))
)
}
Tiles(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
.height(40.dp)
) {
Text(
text = "Foo",
textAlign = TextAlign.Center,
modifier = Modifier.background(Color.Red.copy(alpha = .3f))
)
Text(
text = "Bar",
textAlign = TextAlign.Center,
modifier = Modifier.background(Color.Blue.copy(alpha = .3f))
)
Text(
text = "Some very long text",
textAlign = TextAlign.Center,
modifier = Modifier.background(Color.Red.copy(alpha = .3f))
)
}
Tiles(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
.height(40.dp)
) {
Text(
text = "Foo",
textAlign = TextAlign.Center,
modifier = Modifier.background(Color.Red.copy(alpha = .3f))
)
Text(
text = "Bar",
textAlign = TextAlign.Center,
modifier = Modifier.background(Color.Blue.copy(alpha = .3f))
)
Text(
text = "Some even much longer text that doesn't fit",
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.background(
Color.Red.copy(alpha = .3f)
)
)
}
}
}
}
}