Jetpack Compose Internals를 참고하여 작성하였습니다
레이아웃 제약 조건 (Layout Constraints)
제약 조건은 부모 LayoutNode나 modifier로부터 올 수 있다
자식 레이아웃을 측정하기 위해 제약 조건을 사용한다
대부분의 레이아웃은
1. 자식에게 수정되지 않은 제약 조건을 전달하거나
2. 최소 제약 조건을 완화한다 (0으로 설정) -> Box
부모 노드나 modifier는 자식에게 크기가 얼마인지 묻기도 한다
이 경우 무한한 제약 조건을 내려보낼 수 있다 (Constraints.Infinity)
자식 컴포넌트가 자신의 크기를 결정할 수 있는 자유가 부여된다
예시) Box Composable
// 일반적으로 Box는 부모의 전체 높이를 채움
// LazyColumn 안에서는 Box는 무한 높이 제약으로 인해 Text를 감싸는 최소 크기로 조정됨
Box(modifier = Modifier.fillMaxHeight()) {
Text("test")
}
Infinity Constraints를 활용하는 LazyColumn과 LazyList
LazyList는 자식을 측정할 때 Subcomposition(SubcomposeLayout)을 사용한다
LazyList에서 아이템을 측정하기 위한 제약 조건이 생성되는 방법
LazyListMeasuredItemProvider
@OptIn(ExperimentalFoundationApi::class)
internal abstract class LazyListMeasuredItemProvider @ExperimentalFoundationApi constructor(
constraints: Constraints,
isVertical: Boolean,
private val itemProvider: LazyListItemProvider,
private val measureScope: LazyLayoutMeasureScope
) {
// 세로 스크롤(LazyColumn)에서는 높이가 무한대로 설정됨
// 가로 스크롤(LazyRow)에서는 너비가 무한대로 설정됨
val childConstraints = Constraints(
maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity,
maxHeight = if (!isVertical) constraints.maxHeight else Constraints.Infinity
)
...
}
자식을 측정할 때마다 위 제약 조건이 사용된다
- 먼저 Subcomposition(SubcomposeLayout)을 사용해 화면에 보이는 아이템만 실제로 구성한다
- 아이템을 측정할 때
- 아이템의 내용을 subcompose하여 measurable 목록을 생성
- 생성된 childConstraints를 사용하여 측정
- 높이나 너비에 대해 Infinity 제약 조건으로 인해 자식들은 자신의 높이나 너비를 선택할 수 있다
- 정확한 크기(고정 크기)를 설정하는 경우
- minWidth == maxWidth, minHeight == maxHeight로 설정
- 자식 컴포넌트가 정확한 크기를 가지도록 강제함
LookaheadLayout
Compose에서 측정과 배치에 전적으로 관련됨
Lookahead는 미리보기, 선제적 계산, 미리 예측하는 행위를 의미한다
예시) 클릭 시 가변 상태(vertical)를 기반으로 레이아웃 전환
@Composable
fun SmartBox(modifier: Modifier = Modifier) {
var vertical by remember { mutableStateOf(false) }
Box(modifier = modifier.clickable { vertical = !vertical }) {
if (vertical) {
Column {
Text("Text 1")
Text("Text 2")
}
} else {
Row {
Text("Text 1")
Text("Text 2")
}
}
}
}
Text들은 완전히 동일하기에 공유 요소(shared element)가 되고, 쉽게 애니메이션 될 수 있으면 더 좋을 것이다
또한 애니메이션 중이나 후에 상태를 잃지 않고 재사용하도록 movableContentOf를 사용하는 것도 좋을 것이다
@Composable
fun SmartBox(modifier: Modifier = Modifier) {
var vertical by remember { mutableStateOf(false) }
val text1 = remember {
movableContentOf {
Text("Text 1")
}
}
val text2 = remember {
movableContentOf {
Text("Text 2")
}
}
LookaheadScope {
Box(modifier = modifier.clickable { vertical = !vertical }) {
AnimatedContent(
targetState = vertical,
transitionSpec = {
fadeIn() togetherWith fadeOut()
}
) { isVertical ->
if (isVertical) {
Column {
text1()
text2()
}
} else {
Row {
text1()
text2()
}
}
}
}
}
}
위 예제에서는 LookaheadScope를 사용하지 않아도 차이가 나지 않는다
LookaheadScope은 변경될 때 직접적이거나 간접적인 자식들의 새로운 크기와 위치를 미리 계산할 수 있다
각 자식에게 측정/배치 단계에서 미리 계산된 값을 관찰할 수 있도록 하고 이 값을 사용해 시간이 지남에 따라 점진적으로 변화하도록 자신의 크기와 위치를 재조정할 수 있도록 한다
레이아웃을 사전 계산하는 방법으로서의 LookaheadLayout (Yet another way of pre-calculating layouts)
SubcomposeLayout, 고유 크기 측정(intrinsics), LookaheadLayout의 차이점
- SubcomposeLayout
- 사전배치(pre-layout)보다 조건부 composition에 초점
- 측정 시간까지 composition을 지연시킨다
- 측정, 배치 단계보다 훨씬 더 많은 비용이 들어 레이아웃의 사전 계산에 사용하는 것을 추천하지 않음
- LazyList ..
- 고유 크기 측정 (Intrinsics)
- Subcomposition보다 효율적이고 내부적으로 LookaheadLayout과 유사
- 얻어진 값을 사용해 실제 측정을 수행하기 위한 잠정적인 계산
- 3개의 자식이 있는 행의 경우, 가장 키가 큰 자식의 높이에 맞추기 위해 모든 자식의 고유한 크기를 측정하기 때문
- LookaheadLayout
- 자동 애니메이션을 위해 자식의 크기와 위치를 정확하게 사전 계산하는 데 사용
- 사전 계산된 크기에 기반한 배치(placement) 계산도 수행
- 트리가 변경되지 않는 한 사전 계산을 피한다
LookaheadLayout의 동작 원리 (How it works)
측정과 배치에 대한 사전 단계를 수행해 일반 측정/배치 단계 동안 미리 계산된 값을 사용해 매 프레임마다 노드를 업데이트할 수 있게 한다
사전 측정 단계는
- 트리 변경이나 State 변경으로 레이아웃이 변경될 때만 수행
- 모든 레이아웃 애니메이션이 무시된다
- 애니메이션이 이미 완료된 것처럼 수행 (미리 계산)
미리 계산된 데이터를 사용할 수 있도록 LookaheadLayoutScope 사용
- LookaheadLayout은 content 람다를 LookaheadLayoutScope 스코프에서 실행
- LookaheadLayoutScope는 자식에게 사전 계산된 정보에 접근할 수 있는 modifier 제공
- Modifier.intermediateLayout(deprecated)
- 현재는 Modifier.approachLayout으로 대체됨
- modifier가 있는 레이아웃이 다시 측정될 때마다 호출
- 레이아웃의 새로운 미리 계산된 크기에 접근할 수 있고 그 크기를 기반으로 예상되는 중간 레이아웃 생성
- Modifier.onPlaced(deprecated)
- onPlaced는 삭제됨
- modifier가 있는 레이아웃의 재배치시에 호출
예시) Sample 코드
fun Modifier.animateConstraints(
sizeAnimation: DeferredTargetAnimation<IntSize, AnimationVector2D>,
coroutineScope: CoroutineScope
) =
this.approachLayout(
isMeasurementApproachInProgress = { lookaheadSize ->
// Update the target of the size animation.
sizeAnimation.updateTarget(lookaheadSize, coroutineScope)
// Return true if the size animation has pending target change or hasn't finished running.
!sizeAnimation.isIdle
}
) { measurable, _ ->
// First, update the target of the animation, and read the current animated size.
val (width, height) = sizeAnimation.updateTarget(lookaheadSize, coroutineScope)
// Then create fixed size constraints using the animated size
val animatedConstraints = Constraints.fixed(width, height)
// Measure child with animated constraints.
val placeable = measurable.measure(animatedConstraints)
layout(placeable.width, placeable.height) { placeable.place(0, 0) }
}
Lookaheadlayout의 내부 동작 (Internals of Lookaheadlayout)
초기 측정과 배치 단계 동안 선제적 측정과 선제적 배치(주황색)를 포함해 어떤 일이 발생하는지 보여주는 다이어그램
측정 단계
- LayoutNode가 처음 측정될 때, 선제적 측정 단계를 먼저 시작하기 위해 루트에 배치되었는지 확인
- LayoutNode는 선제적 측정을 시작하기 위해 lookaheadPassDelegate measure 함수를 호출
- 노드에 들어오는 모든 선제적 측정/배치 요청을 처리
- 외부 LayoutNodeWrapper의 LookaheadDelegate를 사용해 선제적 측정을 실행
- 모든 단계에서 LookaheadDelegate를 통해 선제적 측정이 실행된다
- 선제적 측정이 완료되거나 노드가 LookaheadLayout의 루트가 아닌 경우 일반 측정 단계가 실행된다
- 실제 측정 단계이기에 LookaheadPassDelegate가 아닌 MeasurePassDelegate가 사용된다
배치 단계
- 측정 단계와 동일하며 같은 delegate가 사용된다
- placeAt 함수를 호출하는 목적이 다르다
- 일반 배치 단계에서는 노드와 자식들을 배치하기 위해 호출
- 선제적 배치 단계에서는 선제적 위치를 계산하기 위해 호출
'Compose' 카테고리의 다른 글
[ComposeInternals] 컴포즈 UI (Compose UI) - (4) Modifier (0) | 2025.04.13 |
---|---|
[ComposeInternals] 컴포즈 UI (Compose UI) - (2) Measure(측정) (0) | 2025.04.04 |
[ComposeInternals] 컴포즈 UI (Compose UI) - (1) (0) | 2025.04.03 |
[ComposeInternals] 컴포즈 런타임 (ComposeRuntime) - (3) Recomposer (0) | 2025.03.30 |
[ComposeInternals] 컴포즈 런타임 (ComposeRuntime) - (2) Composition (0) | 2025.03.30 |