5

I'm trying to create a UI in Jetpack Compose where each child Composable has the same height as the tallest child Composable.

Initially, I implemented this using Row and IntrinsicSize, but I noticed that the efficiency decreases as the number of items in the Row increases.

Row( modifier = Modifier .fillMaxWidth() .height(IntrinsicSize.Max) .horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement .spacedBy( space = Theme.dimen.spacingS, alignment = Alignment.CenterHorizontally ), ) { teams.forEachIndexed { index, item -> key(item.id) { VerticalTeamItem( modifier = Modifier .fillMaxHeight() .padding( start = if (index == 0) Theme.dimen.paddingL else 0.dp, end = if (index == teams.lastIndex) Theme.dimen.paddingL else 0.dp, ) ) } } } 

When using Row, the process of calculating the height of each item becomes increasingly inefficient as the number of items grows. Therefore, I am curious if there is a way to implement the above UI using LazyRow instead of Row to improve efficiency.

3 Answers 3

3

Intrinsic sizes do 2 layout and measure passes that's why it's not a performant Modifier in LazyRows or Composables with lots child Composable.

You can't achieve this with LazyRow because LazyRow uses SubcomposeLayout to compose, via subCompose function, that are in viewport of LazyRow. If there are only 5 items visible you will only get 6 item not every Composable and because of that it performs better over scroll modifiers with Composables that have many items.

However, you can use SubComposeLayout itself to get maxHeight then calling subCompose with that maxHeight you can always get Composable measured with that height.

@Composable internal fun MaxHeightSubcomposeLayout( modifier: Modifier = Modifier, content: @Composable () -> Unit ) { var maxHeight = 0 val positionMap = remember { hashMapOf<Int, Int>() } SubcomposeLayout(modifier = modifier) { constraints -> var subCompositionIndex = 0 // This is for measuring Composables with range between 0 and width of parent composable val wrappedConstraints = constraints.copy(minWidth = 0) // get maxHeight by sub-composing each item if max height hasn't been calculated alread if (maxHeight == 0) { maxHeight = subcompose(subCompositionIndex, content).map { subCompositionIndex++ it.measure(wrappedConstraints) }.maxOf { it.height } } // get placeables to place in this Layout val placeables: List<Placeable> = subcompose(subCompositionIndex, content).map { subCompositionIndex++ it.measure( wrappedConstraints.copy( minHeight = maxHeight, maxHeight = maxHeight ) ) } val hasBoundedWidth = constraints.hasBoundedWidth val hasFixedWidth = constraints.hasFixedWidth // If Composable has fixed Size or fillMaxWidth use that Constraints else // max width is equal to sum of width of all child Composables val layoutWidth = if (hasBoundedWidth && hasFixedWidth) constraints.maxWidth else placeables.sumOf { it.width }.coerceIn(constraints.minWidth, constraints.maxWidth) var xPos = 0 layout(layoutWidth, maxHeight) { placeables.forEachIndexed { index, placeable: Placeable -> val indexedPosition = positionMap[index] if (indexedPosition == null) { positionMap[index] = xPos } placeable.placeRelative(indexedPosition ?: xPos, 0) xPos += placeable.width } } } } 

Demo

enter image description here

@Preview @Composable fun MaxHeightTest() { val items1 = remember { List(3) { Random.nextInt(30, 120) } } val items2 = remember { List(20) { Random.nextInt(30, 120) } } Column(modifier = Modifier.fillMaxSize()) { Text("items1: $items1") Text("items2: $items2") MaxHeightSubcomposeLayout( modifier = Modifier .border(1.dp, Color.Red) .horizontalScroll( rememberScrollState() ) ) { items1.forEach { Box( modifier = Modifier.width(50.dp).height(it.dp) .background(Color.Yellow, RoundedCornerShape(16.dp)) ) Spacer(modifier = Modifier.width(16.dp)) } } Spacer(Modifier.height(16.dp)) MaxHeightSubcomposeLayout( modifier = Modifier.border(1.dp, Color.Red) .fillMaxWidth() .horizontalScroll( rememberScrollState() ) ) { items1.forEach { Box( modifier = Modifier.width(50.dp).height(it.dp) .background(Color.Yellow, RoundedCornerShape(16.dp)) ) Spacer(modifier = Modifier.width(16.dp)) } } Spacer(Modifier.height(16.dp)) MaxHeightSubcomposeLayout( modifier = Modifier.border(1.dp, Color.Red) .fillMaxWidth() .horizontalScroll( rememberScrollState() ) ) { items2.forEach { Box( modifier = Modifier.width(50.dp).height(it.dp) .background(getRandomColor(), RoundedCornerShape(16.dp)) ) Spacer(modifier = Modifier.width(16.dp)) } } } } 

You can refer this tutorial that covers Constrains, Layouts, intrinsic sizes, SubcomposeLayouts and more if you are interested learning more about these concepts.

https://github.com/SmartToolFactory/Jetpack-Compose-Tutorials/tree/master/Tutorial1-1Basics/src/main/java/com/smarttoolfactory/tutorial1_1basics/chapter3_layout

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

1 Comment

You can also change number of items to be measured and laid out as well by changing subcompose(subCompositionIndex, content) to measure items on screen by using take or other kotling funtions
2

I see basically three options:

  1. Compose the layout for every item and find the highest. That's what you tried with Row and it was not efficient enough.
  2. Calculate the height of each item with more efficient method than composition and find the highest. This could be something like height = imageHeight + numberOfLines * lineHeight + padding. This is obviously problematic, since it's hard to tell the number of lines without laying it out.
  3. Combine both approaches. Use efficient algorithm to find the item that is gonna be the highest (longest text?) and compose the layout for that item to find the exact height. This is also problematic, since the longest text doesn't mean it will take the most lines... you can find say 10 candidates for the highest item and compose those, correct result still not guaranteed.

TLDR: Algorithm that will be correct in all cases will be inefficient. Good enough and efficient approximation should be possible, depending on your data and layout.

1 Comment

Nice write up of the options
0

In my case, I know the width of the row item, and I have an image with fixed height, and some text below. So in order to calculate the height of the highest item beforehand, I calculated the height of the largest text using the TextMeasurer tool. It's a tool that allows measuring a text with known TextStyle and constraints.

I used it as follows:

// Get the longest text in the present data val largestText = viewState.items.maxBy { it.text.length }.text val textMeasurer = rememberTextMeasurer() // Measure the longest text based on the text style and max lines val labelTextLayoutResult = labelTextMeasurer.measure( text = largestText, style = TextStyle( fontSize = 12.sp, lineHeight = 1.sp ), maxLines = 2, constraints = Constraints(maxWidth = ROW_WIDTH_PX) ) // Add the height of all the elements present in the layout val lazyRowHeightPx = labelTextLayoutResult.size.height + IMAGE_IMAGE_HEIGHT_PX // You can add other (fixed height) items or spaces if present 

You can then use the lazyRowHeightPx (after converting it to Dp) to set a fixed height to the lazy row as follows:

LazyRow( modifier = Modifier.height(lazyRowHeightDp) ) { ... } 

You can measure as many texts as you want if you have multiple texts.

You should however, as mentioned, have a fixed width and be able to determine the height of the rest of the elements.

Hope it helps :)

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.