Reputation: 673
I'd like to build a row in Jetpack Compose, with 3 elements, where the first and last elements are "stuck" to either sides, and the middle one stays in the center. The elements are not all the same width. It's possible for the first element to be really long, in which case I would like the middle item to move to the right, as much as possible. The images below hopefully illustrate what I mean:
Wrapping each element in a Box
and setting each weight(1f)
helps with the first layout, but it doesn't let the first element to grow if it's long. Maybe I need a custom implementation of a Row Arrangement?
Upvotes: 3
Views: 4039
Reputation: 897
One way is to surround the middle element with 2 elements with a weight of 1.
@Composable
fun Test() {
Row(
modifier = Modifier
.fillMaxWidth()
) {
// Left element
Row(
modifier = Modifier.weight(1f)
) {
IconButton(
onClick = { }
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowLeft,
contentDescription = "Previous"
)
}
IconButton(
onClick = { }
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowRight,
contentDescription = "Next"
)
}
Text(
"Overflowing text",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.align(Alignment.CenterVertically)
)
}
// Center element
Button(
onClick = { }
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "Play"
)
}
// Right element
Box(modifier = Modifier.weight(1f)){
IconButton(
onClick = { }
) {
Icon(
imageVector = Icons.Default.RestartAlt,
contentDescription = "Restart"
)
}
}
}
}
You can change the contentAlignment
. There is no absolute "CenterRight", but that's fine because the right element will go to the left.
@Composable
fun Test() {
Row(
modifier = Modifier
.fillMaxWidth()
) {
// Left element
Row(
modifier = Modifier
.weight(1f),
horizontalArrangement = Arrangement.Absolute.Right
) {
IconButton(
onClick = { }
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowLeft,
contentDescription = "Previous"
)
}
IconButton(
onClick = { }
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowRight,
contentDescription = "Next"
)
}
}
// Center element
Button(
onClick = { }
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = "Play"
)
}
// Right element
Box(
modifier = Modifier
.weight(1f),
contentAlignment = Alignment.CenterEnd
){
IconButton(
onClick = { }
) {
Icon(
imageVector = Icons.Default.RestartAlt,
contentDescription = "Restart"
)
}
}
}
}
@Preview(showBackground = true)
@Composable
fun TestPreview() {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
Test()
}
}
Upvotes: 3
Reputation: 673
Ok, I managed to get the desired behaviour with a combination of custom implementation of an Arrangement
and Modifier.weight
.
I recommend you investigate the implementation of Arrangement.SpaceBetween
or Arrangement.SpaceEvenly
to get the idea.
For simplicity, I'm also assuming we'll always have 3 elements to place within the Row
.
First, we create our own implementation of the HorizontalOrVertical
interface:
val SpaceBetween3Responsively = object : Arrangement.HorizontalOrVertical {
override val spacing = 0.dp
override fun Density.arrange(
totalSize: Int,
sizes: IntArray,
layoutDirection: LayoutDirection,
outPositions: IntArray,
) = if (layoutDirection == LayoutDirection.Ltr) {
placeResponsivelyBetween(totalSize, sizes, outPositions, reverseInput = false)
} else {
placeResponsivelyBetween(totalSize, sizes, outPositions, reverseInput = true)
}
override fun Density.arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) = placeResponsivelyBetween(totalSize, sizes, outPositions, reverseInput = false)
override fun toString() = "Arrangement#SpaceBetween3Responsively"
}
The placeResponsivelyBetween
method needs to calculate the correct gap sizes between the elements, given their measured widths, and then place the elements with the gaps in-between.
fun placeResponsiveBetween(
totalSize: Int,
size: IntArray,
outPosition: IntArray,
reverseInput: Boolean,
) {
val gapSizes = calculateGapSize(totalSize, size)
var current = 0f
size.forEachIndexed(reverseInput) { index, it ->
outPosition[index] = current.roundToInt()
// here the element and gap placement happens
current += it.toFloat() + gapSizes[index]
}
}
calculateGapSize
has to try and "place" the second/middle item in the centre of the row, if the first element is short enough. Otherwise, set the first gap to 0, and check if there's space for another gap.
private fun calculateGapSize(totalSize: Int, itemSizes: IntArray): List<Int> {
return if (itemSizes.sum() == totalSize) { // the items take up the whole space and there's no space for any gaps
listOf(0, 0, 0)
} else {
val startOf2ndIfInMiddle = totalSize / 2 - itemSizes[1] / 2
val firstGap = Integer.max(startOf2ndIfInMiddle - itemSizes.first(), 0)
val secondGap = totalSize - itemSizes.sum() - firstGap
listOf(firstGap, secondGap, 0)
}
}
Then we can use SpaceBetween3Responsively
in our Row
! Some code edited out for simplicity
Row(
horizontalArrangement = SpaceBetween3Responsively,
) {
Box(modifier = Modifier.weight(1f, fill = false)) {
Text(text = "Supercalifragilisticexplialidocious",
maxLines = 1,
overflow = TextOverflow.Ellipsis)
}
Box {
// Button
}
Box {
// Icon
}
}
Modifier.weight(1f, fill = false)
is important here for the first element - because it's the only one with assigned weight
, it forces the other elements to be measured first. This makes sure that if the first element is long, it's truncated/cut to allow enough space for the other two elements (button and icon). This means the correct sizes are passed into placeResponsivelyBetween
to be placed with or without gaps. fill = false
means that if the element is short, it doesn't have to take up the whole space it's assigned - meaning there's space for the other elements to move closer, letting the Button in the middle.
Et voila!
Upvotes: 0