Adaptive Layout (WIP)

This commit is contained in:
Christian Basler
2025-07-08 21:41:50 +02:00
parent a8a00d24ef
commit 3dbc994dc5
2 changed files with 224 additions and 92 deletions

View File

@@ -116,10 +116,6 @@ fun App(
modifier = Modifier.blurOnFinished(isSolved),
grid = {
PuzzleGrid(
// modifier = Modifier
// .aspectRatio(1f)
// .fillMaxWidth()
// .align(Alignment.CenterHorizontally),
grid = game.grid,
onUpdate = {
horizontalClues.forEach { it.update(game.grid) }
@@ -128,19 +124,29 @@ fun App(
)
},
horizontalClues = {
HorizontalClues(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
clues = horizontalClues
)
for (clue in horizontalClues) {
HorizontalClue(
modifier = Modifier.forClue(clue),
clue = clue.clue,
isClueViolated = clue.isViolated
)
}
},
verticalClues = {
VerticalClues(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
clues = verticalClues
)
for (clue in verticalClues) {
VerticalClue(
modifier = Modifier.forClue(clue),
clue = clue.clue,
isClueViolated = clue.isViolated
)
}
},
time = {
Text(time, fontSize = TextUnit(4f, TextUnitType.Companion.Em), textAlign = TextAlign.End)
Text(
time,
fontSize = TextUnit(4f, TextUnitType.Companion.Em),
textAlign = TextAlign.End
)
}
)
EndOfGame(isSolved = isSolved, time = time, onRestart = onNewGame)

View File

@@ -3,13 +3,15 @@ package ch.dissem.yaep.ui.common
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Constraints.Companion.fitPrioritizingWidth
import androidx.compose.ui.unit.Constraints.Companion.fixed
import androidx.compose.ui.unit.dp
import ch.dissem.yaep.ui.common.AspectRatio.LANDSCAPE
import ch.dissem.yaep.ui.common.AspectRatio.PORTRAIT
import ch.dissem.yaep.ui.common.AspectRatio.SQUARISH
import kotlin.math.max
import kotlin.math.min
@@ -48,98 +50,222 @@ fun AdaptiveGameLayout(
layout(width = constraints.maxWidth, height = constraints.maxHeight) {
val aspectRatio = AspectRatio.from(constraints)
val gridConstraints: Constraints
val horizontalCluesConstraints: Constraints
val verticalCluesConstraints: Constraints
val timeConstraints: Constraints
val gridMeasurable = measurables[0][0]
val horizontalCluesMeasurables = measurables[1]
val verticalCluesMeasurables = measurables[2]
val timeMeasurable = measurables[3][0]
val spacingPx = spacing.roundToPx()
when (aspectRatio) {
PORTRAIT -> {
val gridSize = constraints.maxWidth
val bottomBarHeight = constraints.maxHeight - gridSize - spacingPx
val timeWidth = constraints.maxWidth / 10
gridConstraints = fixed(gridSize, gridSize)
horizontalCluesConstraints = fixed(gridSize, bottomBarHeight / 2)
verticalCluesConstraints =
fixed(gridSize - timeWidth - spacingPx, bottomBarHeight / 2)
timeConstraints = fixed(timeWidth, bottomBarHeight / 2)
}
SQUARISH -> {
val gridSize = (8 * min(constraints.maxWidth, constraints.maxHeight)) / 10
val rightBarWidth = constraints.maxWidth - gridSize - spacingPx
val bottomBarHeight = constraints.maxHeight - gridSize - spacingPx
gridConstraints = fixed(gridSize, gridSize)
horizontalCluesConstraints = fixed(rightBarWidth, gridSize)
verticalCluesConstraints = fixed(gridSize, bottomBarHeight)
timeConstraints = fixed(rightBarWidth, bottomBarHeight)
}
LANDSCAPE -> {
val gridSize = constraints.maxHeight
val rightBarWidth = constraints.maxWidth - gridSize - spacingPx
gridConstraints = fixed(gridSize, gridSize)
val baseSpace = gridSize - 2 * spacingPx
horizontalCluesConstraints = fitPrioritizingWidth(rightBarWidth, rightBarWidth, 0, baseSpace)
verticalCluesConstraints = fitPrioritizingWidth(rightBarWidth, rightBarWidth, 0, baseSpace)
timeConstraints = Constraints.fixedHeight(baseSpace / 10)
}
}
// The grid takes up most of the space
// Place the clues to the right for landscape and below for portrait. If the aspect ratio is close to 1, we place the horizontal clues to the right and the vertical clues to the left.
// The time is placed at the bottom right corner.
val gridPlaceable = measurables[0][0].measure(gridConstraints)
val horizontalCluesPlaceable =
measurables[1][0].measure(horizontalCluesConstraints)
val verticalCluesPlaceable = measurables[2][0].measure(verticalCluesConstraints)
val timePlaceable = measurables[3][0].apply {}.measure(timeConstraints)
// Position the grid
gridPlaceable.place(0, 0)
// Position the horizontal clues
when (aspectRatio) {
PORTRAIT -> {
horizontalCluesPlaceable.place(0, gridPlaceable.height + spacingPx)
}
SQUARISH, LANDSCAPE -> {
horizontalCluesPlaceable.place(gridPlaceable.width + spacingPx, 0)
}
}
// Position the vertical clues
when (aspectRatio) {
PORTRAIT -> {
verticalCluesPlaceable.place(
0,
gridPlaceable.height + horizontalCluesPlaceable.height + 2 * spacingPx
portrait(
constraints,
spacingPx,
gridMeasurable,
horizontalCluesMeasurables,
verticalCluesMeasurables,
timeMeasurable
)
}
SQUARISH -> {
verticalCluesPlaceable.place(0, gridPlaceable.height + spacingPx)
squarish(
constraints,
spacingPx,
gridMeasurable,
horizontalCluesMeasurables,
verticalCluesMeasurables,
timeMeasurable
)
}
LANDSCAPE -> {
verticalCluesPlaceable.place(
gridPlaceable.width + spacingPx,
horizontalCluesPlaceable.height + spacingPx
landscape(
constraints,
spacingPx,
gridMeasurable,
horizontalCluesMeasurables,
verticalCluesMeasurables,
timeMeasurable
)
}
}
// Position the time
timePlaceable.place(
x = constraints.maxWidth - timePlaceable.width - spacingPx,
y = constraints.maxHeight - timePlaceable.height
)
}
}
}
private fun Placeable.PlacementScope.portrait(
constraints: Constraints,
spacingPx: Int,
gridMeasurable: Measurable,
horizontalCluesMeasurables: List<Measurable>,
verticalCluesMeasurables: List<Measurable>,
timeMeasurable: Measurable
) {
val gridSize = constraints.maxWidth
val bottomBarHeight = constraints.maxHeight - gridSize - spacingPx
val timeWidth = constraints.maxWidth / 10
val gridConstraints = fixed(gridSize, gridSize)
val horizontalCluesConstraints = fixed(gridSize, bottomBarHeight / 2)
val verticalCluesConstraints =
fixed(gridSize - timeWidth - spacingPx, bottomBarHeight / 2)
val timeConstraints = fixed(timeWidth, bottomBarHeight / 2)
val gridPlaceable = gridMeasurable.measure(gridConstraints)
val horizontalCluesPlaceables = horizontalCluesMeasurables.map {
it.measure(horizontalCluesConstraints)
}
val verticalCluesPlaceables = verticalCluesMeasurables.map {
it.measure(verticalCluesConstraints)
}
val timePlaceable = timeMeasurable.measure(timeConstraints)
// Position the grid
gridPlaceable.place(0, 0)
// Position the horizontal clues
// TODO horizontalCluesPlaceable.place(0, gridPlaceable.height + spacingPx)
// Position the vertical clues
// TODO verticalCluesPlaceable.place(0, gridPlaceable.height + horizontalCluesPlaceable.height + 2 * spacingPx)
// Position the time
timePlaceable.place(
x = constraints.maxWidth - timePlaceable.width - spacingPx,
y = constraints.maxHeight - timePlaceable.height
)
}
private fun Placeable.PlacementScope.squarish(
constraints: Constraints,
spacingPx: Int,
gridMeasurable: Measurable,
horizontalCluesMeasurables: List<Measurable>,
verticalCluesMeasurables: List<Measurable>,
timeMeasurable: Measurable
) {
val gridSize = (8 * min(constraints.maxWidth, constraints.maxHeight)) / 10
val rightBarWidth = constraints.maxWidth - gridSize - spacingPx
val bottomBarHeight = constraints.maxHeight - gridSize - spacingPx
val gridConstraints = fixed(gridSize, gridSize)
val horizontalCluesConstraints = fixed(rightBarWidth, gridSize)
val verticalCluesConstraints = fixed(gridSize, bottomBarHeight)
val timeConstraints = fixed(rightBarWidth, bottomBarHeight)
val gridPlaceable = gridMeasurable.measure(gridConstraints)
val horizontalCluesPlaceables = horizontalCluesMeasurables.map {
it.measure(horizontalCluesConstraints)
}
val verticalCluesPlaceables = verticalCluesMeasurables.map {
it.measure(verticalCluesConstraints)
}
val timePlaceable = timeMeasurable.measure(timeConstraints)
// Position the grid
gridPlaceable.place(0, 0)
// Position the horizontal clues
// TODO horizontalCluesPlaceable.place(gridPlaceable.width + spacingPx, 0)
// Position the vertical clues
// TODO verticalCluesPlaceable.place(0, gridPlaceable.height + spacingPx)
// Position the time
timePlaceable.place(
x = constraints.maxWidth - timePlaceable.width - spacingPx,
y = constraints.maxHeight - timePlaceable.height
)
}
private fun Placeable.PlacementScope.landscape(
constraints: Constraints,
spacingPx: Int,
gridMeasurable: Measurable,
horizontalCluesMeasurables: List<Measurable>,
verticalCluesMeasurables: List<Measurable>,
timeMeasurable: Measurable
) {
val gridSize = constraints.maxHeight
val gridItemSize = (gridSize - 12 * spacingPx) / 18
val rightBarWidth = constraints.maxWidth - gridSize - spacingPx
val gridConstraints = fixed(gridSize, gridSize)
val baseSpace = gridSize - 2 * spacingPx
val horizontalCluesConstraints = fixed(
width = 3 * gridItemSize + 2 * spacingPx,
height = gridItemSize + 2 * spacingPx
)
val verticalCluesConstraints = fixed(
width = gridItemSize + 2 * spacingPx,
height = 3 * gridItemSize + 2 * spacingPx
)
val timeConstraints = Constraints.fixedHeight(baseSpace / 10)
val gridPlaceable = gridMeasurable.measure(gridConstraints)
val horizontalCluesPlaceables = horizontalCluesMeasurables.map {
it.measure(horizontalCluesConstraints)
}
val verticalCluesPlaceables = verticalCluesMeasurables.map {
it.measure(verticalCluesConstraints)
}
val timePlaceable = timeMeasurable.measure(timeConstraints)
// Position the grid
gridPlaceable.place(0, 0)
// Position the horizontal clues
val offsetY = placeClues(
placeables = horizontalCluesPlaceables,
offsetX = gridPlaceable.width + spacingPx,
offsetY = 0,
maxWidth = rightBarWidth
)
// TODO: add spacer in between
// Position the vertical clues
placeClues(
placeables = verticalCluesPlaceables,
offsetX = gridPlaceable.width + spacingPx,
offsetY = offsetY + spacingPx,
maxWidth = rightBarWidth
)
// Position the time
timePlaceable.place(
x = constraints.maxWidth - timePlaceable.width - spacingPx,
y = constraints.maxHeight - timePlaceable.height
)
}
private fun Placeable.PlacementScope.placeClues(
placeables: List<Placeable>,
offsetX: Int,
offsetY: Int,
maxWidth: Int,
minSpacing: Int = 0
): Int {
if (placeables.isEmpty()) return offsetY
val itemWidth = placeables.first().width
val itemHeight = placeables.first().height
val columns = max(1, maxWidth / itemWidth)
val spacing = max(minSpacing, (maxWidth - columns * itemWidth) / (columns - 1))
var currentX = offsetX
var currentY = offsetY
var i = 0
for (placeable in placeables) {
placeable.place(currentX, currentY)
currentX += itemWidth + spacing
i++
if (i % columns == 0) {
currentX = offsetX
currentY += itemHeight
}
}
return currentY + itemHeight
}