Hanbit the Developer

LazyVerticalGrid in LazyColumn 구현 본문

Mobile/Android

LazyVerticalGrid in LazyColumn 구현

hanbikan 2024. 5. 22. 17:30

문제 상황

아래 화면에서 수집 진행도(NkAnimatedCircularProgress)와 물고기 그리드(LazyVerticalGrid)의 스크롤을 병합하고 싶었다.

Column(
    modifier = Modifier.fillMaxWidth(),
    horizontalAlignment = Alignment.CenterHorizontally,
) {
    NkAnimatedCircularProgress(
        progress = collectibles.calculateProgress(),
        description = stringResource(id = R.string.progress_rate)
    )
    Spacer(modifier = Modifier.height(Dimens.SpacingLarge))
    LazyVerticalGrid(columns = GridCells.Adaptive(CollectibleItemSize)) {
        itemsIndexed(collectibles) { index, item ->
            CollectibleItem(
                item = item,
                onClick = { onClickCollectibleItem(index) }
            )
        }
    }
}

Attemptions

1. LazyVerticalGrid의 첫번째 아이템으로 NkAnimatedCircularProgress 삽입?

이렇게 할 경우 그리드의 아이템으로서 병합되기 때문에 기존 화면처럼 구현이 불가능하다.

2. LazyVerticalGrid in LazyColumn?

LazyColumn 내부에 LazyVerticalGrid를 넣을 경우 다음과 같은 오류가 발생한다. LazyVerticalGrid에 높이 제한을 두면 된다고 하는데 우회하는 방법 같아서 사용하지 않기로 했다.(https://velog.io/@dev_thk28/Android-LazyColumn안에-LazyVerticalGrid-넣기nested-scroll)

java.lang.IllegalStateException: Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed.

3. LazyVerticalGrid처럼 동작하는 LazyColumn 구현

아래 사진처럼 아이템을 4개의 청크 단위로 묶어서 처리하는 방식으로 구현이 가능해보인다.

고려해야 할 사항은 다음과 같다:

  • GridCells.Adaptive(size) 구현: 화면의 크기에 따라 한 줄에 아이템이 배치되는 개수가 달라질 수 있다. 즉 화면이 작을 경우 아이템이 한 줄에 3개, 클 경우 한 줄에 10개가 올 수 있는 상황이다. 이를 위해 LazyVerticalGrid에 GridCells.Adaptive를 적용했는데 LazyColumn을 위해 구현이 필요해보인다.

구현 사항이 좀 있으므로 대안이 있으면 좋을 듯 하다.

4. LazyVerticalGrid 구현

LazyVerticalGrid처럼 동작하는 무언가를 만들어서 LazyColumn의 item으로 넣는 방법이 없나 찾아보았고 다음과 같은 코드를 통해 구현할 수 있었다:

LazyColumn(
    modifier = Modifier.fillMaxWidth(),
    horizontalAlignment = Alignment.CenterHorizontally,
) {
    item {
        NkAnimatedCircularProgress(
            progress = collectibles.calculateProgress(),
            description = stringResource(id = R.string.progress_rate)
        )
        Spacer(modifier = Modifier.height(Dimens.SpacingLarge))
    }
    item {
        FlowRow {
            collectibles.forEachIndexed { index, item ->
                CollectibleItem(
                    item = item,
                    onClick = { onClickCollectibleItem(index) }
                )
            }
        }
    }
}

구현 결과 스크롤이 병합되어 잘 작동한다. 하지만 아이템이 너무 많을 경우 퍼포먼스 문제가 발생할 수 있기 때문에, 3번의 방법이 낫다고 생각한다.

Solution: LazyVerticalGrid처럼 동작하는 LazyColumn 구현

var containerWidth by remember { mutableIntStateOf(0) }
val itemWidth = with(LocalDensity.current) { CollectibleItemWidth.toPx() }
val itemsPerRow = (containerWidth / itemWidth).toInt()

LazyColumn(
    modifier = Modifier
        .fillMaxWidth()
        .onGloballyPositioned { containerWidth = it.size.width },
    horizontalAlignment = Alignment.CenterHorizontally,
) {
    item {
        NkAnimatedCircularProgress(
            progress = collectibles.calculateProgress(),
            description = stringResource(id = R.string.progress_rate)
        )
        Spacer(modifier = Modifier.height(Dimens.SpacingLarge))
    }
    if (itemsPerRow > 0) {
        itemsIndexed(collectibles.chunked(itemsPerRow)) { rowIndex, rowItems ->
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.Start,
            ) {
                rowItems.forEachIndexed { index, item ->
                    CollectibleItem(
                        item = item,
                        onClick = { onClickCollectibleItem(rowIndex * itemsPerRow + index) }
                    )
                }
            }
        }
    }
}
  • containerWidth: onGloballyPositioned에서 측정한 컨테이너의 가로 길이
  • itemsPerRow: containerWidth에 따라 변동되는 값으로 한 줄에 아이템이 몇 개 들어가야 하는지를 결정한다. 가령 containerWidth가 900, itemWidth가 200이라면 itemsPerRow는 4이 된다.
  • itemsIndexed(collectibles.chunked(itemsPerRow)): 계산한 itemsPerRow를 토대로 청크를 나눈다. 이 값이 4이라면 Row 안에 아이템을 4개를 넣게 된다.
  • 주의 사항: 특정 아이템의 인덱스를 계산하기 위해서 itemsIndexed의 rowIndex, rowItems.forEachIndexed의 index를 모두 고려해야 한다.(rowIndex * itemsPerRow + index)

구현 결과

잘 작동한다!

References