커스텀 레이아웃 구문
- 대부분의 커스텀 레이아웃 선언은 같은 표준 구조로 시작한다.
- 다음은 자식의 레이아웃 프로퍼티를 변경하지 않으며, 사용자 커스텀 레이아웃을 구성하는 템플릿 역할의 레이아웃의 선언 코드이다.
@Composable
fun DoNothingLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) {
measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach { placeable ->
placeable.placeRelative(x = 0, y = 0)
}
}
}
}
- 이 함수는 모디파이어 하나와 Slot API를 통해 표시되는 콘텐츠를 받는다.
- 함수는 이후 Layout() 컴포저블을 호출하고 그 이후에 람다를 받는다. measurables 파라미터는 콘텐츠 안에 포함된 모든 자식 요소를 포함하며, contraints 파라미터는 자식 요소에 지정될 수 있는 최대/최소 폭과 높이 값을 포함한다.
- 그 다음으로 자식을 측정하고 그 측정값은 Placeable 객체 리스트와 매핑된다.
- 마지막으로 layout() 함수를 호출하고 부모가 할당할 수 있는 최대 높이와 폭의 값을 전달한다. 이어지는 람다에서는 placeables 변수 안에 있는 모든 자식에 대해 반복하면서 부모가 지정한 기본 위치의 상대적인 위치에 각 자식을 배치한다.
커스텀 레이아웃 이용하기
@Composable
fun MainScreen() {
DoNothingLayout (Modifier.padding(8.dp)) {
Text("Text Line1")
Text("Text Line2")
Text("Text Line3")
Text("Text Line4")
}
}
@Composable
fun DoNothingLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) {
measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
layout(constraints.maxWidth, constraints.maxHeight) {
placeables.forEach { placeable ->
placeable.placeRelative(x = 0, y = 0)
}
}
}
}

- 현재 구현한 커스텀 레이아웃은 자식 요소를 재배치하지 않으므로, 위의 코드를 실행하면 4개의 Text 컴포저블이 스택으로 쌓이게 된다.
- 위의 코드에서 알 수 있듯이 컴포즈에서 함수의 선언에 @Composable 어노테이션과 함께 람다 함수가 매개변수로 존재한다면, 함수 호출 시 해당 람다에 블록의 형태로 적절한 값을 전달해야 한다.
- 함수를 호출하여 전달한 Text 블록은 content 변수에 전달되고 이후에 measurables에 포함되며 measure 함수에 의해 크기가 측정된다.
CascadeLayout 컴포저블 만들기
- 이번에 구현할 커스텀 레이아웃은 자식 요소를 하나의 열 안에 배치하는 기능을 포함한다.
- 각 자식은 이전 자식의 폭값을 이용해 식별한다. 선택 파라미터를 이용해 각 자식 요소 사이의 간격을 지정한다.
@Composable
fun CascadeLayout(
spacing: Int = 0,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) {
measurables, constraints ->
var indent = 0
layout(constraints.maxWidth, constraints.maxHeight) {
var yCoord = 0
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
placeables.forEach{ placeable ->
placeable.placeRelative(x = indent, y = yCoord)
indent += placeable.width + spacing
yCoord += placeable.height + spacing
}
}
}
}
- spacing 파라미터를 선택 파라미터로 선언하기 위해 기본값은 0으로 설정한다.
- 특정 자식을 식별하기 위한 indent 값은 자식이 열에 추가할 때마다 증가하므로, 가장 최근의 indent를 추적하기 위한 변수도 추가한다.
- 한 자식을 바로 이전 자식의 아래 표시하도록 하기 위해 y좌푯값도 유지해야 한다.
- 위의 코드는 첫번째 자식을 0, 0 위치에 두고 그 다음 자식을 이전 자식의 오른쪽 아래 꼭짓점에 배치한다. spacing 변수의 역할은 다음 자식을 이전 자식의 오른쪽 아래 꼭짓점으로부터 얼만큼 떨어뜨려서 배치할 것인가를 결정하는 변수이다.
CascadeLayout 컴포저블 이용하기
@Composable
fun MainScreen() {
CascadeLayout(spacing = 20) {
Box(modifier = Modifier
.size(60.dp)
.background(Color.Blue))
Box(modifier = Modifier
.size(80.dp, 40.dp)
.background(Color.Red))
Box(modifier = Modifier
.size(90.dp, 100.dp)
.background(Color.Cyan))
Box(modifier = Modifier
.size(50.dp)
.background(Color.Magenta))
Box(modifier = Modifier
.size(70.dp)
.background(Color.Green))
}
}
- 위의 코드를 실행하면 아래와 같이 표시되는 것을 알 수 있다.

정리
- 커스텀 레이아웃들은 컴포즈의 Layout 함수를 중심으로 구현된 표준 템플릿 메커니즘을 이용해 만들 수 있다.
- 이 함수는 모든 자식을 포함하는 하나의 measurables 객체와 함께 부모가 허용하는 최댓값 및 최솟값을 제공하는 constraints 집합을 함께 전달한다.
- 각각의 자식은 measurables 객체에서 추출되고 커스텀 레이아웃의 요구사항을 만족하는 레이아웃 콘텐츠 영역 안의 특정 위치에 배치된다.