Last active
June 22, 2022 20:59
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | |
| ) |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Previews: