20

I'm trying to implement a layout in Compose where the items of a horizontally scrollable Row should all have the same height, so smaller items should adjust to the size of the biggest item in the row. I know about intrinsic size but I just can't get it to work. Also I don't want to assign a fixed height to the Row, as the Row's height should also be the height of its biggest child composable.

This is the simplified layout

@Composable
fun Screen(
    modifier: Modifier = Modifier,
) {
    Row(
        modifier = modifier
            .height(IntrinsicSize.Min)
            .horizontalScroll(state = rememberScrollState()),
        horizontalArrangement = Arrangement.spacedBy(10.dp),
    ) {
        Item(
            text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy " +
                    "eirmod tempor invidunt ut labore et dolore magna aliquyam"
        )

        Item(
            text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy " +
                    "eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam " +
                    "voluptua. At"
        )

        Item(
            text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam"
        )
    }
}

@Composable
private fun Item(
    modifier: Modifier = Modifier,
    text: String,
) {
    Column(
        modifier = modifier.width(200.dp),
        horizontalAlignment = Alignment.End,
        verticalArrangement = Arrangement.SpaceBetween
    ) {
        Column {
            Text("Some static text")

            // Dynamic text
            Text(
                text,
                modifier = Modifier.padding(top = 5.dp)
            )
        }

        // The space between these two composables should be flexible,
        // hence the outer column with Arrangement.SpaceBetween

        Button(
            modifier = Modifier.padding(top = 20.dp),
            onClick = {}
        ) {
            Text("Button")
        }
    }
}

This is the result

what I get

but what I actually want is

what I want

When I apply fillMaxHeight() to Item, the items take up the whole height and all buttons are aligned to the bottom of the screen.

Jetpack Compose version: 1.1.0

Update: This was a bug in Compose which was fixed in compose-foundation version 1.3.0-beta01.

