미리 정의된 레이아웃 사용
고려해야 할 세가지 축
- 수평(Horizontal)
- 수직(Vertical)
- 스택(Stacked)
Row() - 가로
Column() - 세로
Box() & BoxWithConstraint() - 콘텐츠를 맨위에 쌓음.
CheckBox() - 현재 상태(checked)와 선택하면 호출되는 람다(onCheckedChange)를 받는다
- 함수 작성시점에 레이블 전달할 수 없다.
- 대신 CheckBox(), Text()를 Row()안에 포함해 유사한 결과물 만들 수 있다.
CheckBoxWithLabel()은 MutableState<Boolean>을 받는다.
- onCheckedChange 내부에서 값이 변경되면 다른 컴포저블 함수를 재구성해야 하기 때문이다
제약 조건을 기반으로 하는 레이아웃 생성
코드를 단순하고 깔끔하게 만들고자 Box(), Row(), Cloumn()을 중첩하는 행위를 제한하고 싶을 수 있다.
ConstraintLayout()
- 도메인 특화 언어를 사용해 다른 요소와 연관있는 UI요소의 위치와 크기를 정의
- 자신과 관련된 참조를 갖고 있어야 하며 createRefs()를 사용해 생성
- 제약 조건은 constrainAs() 변경자를 사용해 제공
- 변경자의 람다 표현식은 ConstrainScope를 받음
- start, top, bottom과 같은 프로퍼티를 포함
- 이 프로퍼티는 다른 컴포저블 위치와 연결되는 위치를 정의하기 때문에 앵커라 부름
- parent는 ConstraintLayout()을 의미
ConstraintLayout()의 동작 방식
- 컴포저블의 앵커를 다른 앵커와 연결함으로써 컴포저블 함수 간 제약 관계를 형성
- 이러한 연결은 참조 기반으로 한다. 참조를 설정하려면 createRefs()를 호출해야만 한다.
Box()방식
- 또 다른 Box를 쌓아 올려야 했다.
Constraint()방식
- 세개의 박스가 동일한 부모(ConstraintLayout())를 공유하게 된다.
→ 이는 컴포저블 함수 개수를 줄이고 코드를 더욱 깔끔하게 만드는 효과
단일 측정 단계의 이해
레이아웃은 콘텐츠의 크기를 얻거나 측정하고 나면 자식(콘텐츠)을 배치할 것이다.
Column() 코드
@Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
) {
val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
Layout(
content = { ColumnScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
- measurePolicy에 값을 할당하는 부분을 제외하면
- content, measurePolicy, modifier를 Layout에 전달해 호출할 뿐이다.
- measurePolicy 변수는 MeasurePolicy 인터페이스의 구현체를 참조
- columnMeasurePolicy()를 호출한 결과값이 이에 해당
측정 정책 정의
columnMeasurePolicy()
- verticalArrangement와 horizontalAlignment값에 따라
- DefaultColumnMeasurePolicy나 rowColumnMeasurePolicy()의 결과를 반환
- DefaultColumnMeasurePolicy는 rowColumnMeasurePolicy를 호출
- rowColumMeasurePolicy는 MeasurePolicy를 반환
MeasurePolicy
- androidx.compose.ui.layout에 포함
- 레이아웃을 어떻게 측정하고 배치할지 정의
- 떄문에 미리 정의된 (Box(), Row(), Column()) 레이아웃과 커스텀 레이아웃의 기본 구성요소가 된다.
- 가장 중요한 함수는 MeasureScope의 확장 함수인 measure()
- measure()
- List<Measurable>, Constraints 두 개의 매개변수를 받는다.
- 리스트 : 자식 레이아웃을 나타낸다
- 이러한 함수는 Measurable, measure()을 사용해 측정할 수 있다.
- 자식 레이아웃이 확장하고자 하는 크기를 나타내는 Placeable 인스턴스 반환
MeasureScope.measure()
- MeasureResult 인스턴스를 반환
- 이 인터페이스는 다음의 컴포넌트를 정의
- 레이아웃의 크기(width, height)
- 정렬선(aligmentLines)
- 자식 요소를 배치하기 위한 로직(placeChildren())
UI 복잡도에 따라 자식 레이아웃이 경계안에 잘 맞지 않는다는 것을 발견할 수도 있다.
- 이 때 다른 측정 환경설정을 전달해 자식 레이아웃을 다시 측정하고 싶을 수도 있다.
- Android View시스템에서는 자식 레이아웃을 다시 측정하는것이 가능하지만 성능 저하를 일으킬 수 있다.
- 때문에 컴포즈에서 레이아웃은 콘텐츠를 한번만 측정한다.
- 다시 측정하려고 시도할 경우 예외 발생
반면
레이아웃은 자식 레이아웃의 고유크기를 질의할 수 있다.(부모가 자식크기를 물어봐서 알수 있다는 것인듯)
이를 사용해 크기나 위치를 지정.
MeasurePolicy에서는 IntrinsicMeasureScope에 대한 네가지 확장 함수를 정의하고 있다.
- minIntrinsicWidth()
- maxIntrinsicWidth()
- minIntrinsicHeight()
- maxIntrinsicHeight()
: 주어진 특정 (높이 or 너비)에서 최소 최대 (너비 or 높이)값 반환
minIntrinsicWidth을 예로 파악해보기
fun IntrinsicMeasureScope.minIntrinsicWidth(
measurables: List<IntrinsicMeasurable>,
height: Int
): Int {
val mapped = measurables.fastMap {
DefaultIntrinsicMeasurable(it, IntrinsicMinMax.Min, IntrinsicWidthHeight.Width)
}
val constraints = Constraints(maxHeight = height)
val layoutReceiver = IntrinsicsMeasureScope(this, layoutDirection)
val layoutResult = layoutReceiver.measure(mapped, constraints)
return layoutResult.width
}
- 해당 함수는 자식요소의 목록과 height값을 받는다.
- 각 자식요소는 DefaultIntrinsicMeasurable 인스턴스로 변환되고, 이 클래스는 Measurable 인터페이스를 구현하므로 measure() 구현체를 제공하게 된다.
- 그리고 height값으로 최대 높이값이 정해지게 되고, 이 height에 대해 가능한 가장 작은 너비를 제공하는 FixedSizeIntrinsicPlaceable을 반환환다. 변환된 자식 요소는 IntrinsicMeasureScope의 인스턴스를 통해 측정된다고 한다.
커스텀 레이아웃 작성
@Composable
@Preview
fun CustomLayoutDemo() {
SimpleFlexBox {
for (i in 0..42) {
ColoredBox()
}
}
}
@Composable
fun SimpleFlexBox(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content,
measurePolicy = simpleFlexboxMeasurePolicy()
)
}
private fun simpleFlexboxMeasurePolicy(): MeasurePolicy =
MeasurePolicy { measurables, constraints ->
val placeables: List<Placeable> = measurables.map { measurable ->
measurable.measure(constraints)
}
layout(
constraints.maxWidth,
constraints.maxHeight
) {
var yPos = 0
var xPos = 0
var maxY = 0
placeables.forEach { placeable ->
Log.d("Test", "placeable width : ${placeable.width}")
Log.d("Test", "constraints maxWidth : ${constraints.maxWidth}")
if (xPos + placeable.width > constraints.maxWidth) {
xPos = 0
yPos += maxY
maxY = 0
}
placeable.placeRelative(
x = xPos,
y = yPos
)
xPos += placeable.width
if (maxY < placeable.height) {
maxY = placeable.height
}
}
}
}
- MeasurePolicy 구현체는 MeasureScope.reasure() 구현체를 제공해야만 한다.
- 이 함수는 MeasureResult 인터페이스의 객체를 반환
- 이를 구현할 필요는 없다 대신 layout()을 호출해야만 한다.
- 이 함수는 MeasureScope에 포함돼 있다.
- 측정한 레이아웃 크기와 Placeable.PlacementScope의 확장 함수인 PlacementBlock을 전달한다.
- 이는 부모 좌표계에서 자식을 배치하고자 placeRelative()와 같은 함수를 호출할 수 있다는 의미
- 측정 정책은 콘텐츠나 자식 에이아웃을 List<Measureable>로 전달 받는다.
참조 : 젯팩 컴포즈로 개발하는 안드로이드 UI
'코틀린 & Java > 컴포즈 Compose' 카테고리의 다른 글
컴포즈(compose) 앱 스타일링 (0) | 2024.01.29 |
---|---|
컴포즈(compose) 컴포저블 함수 상태 관리 (0) | 2023.09.05 |
컴포즈(Compose) 컴포즈 핵심 원칙 자세히 알아보기 (0) | 2023.08.16 |
컴포즈(Compose) 선언적 패러다임 이해 (0) | 2023.08.16 |
컴포즈(Compose) 앱 빌드 (0) | 2023.08.16 |