[Android/Compose] Layout으로 Staggered Grid 커스텀하기
Material3에서 기본으로 제공하는 LazyHorizontalStaggeredGrid를 사용하다가
내가 만들고자 하는 UI와 다르게 작동하여 커스텀하게 되었다.
비교적 쉽게 개발했기 때문에 간단히 남겨보려고 한다.
원하는 UI
Chip을 Grid에 배치하는 것이다.
Chip은 가변적인 Width를 가지기 때문에 Staggered Grid로 해결하고자 했다.
개발한 UI
2가지 방법으로 시도해보았다.
방법 1 : LazyHorizontalStaggeredGrid로 개발하기
개발한 코드
LazyHorizontalStaggeredGrid(
modifier = Modifier.fillMaxWidth().height(130.dp),
rows = StaggeredGridCells.Fixed(3),
horizontalItemSpacing = 12.dp,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(categories) { (emoji, text) ->
CategoryChip(emoji = emoji, text = text, isSelected = false) { }
}
}
- StaggeredGridCells.Fixed : HorizontalGrid인지 VerticalGrid인지에 따라 고정된 열 / 행의 수을 설정
- StaggeredGridCells.Adaptive : 적응형 크기를 설정
음...?
Fixed 100 | Fixed 200 |
Dp를 살짝씩 조정하다보니 시안과 얼추 비슷해보였다.
그래서 130% 배율로 Preview를 확인하니 역시나 UI 깨짐 현상이 보였다... 하하
그래서 Fixed가 아닌 Adaptive를 사용해보았다.
Adaptive 30 | Adaptive 100 | Adaptive 300 |
역시나 원치 않는 UI가 만들어졌다.
내가 원하는 건 스크롤이 되지 않고, 화면 크기에 맞게 알아서 줄바꿈을 하는 StaggeredGrid인데..
아마 Adaptive는 화면에 꽉찬 레이아웃이 필요할 때 사용하게 되지 않을까 싶다.
구글의 깊은 뜻이 있겠죠...
방법 2 : Custom StaggeredGrid 만들기
Custom Layout?
공식문서에서 Column의 기본 형태를 만드는 코드를 보게 되었다.
자식의 크기를 재서 layout에 x, y 좌표를 이용해서 자식을 배치하는 형태였다.
Grid 하위 자식인 Chip의 동적 크기를 안다면 x, y 좌표를 계산할 수 있겠다
다음 자식을 배치할 x 좌표가 Layout의 최대 길이를 넘으면
y 좌표에 Chip 높이를 더해 줄바꿈을 할 수 있지 않을까?
개발한 코드
위의 아이디어 그대로 코드로 만들었다.
HorizontalStaggeredGrid이기 때문에 다음 x좌표가 레이아웃의 width를 넘으면 줄바꿈을 실행한다.
@Composable
fun CustomHorizontalStaggeredGrid(
horizontalSpacing: Dp,
verticalSpacing: Dp,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(content = content , modifier = modifier) { measurables, constraints ->
val placeable = measurables.map { measurable ->
measurable.measure(constraints)
}
layout(constraints.maxWidth, constraints.maxHeight) {
var x = 0
var y = 0
placeable.forEach {
if (x + it.width > constraints.maxWidth) {
x = 0
y += it.height + verticalSpacing.roundToPx()
}
it.placeRelative(x, y)
x += it.width + horizontalSpacing.roundToPx()
}
}
}
}
처음에는 Chip에 padding과 margin을 주어 Chip 사이의 공간을 해결하려고 했지만,
재사용성이 떨어질 것 같아 (멋도 없고) horizontalSpacing, verticalSpacing 파라미터를 추가해서 해결했다.
Preview를 통해 디바이스 사이즈에 따라 Chip의 줄바꿈이 문제없이 동작하고 있는 것도 확인할 수 있었다.
100% 비율 | 115% 비율 |
Custom Layout
생각대로 개발이 되었으니, 좀 더 파헤쳐보도록 하자.
- Composition : Composable 함수를 실행하고, UI 트리를 생성함
- Layout : UI 트리를 작동하고, 노드들의 크기를 측정하고 배치함
- Drawing : UI 트리를 작동하고, 화면에 렌더링함
Layout
각 UI 요소들은 하나의 parent가 있고 여러 child로 구성된다.
각 요소들은 특정한 (x, y) 위치와 특정 width, height로 parent 안에 위치하게 된다.
→ parent는 child 요소들의 constraints를 정의한다.
- UI 요소들은 constraints 내에서 size를 정의하도록 요구됨
- constraints는 최소, 최대 width, height를 규제함
- children이 있는 경우, 각 child의 size를 측정하여 자신의 size를 결정할 수 있음
- UI 요소가 size를 결정하고 보고하면, 자신을 기준으로 child를 배치하는 방법을 정의할 수 있음
이것은 곧 UI 트리 안의 node를 배치하는 것에는 3가지 단계를 의미한다.
- children 측정하기
- children의 크기 결정하기
- children 위치시키기
해당 코드는 아이디어를 얻을 수 있었던 공식문서 내 기본 Column을 만들 수 있는 코드이다.
Layout이라는 컴포저블을 사용했고, 이 Layout은 람다로 아래 2개의 파라미터를 넘길 수 있다.
- measurable : UI element를 측정함
- constraints : composable의 constrains
@Composable
fun MyBasicColumn(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// Don't constrain child views further, measure them with given constraints
// List of measured children
val placeables = measurables.map { measurable ->
// Measure each children
measurable.measure(constraints)
}
// Set the size of the layout as big as it can
layout(constraints.maxWidth, constraints.maxHeight) {
// Track the y co-ord we have placed children up to
var yPosition = 0
// Place children in the parent layout
placeables.forEach { placeable ->
// Position item on the screen
placeable.placeRelative(x = 0, y = yPosition)
// Record the y co-ord placed up to
yPosition += placeable.height
}
}
}
}
- 해당 코드에서 measure 함수는 measurables를 통해 배치 방법을 리스트로 전달 받음
- 이때 measurable을 실행하기 위해 크기에 대한 제약을 constraints로 전달 받고 measure로 크기를 측정함
- placeable == 크기가 결정된 하위 노드 (측정이 된 노드만 배치가 가능함 + '측정 후 배치'라는 순서 보장)
- layout에 레이아웃 크기를 알려줌
- layout은 placementBlock이라는 람다로 각 아이템을 원하는 곳에 배치함
- placeable은 placeRelative로 레이아웃 내에 배치됨 (실제 화면에 표시)
좀 더 자세한 내용은 2021 Summit에서 볼 수 있었다.
다음 글에서는 레이아웃의 성능 개선 방법에 대해 정리할 예정입니다!
Reference