1

In Jetpack Compose, How to find the width of each child of a Row?

I tried using onGloballyPositioned like below, but the size is wrong. Also, not sure if this is the optimal one.

I need the width of each child and further processing will be done based on that, the below is a simplified code for example.

@Composable
fun MyTabSample() {
    MyCustomRow(
        items = listOf("Option 1 with long", "Option 2 with", "Option 3"),
    )
}

@Composable
fun MyCustomRow(
    modifier: Modifier = Modifier,
    items: List<String>,
) {
    val childrenWidths = remember {
        mutableStateListOf<Int>()
    }
    Box(
        modifier = modifier
            .background(Color.LightGray)
            .height(IntrinsicSize.Min),
    ) {
        // To add more box here
        Box(
            modifier = Modifier
                .widthIn(
                    min = 64.dp,
                )
                .fillMaxHeight()
                .width(childrenWidths.getOrNull(0)?.dp ?: 64.dp)
                .background(
                    color = DarkGray,
                ),
        )
        Row(
            horizontalArrangement = Arrangement.Center,
        ) {
            items.mapIndexed { index, text ->
                Text(
                    modifier = Modifier
                        .widthIn(min = 64.dp)
                        .padding(
                            vertical = 8.dp,
                            horizontal = 12.dp,
                        )
                        .onGloballyPositioned {
                            childrenWidths.add(index, it.size.width)
                        },
                    text = text,
                    color = Black,
                    textAlign = TextAlign.Center,
                )
            }
        }
    }
}

Screenshot

Abhimanyu
  • 11,351
  • 7
  • 51
  • 121

1 Answers1

2

The size you get from Modifier.onSizeChanged or Modifier.onGloballyPositioned is in px with unit Int, not in dp. You should convert them to dp with

val density = LocalDensity.current
density.run{it.size.width.toDp()}

Full code

@Composable
fun MyCustomRow(
    modifier: Modifier = Modifier,
    items: List<String>,
) {
    val childrenWidths = remember {
        mutableStateListOf<Dp>()
    }
    Box(
        modifier = modifier
            .background(Color.LightGray)
            .height(IntrinsicSize.Min),
    ) {

        val density = LocalDensity.current
        // To add more box here
        Box(
            modifier = Modifier
                .widthIn(
                    min = 64.dp,
                )
                .fillMaxHeight()
                .width(childrenWidths.getOrNull(0) ?: 64.dp)
                .background(
                    color = DarkGray,
                ),
        )
        Row(
            horizontalArrangement = Arrangement.Center,
        ) {
            items.mapIndexed { index, text ->
                Text(
                    modifier = Modifier
                        .onGloballyPositioned {
                            childrenWidths.add(index, density.run { it.size.width.toDp() })
                        }
                        .widthIn(min = 64.dp)
                        .padding(
                            vertical = 8.dp,
                            horizontal = 12.dp,
                        ),
                    text = text,
                    color = Black,
                    textAlign = TextAlign.Center,
                )
            }
        }
    }
}

However this is not the optimal way to get size since it requires at least one more recomposition. Optimal way for getting size is using SubcomposeLayout as in this answer

How to get exact size without recomposition?

TabRow also uses SubcomposeLayout for getting indicator and divider widths

@Composable
fun TabRow(
    selectedTabIndex: Int,
    modifier: Modifier = Modifier,
    containerColor: Color = TabRowDefaults.containerColor,
    contentColor: Color = TabRowDefaults.contentColor,
    indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
        TabRowDefaults.Indicator(
            Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
        )
    },
    divider: @Composable () -> Unit = @Composable {
        Divider()
    },
    tabs: @Composable () -> Unit
) {
    Surface(
        modifier = modifier.selectableGroup(),
        color = containerColor,
        contentColor = contentColor
    ) {
        SubcomposeLayout(Modifier.fillMaxWidth()) { constraints ->
            val tabRowWidth = constraints.maxWidth
            val tabMeasurables = subcompose(TabSlots.Tabs, tabs)
            val tabCount = tabMeasurables.size
            val tabWidth = (tabRowWidth / tabCount)
            val tabRowHeight = tabMeasurables.fold(initial = 0) { max, curr ->
                maxOf(curr.maxIntrinsicHeight(tabWidth), max)
            }

            val tabPlaceables = tabMeasurables.map {
                it.measure(
                    constraints.copy(
                        minWidth = tabWidth,
                        maxWidth = tabWidth,
                        minHeight = tabRowHeight
                    )
                )
            }

            val tabPositions = List(tabCount) { index ->
                TabPosition(tabWidth.toDp() * index, tabWidth.toDp())
            }

            layout(tabRowWidth, tabRowHeight) {
                tabPlaceables.forEachIndexed { index, placeable ->
                    placeable.placeRelative(index * tabWidth, 0)
                }

                subcompose(TabSlots.Divider, divider).forEach {
                    val placeable = it.measure(constraints.copy(minHeight = 0))
                    placeable.placeRelative(0, tabRowHeight - placeable.height)
                }

                subcompose(TabSlots.Indicator) {
                    indicator(tabPositions)
                }.forEach {
                    it.measure(Constraints.fixed(tabRowWidth, tabRowHeight)).placeRelative(0, 0)
                }
            }
        }
    }
}
Thracian
  • 43,021
  • 16
  • 133
  • 222
  • Thanks :). I was thinking SubcomposeLayout should be better. I didn't know how to use it. Was going through [your old question and answers](https://stackoverflow.com/q/69663194/9636037). Will try this out. – Abhimanyu Aug 28 '22 at 07:59
  • I updated my initial answer. You need to call Modifier.onGloballyPositioned before padding to get width with padding in dp. With the subcomposeLayout instead of returning one Size you only need to put them into a list and pass them. I suggest you to pass DpSize instead of converting every single one of them in dependent content – Thracian Aug 28 '22 at 08:00
  • Though not optimal, I was able to make it work. I will have to learn more about how to use SubcomposeLayout to optimize it. – Abhimanyu Aug 28 '22 at 11:49