35

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.

9
  • 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 it. Commented Feb 11, 2022 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. Commented Feb 11, 2022 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 Commented Feb 11, 2022 at 15:02
  • 1
    I reported the issue here Commented Feb 11, 2022 at 15:18
  • Is there a solution that supports LazyRow? Commented Apr 2, 2022 at 14:24

10 Answers 10

51

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:

Sign up to request clarification or add additional context in comments.

3 Comments

Thanks for your answer. This was a bug in Compose which was fixed in 1.3.0-beta01. So my code is working now since Compose 1.3.0+ :)
Thx! It's helped with my problem. height(intrinsicSize = IntrinsicSize.Max) for container.
I see there is a video on YouTube talking about this youtube.com/watch?v=vPwqP7z2CYc
10

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

2 Comments

Thanks for your answer. This was a bug in Compose which was fixed in 1.3.0-beta01. So my code is working now since Compose 1.3.0+ :)
I wish could add more than 1 upvote
7

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

2 Comments

Did you read the comments to my post? I believe the mentioned behaviour is a bug in Compose.
Thanks for .height(IntrinsicSize.Min)! This way we can set .fillMaxHeight() to all items, including first. Not in your example, but in other scenarios.
5

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.

Comments

4

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

5 Comments

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?
@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.
Is there a variant that will work with LazyRow?
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.
do you have this solution for pager?
4

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

1 Comment

Thanks for your answer. This was a bug in Compose which was fixed in 1.3.0-beta01. So my code is working now since Compose 1.3.0+ :)
2

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.

2 Comments

If I do this, the height of the children will be set to 0.
Seems like the right solution for now. Use this height as heighIn(min = rowHeight). It'll lead to unnecessary recomposition though, but there's nothing we can do about it.
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.

Comments

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.

Comments

0

I used kinda messy (by my standards) solution for LazyRow. It is a combination of dummy element in the Row and Modifier.onPlaced to calculate minHeight for all elements.

val localDensity = LocalDensity.current val minHeight = remember { mutableStateOf(0.dp) } val placedModifier = remember { mutableStateOf( Modifier.onPlaced { minHeight.value = with(localDensity) { scrollState.layoutInfo.viewportSize.height.toDp() } } ) } // Removing placed modifier to prevent height recalculation every time Row moves. LaunchedEffect(minHeight.value) { if (minHeight.value > 0.dp) { placedModifier.value = Modifier } } LazyRow( modifier = placedModifier.value, state = scrollState ) { // This is dummy longest card used to calculate height of other cards. if(minHeight.value <= 0.dp) { item { Card( modifier = Modifier.width(0.dp), .... Longest possible content should be displayed in this card .... ) } } // This contains actual cards with the data items(data) { content -> Card( modifier = Modifier.heightIn(min = minHeight.value) .... Content .... ) } } 

Hopefully it will help someone.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.