읽기 전
- 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
- 개인적으로 사용해보면서 배운 점을 정리한 글입니다.
Jetpack Compose의 레이아웃 코드랩에서 실습한 내용들을 정리합니다. 레이아웃 컴포저블을 사용하여 좀 더 고도화된 커스텀 레이아웃을 정의합니다. 코드랩 실습에 요구되는 시간은 대략 1시간 정도로 학습 목적을 고려하면 대략 2 - 3시간 정도를 투자해야 합니다. 분량이 너무 많아 챕터 별로 분할하여 업로드합니다.
고도화된 커스텀 레이아웃 - StaggeredGrid
Layout 컴포저블을 사용하여 기본적인 컴포저블을 재현해봤다. 좀 더 발전시켜서 복잡한 커스텀 레이아웃을 작성해보자. 코드랩에서 제시하는 커스텀 레이아웃은 Material Design 예제 프로젝트 - Owl의 지그재그형 그리드다.
가운데 배치된 지그재그형 그리드는 통상적인 Column과 Row로 구현하기에는 조금 번잡한 면이 있다. 받아오는 데이터 구조가 Column 단위로 쪼개서 Row로 나열할 수 있도록 구성되어 있으면 가능하겠으나 가급적 한 번에 모든 구획을 채워지게끔 만들고 싶다.
커스텀 레이아웃을 정의하면 데이터 구조에 대해 고민할 필요 없이 모든 항목의 높이를 측정하고 제한하여 배치할 수 있다. 재사용성을 위해 row는 커스텀할 수 있도록 매개변수로 전달하자.
@Composable
fun StaggeredGrid(
modifier: Modifier = Modifier,
rows: Int = 3,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 주어진 제약조건에 따라 자식 요소를 측정하고 배치
}
}
커스텀 레이아웃이지만 앞서 작성했듯이 Layout 컴포저블 선언 - 자식 요소 측정 - 자식 요소 배치의 구조를 띌 것이다.
이제껏 그래왔듯이, 추가적인 제약조건 정의는 하지 않고 하위 요소는 한 번만 측정할 수 있음을 유념하자.
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 각 행의 너비를 추적 - 초기값은 0
val rowWidths = IntArray(rows) { 0 }
// 각 열의 최대 높이를 추적 - 초기값은 0
val rowHeights = IntArray(rows) { 0 }
// 자식 요소에 추가 제약 없이 주어진 제약조건대로 측정
// 측정된 자식들이 담긴 리스트 반환
val placeables = measurables.mapIndexed { index, measurable ->
// 각 자식 요소 측정
val placeable = measurable.measure(constraints)
// 각 행에 대해 너비와 최대 높이 추적
// item이 배치될 행 좌표를 index % 최대 행 개수로 계산하여 균등하게 배분
val row = index % rows
// 배치 후 너비 반영
rowWidths[row] += placeable.width
// 배치 후 높이 반영(현재 높이와 배치한 아이템의 높이 비교하여 최대값 반영)
rowHeights[row] = Math.max(rowHeights[row], placeable.height)
// 자식 요소 배치
placeable
}
...
}
위 코드에서 주석 처리한 설명처럼 각 행의 너비와 높이를 추적하여 반영한다.
하위 요소의 배치 로직을 작성하였으므로 화면에 배치하기 전 전체 그리드의 너비와 높이를 결정해야 한다.
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
...
// 그리드의 너비는 가장 넓은 너비의 행과 동일하다.
val width = rowWidths.maxOrNull()
?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth)) ?: constraints.minWidth
// 그리드의 높이는 각 행의 가장 기다란 요소의 높이들을 더한 값이다.
// 높이 제약 조건으로 강제하였음
val height = rowHeights.sumOf { it }
.coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
// 각 행의 Y값은 이전 행의 높이 누적값이다.
val rowY = IntArray(rows) { 0 }
for (i in 1 until rows) {
rowY[i] = rowY[i-1] + rowHeights[i-1]
}
...
}
하위 요소의 배치 로직에 따라 각 행의 너비와 최대 높이는 확보하였으므로 해당 값들을 기반으로 그리드 레이아웃의 전체 너비와 높이를 결정한다.
마지막으로 하위 요소들을 placeable.placeRelative(x, y)를 호출하여 화면에 하위 요소를 배치한다.
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
...
// 부모 레이아웃의 사이즈 지정
layout(width, height) {
// row마다 아이템이 배치될 X좌표
val rowX = IntArray(rows) { 0 }
placeables.forEachIndexed { index, placeable ->
val row = index % rows
placeable.placeRelative(
x = rowX[row],
y = rowY[row]
)
rowX[row] += placeable.width
}
}
}
rowX 변수는 하위 아이템을 배치하면서 변경되기에 layout 함수 scope에 선언되었다. 이로써 rowX, rowY 변수를 사용하여 x, y 변수를 추적한다.
StaggeredGrid 아이템 정의
@Composable
fun Chip(modifier: Modifier = Modifier, text: String) {
Card(
modifier = modifier,
border = BorderStroke(color = Color.Black, width = Dp.Hairline),
shape = RoundedCornerShape(8.dp)
) {
Row(
modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier.size(16.dp, 16.dp)
.background(color = MaterialTheme.colors.secondary)
)
Spacer(Modifier.width(4.dp))
Text(text = text)
}
}
}
@Preview
@Composable
fun ChipPreview() {
LayoutsCodelabTheme {
Chip(text = "Hi there")
}
}
앞서 정의한 커스텀 레이아웃인 StaggeredGrid 레이아웃에 넣을 아이템 레이아웃을 정의해야 한다. 좌측에 placeholder를 정의하고 우측에 파라미터로 받은 텍스트를 표시한다. 코드대로 작성한 경우 미리보기는 다음과 같다.
StaggeredGrid의 전체 코드는 다음과 같다.
@Composable
fun StaggeredGrid(
modifier: Modifier = Modifier,
rows: Int = 3,
content: @Composable () -> Unit
) {
Layout(
modifier = modifier,
content = content
) { measurables, constraints ->
// 각 행의 너비를 추적 - 초기값은 0
val rowWidths = IntArray(rows) { 0 }
// 각 열의 최대 높이를 추적 - 초기값은 0
val rowHeights = IntArray(rows) { 0 }
// 자식 요소에 추가 제약 없이 주어진 제약조건대로 측정
// 측정된 자식들이 담긴 리스트 반환
val placeables = measurables.mapIndexed { index, measurable ->
// 각 자식 요소 측정
val placeable = measurable.measure(constraints)
// 각 행에 대해 너비와 최대 높이 추적
// item이 배치될 행 좌표를 index % 최대 행 개수로 계산하여 균등하게 배분
val row = index % rows
// 배치 후 너비 반영
rowWidths[row] += placeable.width
// 배치 후 높이 반영(현재 높이와 배치한 아이템의 높이 비교하여 최대값 반영)
rowHeights[row] = Math.max(rowHeights[row], placeable.height)
// 자식 요소 배치
placeable
}
// 그리드의 너비는 가장 넓은 너비의 행과 동일하다.
val width = rowWidths.maxOrNull()
?.coerceIn(constraints.minWidth.rangeTo(constraints.maxWidth)) ?: constraints.minWidth
// 그리드의 높이는 각 행의 가장 기다란 요소의 높이들을 더한 값이다.
// 높이 제약 조건으로 강제하였음
val height = rowHeights.sumOf { it }
.coerceIn(constraints.minHeight.rangeTo(constraints.maxHeight))
// 각 행의 Y값은 이전 행의 높이 누적값이다.
val rowY = IntArray(rows) { 0 }
for (i in 1 until rows) {
rowY[i] = rowY[i-1] + rowHeights[i-1]
}
// 부모 레이아웃의 사이즈 지정
layout(width, height) {
// row마다 아이템이 배치될 X좌표
val rowX = IntArray(rows) { 0 }
placeables.forEachIndexed { index, placeable ->
val row = index % rows
placeable.placeRelative(
x = rowX[row],
y = rowY[row]
)
rowX[row] += placeable.width
}
}
}
}
StaggeredGrid에 들어갈 텍스트 아이템들을 하드코딩된 배열로 작성하고 실제로 호출해보자.
val topics = listOf(
"Arts & Crafts", "Beauty", "Books", "Business", "Comics", "Culinary",
"Design", "Fashion", "Film", "History", "Maths", "Music", "People", "Philosophy",
"Religion", "Social sciences", "Technology", "TV", "Writing"
)
@Preview
@Composable
fun LayoutCodelabPreview() {
LayoutsCodelab()
}
@Composable
fun LayoutsCodelab() {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "LayoutsCodelab")
},
actions = {
IconButton(onClick = { /* doSomething() */ }) {
Icon(Icons.Filled.Favorite, contentDescription = null)
}
}
)
}
) { innerPadding ->
BodyContent()
}
}
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
StaggeredGrid(modifier = modifier) {
for (topic in topics) {
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
}
허나 이렇게 사용하면 preview에서 볼 수 있듯이 화면 너비를 넘어가는 경우 가로로 스크롤할 수 없다. 가로 스크롤 속성을 부여할 수 있는 Row로 래핑하고 BodyContent 컴포저블 함수의 Modifier 파라미터를 StaggeredGrid가 아니라 Row 컴포저블에 전달하여 스크롤 가능하게 구현할 수 있다.
@Composable
fun BodyContent(modifier: Modifier = Modifier) {
Row(modifier = modifier.horizontalScroll(rememberScrollState())) {
StaggeredGrid {
for (topic in topics) {
Chip(modifier = Modifier.padding(8.dp), text = topic)
}
}
}
}
참고자료
'Android' 카테고리의 다른 글
Codelab | Jetpack Compose layout - Intrinsic Measure (0) | 2022.07.24 |
---|---|
Codelab | Jetpack Compose layout - ConstraintLayout (0) | 2022.07.24 |
Codelab | Jetpack Compose layout - Custom Layout (0) | 2022.07.24 |
Codelab | Jetpack Compose layout - Use List (0) | 2022.07.24 |
Codelab | Jetpack Compose layout - Material Component (0) | 2022.07.24 |