Martyna Maron
Martyna Maron

Reputation: 673

How to center the middle child in a Compose Row and make it responsive

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:

  1. All elements fit nicely

enter image description here

  1. The first element is long and pushes the middle item to the right

enter image description here

  1. The first element is super long, pushes the middle item all the way to the right and uses an ellipsis if necessary.

enter image description here

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

Answers (2)

BPDev
BPDev

Reputation: 897

One way is to surround the middle element with 2 elements with a weight of 1.

preview

@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.

preview

@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

Martyna Maron
Martyna Maron

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!

enter image description here

Upvotes: 0

Related Questions