[젯팩 컴포즈로 개발하는 안드로이드 UI] 4장 UI 요소 배치
미리 정의된 레이아웃 사용
요소가 어디에 나타나야하는지, 어느 크기가 돼야하는지 정의해야 한다.
배열하기 위한 기본적인 레이아웃 구성
- 수평 - Row()
- 수직 - Column()
- 스택 - Box(), BoxWithConstraints()
기본 구성 요소 전환
@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))
}
}
verticalAlignment = Alignment.CenterVertically
: 열 내부에서 Checkbox와 Text를 수직선에서 가운데에 위치하도록 함state: MutableState<Boolean>
: onCheckedChange 내부에서 해당 값이 변경되면 다른 컴포저블 함수를 재구성
@Composable
fun PredefinedLayoutsDemo() {
val red = remember { mutableStateOf(true) }
val blue = remember { mutableStateOf(true) }
val green = remember { mutableStateOf(true) }
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
) {
//
}
}
.fillMaxSize()
: 사용할 수 있는 공간은 행으로 가득 채우기.padding(16.dp)
: 패딩 추가- red, blue, green 상태를 CheckboxWithLabel()로 전달
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
) {
CheckboxWithLabel(label = "red", state = red)
CheckboxWithLabel(label = "blue", state = blue)
CheckboxWithLabel(label = "green", state = green)
Box(
modifier = Modifier.fillMaxSize().padding(top = 16.dp),
) {
if (red.value) {
Box(modifier = Modifier.fillMaxSize().background(Color.Red))
}
if (blue.value) {
Box(modifier = Modifier.fillMaxSize().padding(32.dp).background(Color.Blue))
}
if (green.value) {
Box(modifier = Modifier.fillMaxSize().padding(64.dp).background(Color.Green))
}
}
}
제약 조건을 기반으로 하는 레이아웃 생성
RelativeLayout / LinearLayout 은 성능에 영향을 일으켜서 ConstraintLayout로 View 계층 구조를 평탄화하여 문제를 방지하게 됨.
Box(), Row(), Column()을 중첩하는 것을 제한하고 싶을 때 ConstraintLayout()로 구현
@Composable
fun CheckboxWithLabel2(label: String, state: MutableState<Boolean>, modifier: Modifier = Modifier) {
ConstraintLayout(
modifier = modifier.clickable {
state.value = !state.value
},
) {
val (checkbox, text) = createRefs()
Checkbox(
checked = state.value,
onCheckedChange = {
state.value = it
},
modifier = Modifier.constrainAs(checkbox) {},
)
Text(
text = label,
modifier = Modifier.constrainAs(text) {
start.linkTo(checkbox.end, margin = 8.dp)
top.linkTo(checkbox.top)
bottom.linkTo(checkbox.bottom)
}
)
}
}
- ConstraintLayout()은 도메인 특화 언어(DSL)를 사용해 UI 요소의 위치와 크기를 정의함
- ConstraintLayout() 내부에 있는 각각의 컴포저블 함수는 자신과 관련된 참조를 가져야 함
createRefs()
: 참조 생성constrainAs()
: 제약 조건을 변경자에 의해 생성ConstrainScope
- constrainAs() 변경자의 람다 표현식은 ConstrainScope을 받음
- start, top, bottom, end와 같은 프로퍼티를 생성
- linkTo로 다른 컴포저블의 위치와 연결 →
앵커
라고 부름 bottom.linkTo(checkbox.bottom)
: 텍스트의 bottom과 체크박스의 bottom이 연결
val (cbRed, cbGreen, cbBlue, boxRed, boxBlue, boxGreen) = createRefs()
- createRefs()로 제약조건 정의하는데 필요한 참조 생성
CheckboxWithLabel2(
label = "red",
state = red,
modifier = Modifier.constrainAs(cbRed) { top.linkTo(parent.top) },
)
CheckboxWithLabel2(
label = "blue",
state = blue,
modifier = Modifier.constrainAs(cbBlue) { top.linkTo(cbRed.bottom) },
)
- 제약사항 추가
if (red.value) {
Box(
modifier = Modifier
.background(Color.Red)
.constrainAs(boxRed) {
start.linkTo(parent.start)
end.linkTo(parent.end)
top.linkTo(cbGreen.bottom, margin = 16.dp)
bottom.linkTo(parent.bottom)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
},
)
}
if (blue.value) {
Box(
modifier = Modifier
.background(Color.Blue)
.constrainAs(boxBlue) {
start.linkTo(parent.start, margin = 32.dp)
end.linkTo(parent.end, margin = 32.dp)
top.linkTo(cbGreen.bottom, margin = (16 + 32).dp)
bottom.linkTo(parent.bottom, margin = 32.dp)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
},
)
}
- 제약을 boxRed에 건다면 red 체크박스가 해제되면 파란박스가 사라짐 (제약이 사라지니까..)
단일 측정 단계의 이해
측정 정책 정의
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
)
}
- measurePolicy 값 할당
- Layout()에 content, measurePolicy, modifier을 전달하여 호출
columnMeasurePolicy() 호출
@PublishedApi
@Composable
internal fun columnMeasurePolicy(
verticalArrangement: Arrangement.Vertical,
horizontalAlignment: Alignment.Horizontal
) = remember(verticalArrangement, horizontalAlignment) {
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
)
}
}
- verticalArrangement, horizontalAlignment 값에 따라
- DefaultColumnMeasurePolicy를 반환 → rowColumnMeasurePolicy()를 호출함
- rowColumnMeasurePolicy()의 결과를 반환
- rowColumnMeasurePolicy() →
MeasurePolicy
를 반환
MeasuerPolicy
레이아웃을 어떻게 측정하고 배치할지 정의하는 레이아웃
`override fun MeasureScope.measure()`
매개변수 :
measurables:List<Measurable>, constraints: Constraints
MeasurableResult 인스턴스 반환
리스트에서 각 요소는 자식 레이아웃을 나타냄.
child.measure(
)을 사용하여 측정Measurable 코드
interface Measurable : IntrinsicMeasurable { fun measure(constraints: Constraints): Placeable }
- 레이아웃이 확장하고자하는 크기를 나타내는 Placeable 인스턴스 반환
4가지 확장함수 제공
minIntrinsicWidth(), maxIntrinsicWidth()
: 주어진 특정 높이에서 레이아웃의 최소, 최대 너비 값을 반환minIntrinsicHeight(), maxIntrinsicHeight()
: 주어진 특정 너비에서 레이아웃의 최서, 최대 높이 값을 반환
minIntrinsicWidth 함수
override fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
) = MinIntrinsicWidthMeasureBlock(orientation)(
measurables,
height,
arrangementSpacing.roundToPx()
)
- 매개변수 : height, 자식 요소의 목록
커스텀 레이아웃 작성
가로세로 길이가 여러가지인 43개의 임의의 색을 입힌 박스를 생성
한줄이 꽉차면 알아서 아래로 내려가게 만들기
- 박스 생성
@Composable
fun ColoredBox() {
Box(
modifier = Modifier
.border(width = 2.dp, color = Color.Black)
.background(randomColor())
.width((40 * randomInt123()).dp)
.height((10 * randomInt123()).dp),
)
}
private fun randomInt123() = Random.nextInt(1, 4)
private fun randomColor() = when (randomInt123()) {
1 -> Color.Red
2 -> Color.Blue
else -> Color.Green
}
@Composable
@Preview
fun CustomLayoutDemo() {
SimpleFlexBox {
for (i in 0..42) {
ColoredBox()
}
}
}
- 커스텀 레이아웃 구현
@Composable
fun SimpleFlexBox(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content,
measurePolicy = simpleFlexboxMeasurePolicy()
)
}
커스텀 측정 정책 구현
private fun simpleFlexboxMeasurePolicy(): MeasurePolicy =
MeasurePolicy { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
layout(
constraints.maxWidth,
constraints.maxHeight,
) {
var yPos = 0
var xPos = 0
var maxY = 0
placeables.forEach { placeable ->
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
}
}
}
}
MeasurePolicy 구현체는 MeasureScope.measure() 구현체를 제공해야 함
MeasureResult 인터페이스의 객체 반환 : 이를 구현할 필요는 없지만, layout()를 호출해야 함
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
측정 정책은 content, 자식 리스트로 전달받음
자식 레이아웃은 배치 전에 정확히 한번만 측정됨
measurable.measure()를 호출하는 placeables의 맵을 생성해서 처리함
placeables를 반복하면서 xpos, ypos를 증가시켜 placeables의 위치를 계산함