Sven Jacobs
  • 6,278
  • 5
  • 33
  • 39
  • 2
    Adding `fillMaxHeight()` to `Item` should be enough in this case, and it works with two items, but doesn't work with 3 or more for some reason. It may be a bug, I suggest you [report](https://issuetracker.google.com/issues/new?component=612128) it. – Phil Dukhov Feb 11 '22 at 14:13
  • @PhilipDukhov You're right, it works with two items and `fillMaxHeight()`?! When I reduce the width of items to for instance `100.dp`, it also works with three items. It seems that once an item completely leaves the viewport (because of the horizontal scroll), this breaks intrinsic measurement. Definitively feels like a bug. I will report it. – Sven Jacobs Feb 11 '22 at 14:54
  • 1
    I tried removing scrollable and the issue is still there, so it's not related. But I thought too about view being fully out of container bounds – Phil Dukhov Feb 11 '22 at 15:02
  • 1
    I reported the issue [here](https://issuetracker.google.com/issues/218885161) – Sven Jacobs Feb 11 '22 at 15:18
  • Is there a solution that supports LazyRow? – dazza5000 Apr 02 '22 at 14:24
  • @dazza5000 Using `LazyRow` or `LazyColumn` with intrinsic size is currently (Compose 1.1.1) not possible. It will result in the following exception `Asking for intrinsic measurements of SubcomposeLayout layouts is not supported. This includes components that are built on top of SubcomposeLayout, such as lazy lists, BoxWithConstraints, TabRow, etc.` – Sven Jacobs Apr 04 '22 at 08:29
  • I know - what do you suggest as an alternative? – dazza5000 Apr 04 '22 at 13:37
  • @dazza5000 I'm sorry but I don't have a solution. The problem which I described above is a bug in Compose which has not yet been fixed. – Sven Jacobs Apr 04 '22 at 17:14
  • Unfortunately this is still an issue with Compose 1.2.0. – Sven Jacobs Aug 01 '22 at 05:41

9 Answers9

23

This method is only applicable if you have a small list, as Row() loads all items at once (not lazily)

As of Compose v1.3.0 Lazy lists don't support modifier.height(intrinsicSize = IntrinsicSize.Max)

So for now we have to use Row with modifier = modifier.height(intrinsicSize = IntrinsicSize.Max)

When writing RowItem add Spacer(modifier = Modifier.weight(1f)) to fill empty space

@Composable
fun RowItem(modifier: Modifier = Modifier) {
Surface(
    modifier = modifier,
    color = Color.Gray,
    shape = RoundedCornerShape(12.dp)
) {
    Column(
        verticalArrangement = Arrangement.spacedBy(12.dp),
        modifier = Modifier.padding(16.dp)
    ) {
        repeat((1..4).random()) {
            Text(
                text = "item $it",
                color = Color.White,
                modifier = Modifier.padding(
                    horizontal = 16.dp,
                    vertical = 4.dp
                )
            )
        }

        Spacer(modifier = Modifier.weight(1f)) // this is required to push below composables to bottom

        Button(onClick = {
        }) { Text(text = "Button") }
    }
}}

Make sure to add horizontalScroll(rememberScrollState()) to make Row scrollable and height(intrinsicSize = IntrinsicSize.Max) to make the height of all cards to the tallest item.

@Composable
fun ScrollableRow() {
Row(
    Modifier
        .horizontalScroll(rememberScrollState()) // this makes it scrollable
        .height(intrinsicSize = IntrinsicSize.Max) // this make height of all cards to the tallest card.
        .padding(horizontal = 16.dp),
    content = {
        repeat(4) {
            RowItem(modifier = Modifier.fillMaxHeight())
        }
    },
    horizontalArrangement = Arrangement.spacedBy(16.dp),
)}

Result:

Sahal Nazar
  • 625
  • 4
  • 9
  • 2
    Thanks for your answer. This was a bug in Compose which was fixed in [1.3.0-beta01](https://stackoverflow.com/a/73346000/416029). So my code is working now since Compose 1.3.0+ :) – Sven Jacobs Mar 03 '23 at 07:15
  • 2
    Thx! It's helped with my problem. **height(intrinsicSize = IntrinsicSize.Max)** for container. – Dmitriy Khalturin Mar 25 '23 at 17:21
6

I created a solution that is not recommended by Google, but works well for us.

fun Modifier.minimumHeightModifier(state: MinimumHeightState, density: Density) = onSizeChanged { size ->
    val itemHeight = with(density) {
        val height = size.height
        height.toDp()
    }

    if (itemHeight > state.minHeight ?: 0.dp) {
        state.minHeight = itemHeight
    }
}.defaultMinSize(minHeight = state.minHeight ?: Dp.Unspecified)

class MinimumHeightState(minHeight: Dp? = null) {
    var minHeight by mutableStateOf(minHeight)
}

You then configure and apply the modifier to everything you want to have the same minimum height

val density = LocalDensity.current

val minimumHeightState = remember { MinimumHeightState() }
val minimumHeightStateModifier = Modifier.minimumHeightModifier(
    minimumHeightState,
    density
)

These were all in a LazyRow

    itemsIndexed(items = carouselModel.subviews, key = { _, item -> item.id }) { _, item ->
        when (item) {
            is BasicCard -> {
                FooCard(
                    modifier = minimumHeightStateModifier,
                    section = item,
                    onAction = onAction
                )
            }
            is BarCard -> {
                BarVerticalCard(
                    modifier = minimumHeightStateModifier,
                    section = item,
                    onAction = onAction
                )
            }

A discussion about the solution on the kotlin slack can be found here: https://kotlinlang.slack.com/archives/CJLTWPH7S/p1649956718414129

dazza5000
  • 7,075
  • 9
  • 44
  • 89
  • Thanks for your answer. This was a bug in Compose which was fixed in [1.3.0-beta01](https://stackoverflow.com/a/73346000/416029). So my code is working now since Compose 1.3.0+ :) – Sven Jacobs Mar 03 '23 at 07:16
3

Implenting such a feature to set height of each element is possible with SubComposeLayout which lets you to remeasure your composables based on new metrics such as sibling with max width or height.

You can check description of SubComposeLayout here, or my answer to have Columns with equal widths here.

@Composable
fun SubcomposeRow(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit = {},
) {

    SubcomposeLayout(modifier = modifier) { constraints ->

        var recompositionIndex = 0

        var placeables: List<Placeable> = subcompose(recompositionIndex++, content).map {
            it.measure(constraints)
        }

        placeables.forEachIndexed() { index: Int, placeable: Placeable ->
            println("Index: $index, placeable width: ${placeable.width}, height: ${placeable.height}")
        }

        var rowSize =
            placeables.fold(IntSize.Zero) { currentMax: IntSize, placeable: Placeable ->
                IntSize(
                    width = currentMax.width + placeable.width,
                    height = maxOf(currentMax.height, placeable.height)
                )
            }


        // Remeasure every element using height of longest item as minHeight of Constraint
        if (!placeables.isNullOrEmpty() && placeables.size > 1) {
            placeables = subcompose(recompositionIndex, content).map { measurable: Measurable ->
                measurable.measure(
                    Constraints(
                        minHeight = rowSize.height,
                        maxHeight = constraints.maxHeight
                    )
                )
            }

            rowSize =
                placeables.fold(IntSize.Zero) { currentMax: IntSize, placeable: Placeable ->
                    IntSize(
                        width = currentMax.width + placeable.width,
                        height = maxOf(currentMax.height, placeable.height)
                    )
                }
        }

        layout(rowSize.width, rowSize.height) {
            var xPos = 0
            placeables.forEach { placeable: Placeable ->
                placeable.placeRelative(xPos, 0)
                xPos += placeable.width
            }

        }
    }
}

Constraints in second measurement is important since we want each composable to have max height

                Constraints(
                    minHeight = rowSize.height,
                    maxHeight = constraints.maxHeight
                )

Usage

@Composable
fun Screen(
    modifier: Modifier = Modifier,
) {
    SubcomposeRow(
        modifier = modifier
            .background(Color.LightGray)
            .horizontalScroll(state = rememberScrollState()),
    ) {
        Item(
            text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy " +
                    "eirmod tempor invidunt ut labore et dolore magna aliquyam"
        )

        Item(
            text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy " +
                    "eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam " +
                    "voluptua. At"
        )

        Item(
            text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam"
        )
    }
}

@Composable
private fun Item(
    modifier: Modifier = Modifier,
    text: String,
) {
    Column(
        modifier = modifier
            .width(200.dp)
            .background(Color.Red),
        horizontalAlignment = Alignment.End,
        verticalArrangement = Arrangement.SpaceBetween
    ) {
        Column(modifier = Modifier.background(Color.Yellow)) {
            Text("Some static text")

            // Dynamic text
            Text(
                text,
                modifier = Modifier.padding(top = 5.dp)
            )
        }

        // The space between these two composables should be flexible,
        // hence the outer column with Arrangement.SpaceBetween

        Button(
            modifier = Modifier
                .padding(top = 20.dp),
            onClick = {}
        ) {
            Text("Button")
        }
    }
}

Result

enter image description here

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • Thanks. I have not tried it out yet but this seems to be quite some code for the rather simple layout that I try to achieve. Do you agree, as assumed in the comments to my post, that this strange behaviour feels like a Compose bug? – Sven Jacobs Feb 14 '22 at 10:38
  • @SvenJacobs you should only consider `SubcomposeRow` as new code, since rest is just to demonstrate how to use it. And compared to `Row` code or implementing this behavior with a custom `View` it's quite simple and straightforward. I agree with the bug part but with compose it's so easy to build your own Composable based on your needs easier and faster than you do with views. Added colors to demonstrate composable bounds. – Thracian Feb 14 '22 at 10:41
  • Is there a variant that will work with LazyRow? – dazza5000 Apr 01 '22 at 22:25
  • No because by definition LazyRow probably won't know the height of all the items because it doesn't create them until they are needed. – James Riordan Aug 15 '22 at 17:02
  • do you have this solution for pager? – Yehezkiel L May 31 '23 at 08:49
2

The following example produces the behavior you're looking for. It utilizes intrinsic sizes.

@Preview
@Composable
fun PreviewTest() {
    Row(Modifier.height(IntrinsicSize.Min)) {
        Box(
            modifier = Modifier
                .background(Color.Red)
                .size(size = 80.dp)
        )
        Box(
            modifier = Modifier
                .background(Color.Green)
                .defaultMinSize(minWidth = 80.dp, minHeight = 40.dp)
                .fillMaxHeight()
        )
        Box(
            modifier = Modifier
                .background(Color.Blue)
                .defaultMinSize(minWidth = 80.dp, minHeight = 40.dp)
                .fillMaxHeight()
        )
    }
}

enter image description here

Lasse Magnussen
  • 372
  • 3
  • 8
1

The described behaviour is a bug in Compose which I reported on Feb 11, 2022. It was marked as fixed on Aug 13, 2022. However it is yet unknown which Compose version will contain this fix.

Update: The bug was fixed in compose-foundation version 1.3.0-beta01.

Sven Jacobs
  • 6,278
  • 5
  • 33
  • 39
0

For solving this problem I used onTextLayout in Text().

onTextLayout = { result ->
 val offsetLines = result.multiParagraph.maxLines - 
                   result.multiParagraph.lineCount
 if (offsetLines > 0)
     text = text.plus("/n".repeat(offsetLines))
 }

find the minimum lines and add space until all my texts in carts have a same height.

Dr Mido
  • 2,414
  • 4
  • 32
  • 72
melikaafrakhteh
  • 305
  • 1
  • 2
  • 8
0

A little late but here is the solution. You were almost there. What you want is the height of the row to be equal to the Max height out of all the sub-column items, so what you want is to set the row height to "IntrinsicSize.Max". The trick is to use a Spacer() compose to fill in any space needed to be filled, without affecting the Intrinsic Height of the column itself. If you set the weight of the spacer to "1f" it will fill the remaining space.

@Preview
@Composable
fun Screen(
    modifier: Modifier = Modifier,
) {
    Row(
        modifier = modifier
            //**SET HEIGHT TO INSTRINSICSIZE.MAX**
            .height(IntrinsicSize.Max)
            .horizontalScroll(state = rememberScrollState()),
        horizontalArrangement = Arrangement.spacedBy(10.dp),
    ) {
        Item(
            text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy " +
                    "eirmod tempor invidunt ut labore et dolore magna aliquyam"
        )

        Item(
            text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy " +
                    "eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam " +
                    "voluptua. At"
        )

        Item(
            text = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam"
        )
    }
}

@Composable
private fun Item(
    modifier: Modifier = Modifier,
    text: String,
) {
    Column(
        modifier = modifier.width(200.dp),
        horizontalAlignment = Alignment.End,
        verticalArrangement = Arrangement.SpaceBetween
    ) {
        Column {
            Text("Some static text")

            // Dynamic text
            Text(
                text,
                modifier = Modifier.padding(top = 5.dp)
            )
        }
        //**THIS IS THE TRICK**
        Spacer(modifier = Modifier.weight(1f))

        // The space between these two composables should be flexible,
        // hence the outer column with Arrangement.SpaceBetween

        Button(
            modifier = Modifier.padding(top = 20.dp),
            onClick = {}
        ) {
            Text("Button")
        }
    }
}

see resulting screenshot

  • 2
    Thanks for your answer. This was a bug in Compose which was fixed in [1.3.0-beta01](https://stackoverflow.com/a/73346000/416029). So my code is working now since Compose 1.3.0+ :) – Sven Jacobs Mar 03 '23 at 07:15
0

For LazyRow, you can get the height of the row like this:

val rowHeight = with(LocalDensity.current) { lazyRowtState.layoutInfo.viewportSize.height.toDp() }

Then simply set the height to the child.

Tahmid
  • 315
  • 3
  • 12
0

For anyone who came across this question looking for a solution for LazyLists, I seem to have found one. This is for LazyVerticalGrid, but could probably be tweaked to work for any LazyList.

Essentially, using visibleItemsInfo we can find the max height of items in each row, and set that as the height for every item in that row.

val state = rememberLazyGridState()
val density = LocalDensity.current

LazyVerticalGrid(
    columns = GridCells.Adaptive(minSize = 150.dp),
    state = state
) {
    itemsIndexed(myItemStates) { index, myItemState ->
        val row = state.layoutInfo.visibleItemsInfo.find { it.index == index }?.row
        val itemsInRow = state.layoutInfo.visibleItemsInfo.filter { it.row == row }
        val maxHeightInRow = itemsInRow.maxOfOrNull { it.size.height }
        val maxHeightInRowDp = with(density) { maxHeightInRow?.toDp() } ?: Dp.Unspecified
        MyItem(
            state = myItemState,
            modifier = Modifier.height(maxHeightInRowDp)
        )
    }
}

I'm not sure if this is doing too much work during composition, or breaking some kind of Compose rule, but it seems to work for me and I haven't had any issues.