Skip to content

Instantly share code, notes, and snippets.

@NinoDLC
Created September 23, 2025 11:57
Show Gist options
  • Save NinoDLC/9770237de84071e18e8f1c86ef60e03c to your computer and use it in GitHub Desktop.
Save NinoDLC/9770237de84071e18e8f1c86ef60e03c to your computer and use it in GitHub Desktop.
An example for a Row that will "fold" (when there's not enough width) into even splits
package com.example.myapplication
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.MultiContentMeasurePolicy
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import com.example.myapplication.ui.theme.MyApplicationTheme
import kotlin.math.roundToInt
typealias RowIndex = Int
typealias RowHeight = Int
@Composable
fun FoldableRow(texts: List<String>, modifier: Modifier = Modifier) {
Layout(
modifier = modifier,
contents = listOf(
{
texts.forEach { text ->
Text(
modifier = Modifier.padding(vertical = 2.dp),
text = text,
textAlign = TextAlign.Center,
)
}
},
{
repeat(texts.size - 1) {
VerticalDivider(
modifier = Modifier.padding(horizontal = 4.dp),
)
}
}
),
measurePolicy = object : MultiContentMeasurePolicy {
override fun MeasureScope.measure(
measurables: List<List<Measurable>>,
constraints: Constraints
): MeasureResult = measureFoldableRow(
measurableTexts = measurables[0],
measurableVerticalDividers = measurables[1],
constraints = constraints,
)
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<List<IntrinsicMeasurable>>,
height: Int
): Int = measurables.maxOf { measurableChildren ->
measurableChildren.maxOf {
it.minIntrinsicWidth(height)
}
}
}
)
}
private fun MeasureScope.measureFoldableRow(
measurableTexts: List<Measurable>,
measurableVerticalDividers: List<Measurable>,
constraints: Constraints
): MeasureResult {
val paddingBetweenRows = 16.dp
var itemsPerRow = measurableTexts.size
val maxHeightForRowIndex = mutableMapOf<RowIndex, RowHeight>()
val measuredTexts: List<Placeable>
val measuredVerticalDividers: List<Placeable>
val layoutWidth: Int
val layoutHeight: Int
if (!constraints.hasBoundedWidth) {
measuredTexts = measurableTexts.map {
it.measure(Constraints())
}
layoutWidth = measuredTexts.sumOf { it.width }
layoutHeight = measuredTexts.maxOf { it.height }
val verticalDividerConstraints = Constraints(
minHeight = layoutHeight,
maxHeight = layoutHeight,
)
measuredVerticalDividers = measurableVerticalDividers.map {
it.measure(verticalDividerConstraints)
}
maxHeightForRowIndex[0] = layoutHeight
} else {
val textIntrinsicWidths = measurableTexts.map {
it.maxIntrinsicWidth(height = 0)
}
val maxTextIntrinsicWidth = textIntrinsicWidths.max()
val verticalDividerWidth = measurableVerticalDividers
.first()
.maxIntrinsicWidth(height = 0)
fun computeExpectedWidthPerRow(): Int = maxTextIntrinsicWidth
.times(itemsPerRow)
.plus((itemsPerRow - 1) * verticalDividerWidth)
var expectedWidthPerRow = computeExpectedWidthPerRow()
while (expectedWidthPerRow > constraints.maxWidth && itemsPerRow > 1) {
itemsPerRow /= 2
expectedWidthPerRow = computeExpectedWidthPerRow()
}
val textWidth = constraints.maxWidth
.minus((itemsPerRow - 1) * verticalDividerWidth)
.div(itemsPerRow)
measuredTexts = measurableTexts.map {
it.measure(
constraints.copy(
minWidth = textWidth,
maxWidth = textWidth,
)
)
}
for (rowIndex in 0 until measuredTexts.size / itemsPerRow) {
var maxItemHeightForThatRow = 0
for (index in 0 until itemsPerRow) {
val offsetIndex = index + (rowIndex * itemsPerRow)
val itemHeight = measuredTexts[offsetIndex].height
maxItemHeightForThatRow = maxOf(maxItemHeightForThatRow, itemHeight)
}
maxHeightForRowIndex[rowIndex] = maxItemHeightForThatRow
}
measuredVerticalDividers = measurableVerticalDividers.mapIndexed { index, it ->
val heightForThatRow = maxHeightForRowIndex[index / itemsPerRow]!!
it.measure(
Constraints(
minHeight = heightForThatRow,
maxHeight = heightForThatRow,
)
)
}
val verticalGaps = maxHeightForRowIndex.entries.size - 1
layoutWidth = constraints.maxWidth
layoutHeight = maxHeightForRowIndex.values.sum()
.plus(paddingBetweenRows.toPx() * verticalGaps)
.roundToInt()
}
return layout(
width = layoutWidth,
height = layoutHeight,
) {
var x = 0
var y = 0
measuredTexts.forEachIndexed { index, measuredText ->
val indexInRow = index % itemsPerRow
measuredText.place(x, y)
x += measuredText.width
if (indexInRow + 1 < itemsPerRow) {
val verticalDivider = measuredVerticalDividers[index]
verticalDivider.place(x, y)
x += verticalDivider.width
} else {
x = 0
y += maxHeightForRowIndex[index / itemsPerRow]!!
y += paddingBetweenRows.toPx().roundToInt()
}
}
}
}
@Preview(showBackground = true)
@Composable
fun FoldableRowPreview() {
MyApplicationTheme {
FoldableRow(
listOf(
"Tiago",
"Zach",
"Alex",
"Nino",
)
)
}
}
@Preview(showBackground = true)
@Composable
fun FoldableRow2RowsPreview() {
MyApplicationTheme {
FoldableRow(
listOf(
"Tiago",
"Zach",
"Alex",
"Ninoooooooo",
)
)
}
}
@Preview(showBackground = true)
@Composable
fun FoldableRow4RowsPreview() {
MyApplicationTheme {
FoldableRow(
listOf(
"Tiago",
"Zach",
"Alex",
"Ninoooooooooooooooooo",
)
)
}
}
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.ui.Modifier
import com.example.myapplication.ui.theme.MyApplicationTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
FoldableRow(
modifier = Modifier.padding(innerPadding),
texts = listOf(
"Tiago",
"Zach",
"Alex",
"Nino",
),
)
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment