언제나 Compose의 기본 API만을 사용해가며 레이아웃을 개발할 순 없다. 우리가 익히 알고 있는 Text
, Button
, Column
, Row
등, 기본 Compose API만으로 아래와 같은 화면을 개발하려면 분명 쉽지 않은게 사실이다. 이에 대한 좋은 해결책 중 하나로, Custom Layout
사용을 고려해볼 수 있다.
출처 : Google-JetLagged-Sample App
그러기 위해서 Compose함수의 기본 프레임 렌더링 3단계를 먼저 알 필요가 있다. 단계로는 크게 Composition
- Layout
- Draw
가 있으며, 어떤 화면을 그릴지?(What To Show) 레이아웃의 크기와 위치는 어떻게 할지?(How To Place) 어떻게 그릴지?(How To Render It)를 각각 결정한다.
출처 : 안드로이드 공홈
앱 하나의 화면이 그려지기 위해선 먼저 'Layout Node Tree'가 그려져야 한다. (출처 : 안드로이드 공홈) 위 소제목에도 적어놓았듯이, 이 Tree를 생성하여, What To Show, 즉, 한 화면에 어떤 컴포즈 함수 요소들을 그릴지 결정한다. 이렇게 그려진 'Layout Node Tree'는 추후 '2단계 : Layout'단계때 사용되며, 해당 컴포즈 함수들이 사이즈와 배치 위치가 결정된다.
추가로 compose API에서 제공해주는 여럿 컴포즈 함수들(ex. Text
, TextField
, Column
, Row
, Button
...)이 있다. 이들은 위 이미지와 마찬가지로 'Layout Node Tree'에 당연히 포함이 되는데, 어떻게 그럴 수 있을까?
'Layout Node Tree'에 등록되기 위해선, Layout()
컴포즈 함수를 내부적으로 구현하고 있어야만 하며, 1개의 Layout()
컴포즈 함수가 'Layout Node'의 1개의 단위가 된다. 즉, 위해서 말한 컴포저블 함수들은 모두 내부적으로 Layout()
컴포즈 함수를 구현하고 있다는 뜻이고, 'Layout Node Tree'에 포함될 수 있으며 컴포즈 화면의 구성요소로 포함될 수 있는 것이다.
1단계에서 만들어진 'Layout Node Tree'를 수신받은 후, 컴포즈 함수의 사이즈를 '측정'하고 x, y좌표를 사용한 '배치'를 수행하는 단계이다. (출처 : 안드로이드 공홈) 위 소제목에도 적어 놓았듯이, 'Layout Node Tree'를 사용하여, Where To Show, 즉, 한 화면의 컴포즈 함수 요소들 크기를 정하고 배치를 수행한다. 이렇게 최종적으로 '측정'과 '배치'가 끝나면 '3단계 : Drawing'단계때 최종적으로 UI가 그려지게 된다.
만약, Custom한 Layout을 구현한다 헀을 때, 아래의 순서로 가능하다.
Layout()
을 호출 및 measurePolicy
파라미터 내 '측정'과 '배치'코드를 작성 준비measureable
파라미터를 통해 measure()
를 호출 및 placeable()
를 반환받음MeasureScope.layout()
호출 및 placementBlock
내에서 '배치'를 준비placementBlock
파라미터 안에 placeable.place()
를 호출하여 측정을 수행이를 Sample Code로 나타내면 아래와 같다.
@Composable
fun CustomLayout(
content: @Composable () -> Unit,
// …
) {
// 1. `Layout()`을 호출 및 `measurePolicy`파라미터 내 '측정'과 '배치'코드를 작성 준비
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
// 1.1. 측정 : `measureable`파라미터를 통해 `measure()`를 호출 및 `placeable()`를 반환받음
val placeables = measurables.map { measurable ->
// Returns a placeable
measurable.measure(constraints)
}
// 1.2. `MeasureScope.layout()` 호출 및 `placementBlock` 내에서 '배치'를 준비
layout(totalWidth, totalHeight) {
// 1.3. 배치 : `placementBlock`파라미터 안에 `placeable.place()`를 호출하여 측정을 수행
placeables.map { it.place(xPosition, yPosition) }
}
}
}
하나의 레이아웃의 요소를 변경할 수 있는 방법으로 Modifier.layout()
을 사용할 수도 있다.
아래와 같은 상황이 발생했다 가정해보자.
Column()
가 가진 4개의 자식 컴포즈 함수 중, 단 3번째 자식만 부모의 제약을 없애고 싶을 때
이런 상황엔 기존 Layout()
메서드와 사용법이 유사한 Modifier.layout()
을 사용함으로써 3번째 자식 컴포즈 함수의 '사이즈'를 변경할 수도 있다
@Composable
fun LayoutModifierExample() {
Column(
modifier = Modifier
.fillMaxWidth()
.background(Color.LightGray)
.padding(40.dp)
) {
Element()
Element(modifier = Modifier.layout { measurable, constraints ->
// step1. '측정' 수행
val placeable = measurable.measure(
constraints.copy(
maxWidth = constraints.maxWidth + 80.dp.roundToPx()
)
)
layout(placeable.width, placeable.height) {
// step2. '배치' 수행
placeable.place(0, 0)
}
})
Element()
Element()
}
}
1단계에서 'Layout Node Tree'가 그려졌고, 2단계에서 '레이아웃의 '사이즈'와 '위치'가 정해졌다.
3단계에서는 이를 토대로 'Layout Node Tree'를 재순회함과 동시에 레이아웃의 '사이즈'와 '위치'값을 토대로 UI를 최종적으로 그리게 된다.