Adaptive Layout (WIP)

This commit is contained in:
Christian Basler
2025-07-07 18:45:28 +02:00
parent 85763485d3
commit a7e8325b26
2 changed files with 191 additions and 61 deletions

View File

@@ -6,14 +6,12 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
@@ -27,6 +25,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
@@ -128,40 +127,26 @@ fun App(
}
)
},
clues = {
PuzzleClues(
horizontalClues = horizontalClues,
verticalClues = verticalClues
horizontalClues = {
HorizontalClues(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
clues = horizontalClues
)
},
time = time
verticalClues = {
VerticalClues(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
clues = verticalClues
)
},
time = {
Text(time, fontSize = TextUnit(4f, TextUnitType.Companion.Em), textAlign = TextAlign.End)
}
)
EndOfGame(isSolved = isSolved, time = time, onRestart = onNewGame)
}
}
@Composable
fun AdaptiveGameLayout(
modifier: Modifier = Modifier,
grid: @Composable () -> Unit,
clues: @Composable () -> Unit,
time: String = "00:00"
) {
// TODO: actually use an adaptive layout
Row(modifier = modifier) {
Column(modifier = Modifier.weight(0.6f)) {
grid()
}
Column(
modifier = Modifier.padding(start = 16.dp).weight(0.4f).fillMaxHeight()
) {
clues()
Text(time, fontSize = TextUnit(4f, TextUnitType.Em))
}
}
}
@Composable
fun PuzzleGrid(
modifier: Modifier = Modifier,
@@ -226,42 +211,42 @@ fun PuzzleGrid(
}
@Composable
fun PuzzleClues(
fun HorizontalClues(
modifier: Modifier = Modifier,
horizontalClues: List<DisplayClue<HorizontalClue>>,
verticalClues: List<DisplayClue<SameColumnClue<ItemClass<*>, ItemClass<*>>>>
clues: List<DisplayClue<HorizontalClue>>
) {
Column(modifier = modifier) {
LazyVerticalGrid(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
columns = GridCells.Fixed(4)
) {
for (clue in horizontalClues) {
item {
HorizontalClue(
modifier = Modifier
.forClue(clue),
clue = clue.clue,
isClueViolated = clue.isViolated
)
}
LazyVerticalGrid(
modifier = modifier,
columns = GridCells.Fixed(4)
) {
for (clue in clues) {
item {
HorizontalClue(
modifier = Modifier.forClue(clue),
clue = clue.clue,
isClueViolated = clue.isViolated
)
}
}
HorizontalDivider()
LazyVerticalGrid(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
columns = GridCells.Fixed(8)
) {
for (clue in verticalClues) {
item {
VerticalClue(
modifier = Modifier
.forClue(clue)
.aspectRatio(0.33333334f),
clue = clue.clue,
isClueViolated = clue.isViolated
)
}
}
}
@Composable
fun VerticalClues(
modifier: Modifier = Modifier,
clues: List<DisplayClue<SameColumnClue<ItemClass<*>, ItemClass<*>>>>
) {
LazyVerticalGrid(
modifier = modifier,
columns = GridCells.Fixed(8)
) {
for (clue in clues) {
item {
VerticalClue(
modifier = Modifier.forClue(clue),
clue = clue.clue,
isClueViolated = clue.isViolated
)
}
}
}

View File

@@ -0,0 +1,145 @@
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.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.min
internal val spacing = 8.dp
private enum class AspectRatio {
PORTRAIT, LANDSCAPE, SQUARISH;
companion object {
private const val landscapeRatio = 1.4f
private const val portraitRatio = 1 / landscapeRatio
fun from(constraints: Constraints): AspectRatio {
val ratio = constraints.maxWidth.toFloat() / constraints.maxHeight.toFloat()
return when {
ratio < portraitRatio -> PORTRAIT
ratio > landscapeRatio -> LANDSCAPE
else -> SQUARISH
}
}
}
}
@Composable
fun AdaptiveGameLayout(
modifier: Modifier = Modifier,
grid: @Composable () -> Unit,
horizontalClues: @Composable () -> Unit,
verticalClues: @Composable () -> Unit,
time: @Composable () -> Unit
) {
Layout(
contents = listOf(grid, horizontalClues, verticalClues, time),
modifier = modifier
) { measurables, constraints ->
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 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
)
}
SQUARISH -> {
verticalCluesPlaceable.place(0, gridPlaceable.height + spacingPx)
}
LANDSCAPE -> {
verticalCluesPlaceable.place(
gridPlaceable.width + spacingPx,
horizontalCluesPlaceable.height + spacingPx
)
}
}
// Position the time
timePlaceable.place(
x = constraints.maxWidth - timePlaceable.width - spacingPx,
y = constraints.maxHeight - timePlaceable.height
)
}
}
}