컴포즈는 렌더링될 때, Composition -> Layout(Measurement, Placement) -> Drawing단계를 거친다.
[Composition]
Composition단계일 땐, 컴포저블 함수의 계층구조를 파악하여 이를 Tree형태로 나타내는데, 이를 Layout Node Tree라고 부른다. 또한 이때, 하나의 Node는 Layout
이라는 컴포저블 함수가 기준이 된다. 즉, Layout
컴포저블 함수를 단위로, 컴포저블 함수의 계층을 구조화해 이를 Tree형태로 나타내게 된다.
[Layout]
Root단계의 컴포저블 함수가 우선적으로 자식 컴포저블 함수의 측정(높이/너비)과 배치(x, y좌표)를 지시한다. 자식 컴포저블 함수들의 측정과 배치가 완료되면 이를 상위 컴포저블 함수에 보고하게 되고, 이러한 작업의 반복하여 결국, Root컴포저블 함수 또한 측정과 배치작업이 완료된다. 즉, 하위 컴포저블 함수의 측정과 배치 작업이 완료되면, 비로소 상위 컴포저블 함수의 작업 또한 완료되며, 최종적으로 Root컴포저블 함수의 최종 측정/배치 작업이 끝나게 된다. 이를통해 비로소 하나의 화면의 레이아웃 요소들의 높이와 너비 요소들이 결정된다.
[Drawing]
그 후,Drawing단계에선 결정된 높이/너비, x,y좌표를 기준으로 UI에 표시될 수 있게 해당 요소들을 그리는 작업이다. 아래 코드는 Layout
을 사용하여 기초적인 UI를 그리는 코드이다.
@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) }
}
}
}
참고 :
[부모로부터 받아온 제약을 자식에게 전달]
SubcomposeLayout
은 부모로부터 제약사항을 받아와 자식 컴포저블 함수에게 이를 전달한다. 이를 가장 잘 나타내는 게, LazyColumn
이 있는데, 디바이스 크기의 너비/높이를 SubcomposeLayout
으로 전달 후, 아이템들(하위 컴포저블 함수)의 높이와 너비를 계산하고 화면에 표시될 수 있는 아이템 수를 계산하여 로딩 가능한 아이템들만 로딩하게 된다.
즉,
LazyColumn
은 부모로부터 제약사항을 받아올 수 있기에, 아이템들을 Lazy하게 로딩할 수 있으며,
Column
는 그럴 수 없기에 Lazy한 로딩이 안되는 것이다.
가장 이해하기 쉬운 예시로 BoxWithConstraint
가 있다. 해당 컴포저블 함수는 SubcomposeLayout
로 구현되었을 뿐만 아니라, 부모의 높이와 너비와 같은 제약사항을 받아올 수 있다.
@Composable
@UiComposable
fun BoxWithConstraints(
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
content:
@Composable @UiComposable BoxWithConstraintsScope.() -> Unit
) {
val measurePolicy = rememberBoxMeasurePolicy(contentAlignment, propagateMinConstraints)
SubcomposeLayout(modifier) { constraints ->
val scope = BoxWithConstraintsScopeImpl(this, constraints)
val measurables = subcompose(Unit) { scope.content() }
with(measurePolicy) { measure(measurables, constraints) }
}
}
fun main() {
BoxWithConstraints {
maxWidth
maxHeight
minWidth
maxWidth
}
[자식의 제약을 또 다른 자식이 사용]
또 다른 특징으로, 첫 번째 아이템에 적용됐던 제약 사항을 두 번째 아이템에도 적용할 수 있다는 것이다. SubcomposeLayout
은 내부 구현체를 보면 알 수 있듯, 기존 아이템들의 제약사항을 모두 받아올 수 있다. 이를 토대로, 새로운 아이템을 구성할 때, 기존 아이템에 적용됐던 높이와 너비를 적용해 새롭게 구성할 수 있다.
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.unit.IntSize
// enum class SlotsEnum { Main, Dependent }
SubcomposeLayout { constraints ->
val mainPlaceables = subcompose(SlotsEnum.Main, mainContent).map { it.measure(constraints) }
val maxSize =
mainPlaceables.fold(IntSize.Zero) { currentMax, placeable ->
IntSize(
width = maxOf(currentMax.width, placeable.width),
height = maxOf(currentMax.height, placeable.height)
)
}
layout(maxSize.width, maxSize.height) {
mainPlaceables.forEach { it.placeRelative(0, 0) }
subcompose(SlotsEnum.Dependent) { dependentContent(maxSize) }
.forEach { it.measure(constraints).placeRelative(0, 0) }
}
}
아래와 같은 특징으로 위 둘의 사용을 선택하면 좋다.
[Layout]
[SubcomposeLayout]
firstVisibleIndex
or firstVisibleOffset
)