Skip to content

Instantly share code, notes, and snippets.

@yoloroy
Last active June 22, 2022 20:59
Show Gist options
  • Select an option

  • Save yoloroy/5a268d638887464e68b2e47998b9c2ea to your computer and use it in GitHub Desktop.

Select an option

Save yoloroy/5a268d638887464e68b2e47998b9c2ea to your computer and use it in GitHub Desktop.
Grid layout for even distribution of items across rows if they can fit on that rows
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.yoloroy.gameoflife.presentation.ui.theme.GameOfLifeTheme
import com.yoloroy.gameoflife.util.infiniteIterator // see here https://gist.github.com/yoloroy/ab9d4e01285e0d25deadd9c582c1c81d
@Composable
inline fun FlexibleAdaptiveGrid(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
val rowsOfMeasurables = distributeMeasurablesToRows(measurables, constraints)
val rowsOfPlaceables = measureRowsElements(rowsOfMeasurables, constraints)
layout(
constraints.maxWidth,
rowsOfPlaceables.sumOf { it.maxHeight() },
placementBlock = { placeRows(rowsOfPlaceables) }
)
}
}
fun Placeable.PlacementScope.placeRows(rowsOfPlaceables: List<List<Placeable>>) {
var yPosition = 0
rowsOfPlaceables.forEach { row ->
var xPosition = 0
row.forEach { placeable ->
placeable.placeRelative(xPosition, yPosition)
xPosition += placeable.width
}
yPosition += row.maxHeight()
}
}
fun measureRowsElements(
rowsOfMeasurables: List<List<Measurable>>,
layoutConstraints: Constraints
): List<List<Placeable>> {
return rowsOfMeasurables
.map { row -> // "premeasure"
row.map { measurable ->
val minIntrinsicWidth = measurable.minIntrinsicWidth(layoutConstraints.maxHeight)
val constraints = layoutConstraints.copy(
maxWidth = minIntrinsicWidth,
minWidth = minIntrinsicWidth
)
measurable to constraints
}
}
.map { row ->
val rowWidth = row.sumOf { (_, constraints) -> constraints.minWidth }
row.map { (measurable, constraints) ->
val width = (constraints.minWidth.toFloat() / rowWidth) * layoutConstraints.maxWidth
measurable.measure(
constraints.copy(
minWidth = width.toInt(),
maxWidth = width.toInt()
)
)
}
}
}
fun distributeMeasurablesToRows(
measurables: List<Measurable>,
constraints: Constraints
): List<List<Measurable>> = buildList rows@{
val row = mutableListOf(measurables[0])
measurables.drop(1).forEach { measurable ->
val potentialRowWidth = (row + measurable).minIntrinsicWidth(constraints.maxHeight)
if (potentialRowWidth - 1 > constraints.maxWidth) {
[email protected](row.toList())
row.clear()
}
row.add(measurable)
}
add(row.toList())
}
fun Iterable<Measurable>.minIntrinsicWidth(height: Int) = sumOf { it.minIntrinsicWidth(height) }
fun Iterable<Placeable>.maxHeight() = maxOf { it.height }
@Preview(widthDp = 500)
@Composable
fun FlexibleAdaptiveGridPreview_boxes() {
val gridWidth = 500.dp
val rowHeight = 50.dp
@Composable
fun ProportionItem(widthWeight: Int, row: Int, modifier: Modifier = Modifier) {
val width = gridWidth / widthWeight.toFloat()
Box(
modifier = modifier
.defaultMinSize(
minWidth = width,
minHeight = rowHeight
)
) {
Text("width:$width row:$row", fontSize = 8.sp, color = Color.Red)
}
}
@Composable
fun ProportionItemsRow(rowSize: Int, row: Int): Iterator<Color> {
val colors = List(rowSize) { i ->
Color(
1f - 1f / rowSize * i,
1f - 1f / rowSize * i,
1f - 1f / rowSize * i,
1f
)
}.iterator()
repeat(rowSize) {
ProportionItem(
widthWeight = rowSize,
row = row,
modifier = Modifier.background(colors.next())
)
}
return colors
}
GameOfLifeTheme {
FlexibleAdaptiveGrid(modifier = Modifier.width(gridWidth)) {
ProportionItemsRow(1, 1)
ProportionItemsRow(2, 2)
ProportionItemsRow(4, 3)
ProportionItemsRow(8, 4)
}
}
}
@Preview
@Composable
fun FlexibleAdaptiveGridPreview_texts() {
val backgrounds = listOf(
Color.White,
Color.Black
).infiniteIterator()
@Composable
fun TestText(text: String) = Text(
text = text.useNonBreakingSpaces(),
softWrap = false,
maxLines = 1,
color = Color(
(0..0xFF).random(),
(0..0xFF).random(),
(0..0xFF).random(),
0xFF
),
modifier = Modifier.background(backgrounds.next()).fillMaxWidth()
)
GameOfLifeTheme {
FlexibleAdaptiveGrid(
modifier = Modifier
.width(250.dp)
.background(Color.Red)
) {
TestText("smll")
TestText("smll")
TestText("smll")
TestText("smll")
TestText("smll")
TestText("smll")
TestText("smll")
TestText("smll")
TestText("something very very big")
TestText("medium size")
TestText("medium size")
TestText("smll")
TestText("smll")
TestText("medium size")
TestText("medium size")
TestText("medium size")
TestText("medium size")
TestText("something very very big")
}
}
}
object Constants {
const val REGULAR_SPACE_CHARACTER = ' '
const val NON_BREAKABLE_SPACE_UNICODE = '\u00A0'
}
fun String?.useNonBreakingSpaces() = this.orEmpty()
.replace(
Constants.REGULAR_SPACE_CHARACTER,
Constants.NON_BREAKABLE_SPACE_UNICODE
)
@yoloroy
Copy link
Author

yoloroy commented Jun 22, 2022

Previews:

boxes preview

texts preview

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment