프로젝트 진행 중에 커스텀 레이아웃이 필요했습니다.
스크롤이 가능한 Column에서 staggered layout이 필요했는데, LazyVerticalStaggeredGrid을 사용하기엔 중첩 스크롤 문제가 발생했기 때문입니다.
그래서 이러한 상황에서 사용 가능한 staggered layout을 구현하기로 했습니다.
이를 위해 커스텀 레이아웃을 구현하기 위해 기본적으로 이해가 필요한 부분이 있었기 때문에 이에 대해 포스팅합니다.
Layout
Compose에서 기본적으로 제공하고 있는 Scaffold, Surface, Column, Row 등은 Layout를 기본적으로 사용하여 구현되어 있습니다.
다음은 하나의 예로, 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
)
}
구글 문서의 Jetpack Compose > Custom layouts에서도 Layout 컴포저블을 사용하여 커스텀 레이아웃을 구현하는 방법을 가이드하고 있습니다.
https://developer.android.com/develop/ui/compose/layouts/custom
따라서 우리는 커스텀 레이아웃을 구현하기 위해 Layout 컴포저블을 이해할 필요가 있습니다.
Layout 컴포저블의 코드는 다음과 같습니다. 다른 오버로딩된 함수들도 있지만, 이 함수로 동작하게 됩니다.
@UiComposable
@Composable
inline fun Layout(
content: @Composable @UiComposable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
val compositeKeyHash = currentCompositeKeyHash
val localMap = currentComposer.currentCompositionLocalMap
val materialized = currentComposer.materialize(modifier)
ReusableComposeNode<ComposeUiNode, Applier<Any>>(
factory = ComposeUiNode.Constructor,
update = {
set(measurePolicy, SetMeasurePolicy)
set(localMap, SetResolvedCompositionLocals)
@OptIn(ExperimentalComposeUiApi::class)
set(compositeKeyHash, SetCompositeKeyHash)
set(materialized, SetModifier)
},
content = content
)
}
내부의 코드는 잠시 잊읍시다. 중요한 것은 MeasurePolicy와 content 함수를 우리가 구현해야 된다는 것 입니다.
content는 직관적으로 Layout에 그려질 컴포저블 함수임을 이해할 수 있습니다.
MeasurePolicy에 대해 알아봅시다.
MeasurePolicy
fun interface MeasurePolicy {
fun MeasureScope.measure(
measurables: List<Measurable>,
constraints: Constraints
): MeasureResult
...
}
MeasurePolicy는 레이아웃의 측정과 동작을 정의하는 인터페이스입니다.
MeasureScope 에서 레이아웃에 그려질 컴포저블의 크기나 위치 등을 측정하여 결과를 반환하는 measure 함수가 존재합니다.
실제 인터페이스에는 intrinsic 사이즈 측정과 관련된 확장함수도 존재합니다.
커스텀 레이아웃을 만들면 내부에 그려질 레이아웃들의 크기와 위치 등을 직접 계산하게 되므로 이 인터페이스를 구현하게 됩니다.
Layout 컴포저블을 사용한다면, 다음과 같은 람다로 구현할 수 있습니다. (SAM)
Layout(
modifier = modifier,
content = content,
) { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
...
}
코드를 보면 여러 파라미터들이 나오는데 결국 뭔가 측정 가능한 것들로 제약조건에 따라 측정후에 배치할 수 있는 요소들을 얻는 것처럼 보입니다.
이것을 이해하기 위해 Measurable, Constraints, Placeable 에 대해서 알아봅시다.
Measurable
interface Measurable : IntrinsicMeasurable {
fun measure(constraints: Constraints): Placeable
}
Measurable은 측정 가능한 컴포저블 요소를 말합니다.
요소가 그려지기 위한 제약조건을 바탕으로 측정을 수행하고 Placeable을 반환합니다.
Constraints
@Immutable
@JvmInline
value class Constraints(
@PublishedApi internal val value: Long
) {
...
}
Constraints는 컴포저블이 그려지는 레이아웃의 제약조건을 나타내는 클래스입니다. 내부에 꽤 많은 필드가 존재하는데, 레이아웃의 최소, 최대 너비와 높이와 같은 필드들을 가지고 있습니다.
하위 컴포저블 요소들을 그려낼 때 이 제약조건 내에서 측정될 수 있도록 구현하게 됩니다.
Placeable
abstract class Placeable : Measured {
...
}
Placeable은 상위 레이아웃에 의해 배치될 수 있는 하위 레이아웃을 의미합니다.
Placeable은 일반적으로 Measuable의 측정 결과에 의해 생성됩니다.
커스텀 레이아웃 구현
커스텀 레이아웃을 구현하기 위해선, 다음과 같은 절차가 필요할 수 있음을 이해할 수 있습니다.
첫번째, Layout 컴포저블에 원하는 레이아웃에 따라 MeasurePolicy를 구현해야 합니다.
두번째, Measureble 리스트의 항목들을 측정해서 그 결과로 Placeable 리스트를 얻습니다.
세번째, Placeable 리스트를 레이아웃에 배치합니다.
이에 따라 코드를 작성하면 다음과 같은 형태입니다.
@Composable
fun MyLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content,
) { measurables, constraints ->
val placeables = measurables.map { measurable ->
measurable.measure(constraints)
}
val layoutWidth = ...
val layoutHeight = ...
layout(layoutWidth, layoutHeight) {
placeables.forEach { placeable ->
val x = ...
val y = ...
placeable.placeRelative(x, y)
}
}
}
}
원하는 레이아웃의 형태에 따라 로직을 수정하는 것을 제외하면 위와 같은 틀에서 코드를 작성하게 될 것입니다.
예전 안드로이드 뷰에 비해 커스텀 레이아웃을 구현하는 것이 굉장히 간편해진 것 같습니다.
어렵지 않으니 필요한 커스텀 레이아웃이 있다면 한번 구현해보면 좋겠습니다.
'개발 > Android' 카테고리의 다른 글
[Android] 안드로이드 메모리 누수에 관하여 (0) | 2025.02.04 |
---|---|
Kotlin Coroutines, 에러 처리와 SupervisorJob (0) | 2025.01.29 |
Kotlin Coroutines, 구조화된 동시성 (Structed Concurrency) (0) | 2025.01.27 |
[Android] Retrofit Call Adapter를 활용해서 효과적인 에러 핸들링 하기 (0) | 2025.01.24 |
[Android] Compose Multiplatform + Circuit + Koin (0) | 2025.01.12 |