androidandroid-jetpack-composeandroid-jetpackandroid-jetpack-compose-ui

Is there a way to use LazyRow to set the height of each child Composable to match the height of the tallest child Composable?


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.


Solution

  • 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