
Jetpack Compose는 기준이 되는 축을 따라 컨텐츠를 배열하는 기본적인 레이아웃을 제공한다.
@Composable
fun CheckboxWithLabel(label: String, state: MutableState<Boolean>) {
Row(
modifier = Modifier.clickable {
state.value = !state.value
}, verticalAlignment = Alignment.CenterVertically // 수직 가운데 정렬
) {
Checkbox(
checked = state.value,
onCheckedChange = {
state.value = it
}
)
Text(
text = label,
modifier = Modifier.padding(start = 8.dp)
)
}
}
@Composable
@Preview
fun PredefinedLayoutsDemo() {
val red = remember { mutableStateOf(true) }
val green = remember { mutableStateOf(true) }
val blue = remember { mutableStateOf(true) }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
CheckboxWithLabel(
label = stringResource(id = R.string.red),
state = red
)
CheckboxWithLabel(
label = stringResource(id = R.string.green),
state = green
)
CheckboxWithLabel(
label = stringResource(id = R.string.blue),
state = blue
)
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 16.dp)
) {
if (red.value) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Red)
)
}
if (green.value) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(32.dp)
.background(Color.Green)
)
}
if (blue.value) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(64.dp)
.background(Color.Blue)
)
}
}
}
}
안드로이드의 전통적인 View기반 세계에서 ConstraintLayout으로 View 계층 구조를 평탄화한 것처럼 Box(), Row(), Column()을 중첩하는 행위를
제한하기 위한 방법
Compose에서는 기본 제공은 아니고 gradle 추가 필요함
implementation 'androidx.constraintlayout:constraintlayout-compose:1.0.1'
@Stable
fun Modifier.constrainAs(
ref: ConstrainedLayoutReference,
constrainBlock: ConstrainScope.() -> Unit
) = this.then(ConstrainAsModifier(ref, constrainBlock))
@ExperimentalComposeUiApi
@Composable
@Preview
fun ConstraintLayoutDemo() {
val red = remember { mutableStateOf(true) }
val green = remember { mutableStateOf(true) }
val blue = remember { mutableStateOf(true) }
ConstraintLayout(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
val (cbRed, cbGreen, cbBlue, boxRed, boxGreen, boxBlue) = createRefs() // 여기서 제약조건의 참조가 될 값들을 선언/초기화
CheckboxWithLabel(
label = stringResource(id = R.string.red),
state = red,
modifier = Modifier.constrainAs(cbRed) {
top.linkTo(parent.top) // 맨 위에 위치
}
)
CheckboxWithLabel(
label = stringResource(id = R.string.green),
state = green,
modifier = Modifier.constrainAs(cbGreen) {
top.linkTo(cbRed.bottom) // cbRed의 아래에 위치
}
)
CheckboxWithLabel(
label = stringResource(id = R.string.blue),
state = blue,
modifier = Modifier.constrainAs(cbBlue) {
top.linkTo(cbGreen.bottom) // cbGreen의 아래에 위치
}
)
// Dimension.fillToConstraints
if (red.value) {
Box(
modifier = Modifier
.background(Color.Red)
.constrainAs(boxRed) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(cbBlue.bottom, margin = 16.dp)
bottom.linkTo(parent.bottom)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
}
)
}
if (green.value) {
Box(
modifier = Modifier
.background(Color.Green)
.constrainAs(boxGreen) {
start.linkTo(parent.start, margin = 32.dp)
end.linkTo(parent.end, margin = 32.dp)
top.linkTo(cbBlue.bottom, margin = (16 + 32).dp)
bottom.linkTo(parent.bottom, margin = 32.dp)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
}
)
}
if (blue.value) {
Box(
modifier = Modifier
.background(Color.Blue)
.constrainAs(boxBlue) {
start.linkTo(parent.start, margin = 64.dp)
end.linkTo(parent.end, margin = 64.dp)
top.linkTo(cbBlue.bottom, margin = (16 + 64).dp)
bottom.linkTo(parent.bottom, margin = 64.dp)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
}
)
}
}
}
Column()에서 2개의 Text를 넣으면, 첫 번째 Text의 길이에 따라 Column의 최대 길이가 달라지는 영향이 생긴다. -> 제약조건
@Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
) {
val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
Layout(
content = { ColumnScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
@PublishedApi
@Composable
internal fun columnMeasurePolicy(
verticalArrangement: Arrangement.Vertical,
horizontalAlignment: Alignment.Horizontal
) = remember(verticalArrangement, horizontalAlignment) {
// DefaultColumnMeasurePolicy 혹은 rowColumnMeasurePolicy
if (verticalArrangement == Arrangement.Top && horizontalAlignment == Alignment.Start) {
DefaultColumnMeasurePolicy
} else {
rowColumnMeasurePolicy(
orientation = LayoutOrientation.Vertical,
arrangement = { totalSize, size, _, density, outPosition ->
with(verticalArrangement) { density.arrange(totalSize, size, outPosition) }
},
arrangementSpacing = verticalArrangement.spacing,
crossAxisAlignment = CrossAxisAlignment.horizontal(horizontalAlignment),
crossAxisSize = SizeMode.Wrap
)
}
}
여기서 MeasurePolicy 인터페이스가 레이아웃을 어떻게 측정하고 배치할지 정의하기 때문에
미리 정의된 레이아웃(Row, Column, Box)과 커스텀 레이아웃의 기본 구성 요소가 된다.
MeasureScope.measure()는 MeasureResult 인스턴스를 반환한다.
기존 Android View 시스템에서는 자식 레이아웃을 다시 측정하면 성능 저하 때문에 한 번만 측정하고 있었다.
하지만 Compose에서 Layout은 자식 Layout의 intrinsic size(고유 크기)를 query(질의)할 수 있으며, 이를 사용해 크기나 위치를 지정한다.
fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
): Int {
// measurables에서 DefaultIntrinsicMeasurable() 값에 해당되는 값들로 변경 List<Measurable>
val mapped = measurables.fastMap {
DefaultIntrinsicMeasurable(it, IntrinsicMinMax.Min, IntrinsicWidthHeight.Width)
}
val constraints = Constraints(maxHeight = height)
val layoutReceiver = IntrinsicsMeasureScope(this, layoutDirection) // 크기 측정
val layoutResult = layoutReceiver.measure(mapped, constraints)
return layoutResult.width
}
// Layout.kt
internal class DefaultIntrinsicMeasurable(
val measurable: IntrinsicMeasurable,
val minMax: IntrinsicMinMax,
val widthHeight: IntrinsicWidthHeight
) : Measurable {
override val parentData: Any?
get() = measurable.parentData
override fun measure(constraints: Constraints): Placeable { // Constraints에서 minWidth, minHeight, maxWidth, maxHeight
if (widthHeight == IntrinsicWidthHeight.Width) {
val width = if (minMax == IntrinsicMinMax.Max) {
measurable.maxIntrinsicWidth(constraints.maxHeight)
} else {
measurable.minIntrinsicWidth(constraints.maxHeight)
}
return FixedSizeIntrinsicsPlaceable(width, constraints.maxHeight) // 가장 작은 너비 제공
}
val height = if (minMax == IntrinsicMinMax.Max) {
measurable.maxIntrinsicHeight(constraints.maxWidth)
} else {
measurable.minIntrinsicHeight(constraints.maxWidth)
}
return FixedSizeIntrinsicsPlaceable(constraints.maxWidth, height) // 가장 작은 너비 제공
}
override fun minIntrinsicWidth(height: Int): Int {
return measurable.minIntrinsicWidth(height)
}
override fun maxIntrinsicWidth(height: Int): Int {
return measurable.maxIntrinsicWidth(height)
}
override fun minIntrinsicHeight(width: Int): Int {
return measurable.minIntrinsicHeight(width)
}
override fun maxIntrinsicHeight(width: Int): Int {
return measurable.maxIntrinsicHeight(width)
}
}
@Composable
@Preview
fun CustomLayoutDemo() {
SimpleFlexBox {
for (i in 0..42) {
ColoredBox()
}
}
}
@Composable
fun ColoredBox() {
Box(
modifier = Modifier
.border(
width = 2.dp,
color = Color.Black
)
.background(randomColor())
.width((40 * randomInt123()).dp)
.height((10 * randomInt123()).dp)
)
}
// Layout()을 만드는데 modifier, content 필요
@Composable
fun SimpleFlexBox(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content,
measurePolicy = simpleFlexboxMeasurePolicy()
)
}
MeasurePolicy 구현체는 MeasureScope.measure() 구현체를 제공해야만 한다.
이유는 MeasurePolicy interface 안에 MeasureScope.measure()가 선언되어 있으니까
// MeasurePolicy.kt
fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult
private fun simpleFlexboxMeasurePolicy(): MeasurePolicy =
MeasurePolicy { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
// MeasureScope.measure() 구현체를 제공하는 대신 layout() 호출 -> MeasurePolicy 인터페이스의 구현체함수 호출 -> MeasureResult 리턴함
layout(
constraints.maxWidth,
constraints.maxHeight
) {
// placementBlock: Placeable.PlacementScope.() -> Unit
var yPos = 0
var xPos = 0
var maxY = 0
// placementBlock은 placeables를 반복하면서 xPos와 yPos를 증가시켜 placeable의 위치를 계산한다.
placeables.forEach { placeable ->
// placeable이 현재 열에 완벽히 맞는지 판단 및 xPos, yPos 값 조절
if (xPos + placeable.width >
constraints.maxWidth
) {
xPos = 0
yPos += maxY
maxY = 0
}
placeable.placeRelative(
x = xPos,
y = yPos
)
xPos += placeable.width
if (maxY < placeable.height) {
maxY = placeable.height
}
}
}
}
fun Placeable.placeRelative(x: Int, y: Int, zIndex: Float = 0f) =
placeAutoMirrored(IntOffset(x, y), zIndex, null)

fun layout(
width: Int,
height: Int,
alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
placementBlock: Placeable.PlacementScope.() -> Unit
) = object : MeasureResult {
override val width = width
override val height = height
override val alignmentLines = alignmentLines
override fun placeChildren() {
Placeable.PlacementScope.executeWithRtlMirroringValues(
width,
layoutDirection,
this@MeasureScope as? LookaheadCapablePlaceable,
placementBlock
)
}
}