언제나 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
를 수신받은 후, 본인과 자식의Composable함수를의 사이즈를 '측정'하고 x, y좌표를 사용한 '배치'를 수행하는 단계이다.
Tree형태의 Composable함수가 존재한다 가정해보자. 이때, 하나의 Composable함수가 자식 Composable함수를 측정한다. 그 후, 자식 Composable함수는 또 다시 자식을 측정하는데 이때 자식이 없다면, 자신의 size
를 결정하고 이를 부모 Composable함수에게 보고한다. 그 후, 부모 Composable함수가 모든 자식들로부터 size
를 보고받게 되면 자신의 size
를 결정하고 이를 또 다시 부모 Composable함수에게 보고하고 하는 순환을 반복한다.
(출처 : 안드로이드 공홈)
위 소제목에도 적어 놓았듯이, Layout Node Tree
를 사용하여, Where To Show, 즉, 한 화면의 컴포즈 함수 요소들 크기를 정하고 배치를 수행한다. 이렇게 최종적으로 '측정'과 '배치'가 끝나면 '3단계 : Drawing'단계때 최종적으로 UI가 그려지게 된다.
이때, Custom한 Layout을 구현한다면 2가지 방법이 있다. 첫 번째는 Layout
Composable함수를 사용하는 것이며, 두 번째는 Modifier.layout
확장 함수를 사용하는 것이다.
[복수개의 Composable함수를 포함한 UI의 구현]
Layout
Composable함수는 자식들을 여럿이 둘 수 있다. 이는 Column
/Row
와 같은 것들도 그렇다.
Layout()
을 호출 및 measurePolicy
파라미터 내 '측정'과 '배치'코드를 작성 준비measureables
파라미터로 측정 가능 한 하위 Composable함수를 받으며, constraints
파라미터로 상위 Composable함수의 제약을 받음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. measureables파라미터로 측정 가능 한, 하위 Composable함수를 받으며, constraints파라미터로 상위 Composable함수의 제약을 받음
// 1.2. 측정 : `measureable`파라미터를 통해 `measure()`를 호출 및 `placeable()`를 반환받음
val placeables = measurables.map { measurable ->
// Returns a placeable
measurable.measure(constraints)
}
// 1.3. `MeasureScope.layout()` 호출 및 `placementBlock` 내에서 '배치'를 준비
layout(totalWidth, totalHeight) {
// 1.4. 배치 : `placementBlock`파라미터 안에 `placeable.place()`를 호출하여 측정을 수행
placeables.map { it.place(xPosition, yPosition) }
}
}
}
[1개의 Composable함수에 대한 구현]
Modifier.layout
을 사용했을 땐, 해당 확장함수를 사용한 UI만 변경이 가능하다.
아래와 같은 상황이 발생했다 가정해보자.
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를 최종적으로 그리게 된다.