티스토리 뷰

1. 안드로이드 리사이클러뷰와 그리드 레이아웃 매니저를 이용한 해상도별 item 그리기

프로젝트를 진행하며 마주친 문제인데, 리사이클러 뷰 에서 여러 종류의 아이템(헤더, 아이템카드, 푸터)을 뿌려주고 있는 상황이며, 가이드로 나온 디자인은 특정 해상도에서 아이템 뷰를 다르게 보여주기를 원하고 있었습니다.

아래는 디자인 가이드 예시 입니다.

해상도별 디자인 가이드

리사이클러 내부에 들어가는 아이템이 한 가지 종류가 아니라 위 디자인 가이드를 보시면 알겠지만 최 상단에는 타이틀 헤더(Header)가 최 하단에는 푸터(Footer)가 붙어있는 상황입니다.

기본적으로 사용하고 있었던 LinearLayoutManager 를 걷어내고 GridLayoutManager 를 사용해야 함은 얼추 알고는 있었는데, 아이템 타입이 여러개 일 때는 어떻게 적용을 할지 몰라서 막막했었죠.

1.1. Header 와 Footer 를 한 행으로 처리하기

먼저 찾아본 것은 헤더와 푸터의 경우 해상도가 큰 화면에서 2열로 나뉘어 보여질 때도 한 행에서 보여지게 해야하는 문제였죠. 이 부분은 구글링을 통해서 쉽게 해답을 찾을 수 있었습니다.

val mLayoutManager = GridLayoutManager(context, 2)
mLayoutManager.spanSizeLookup = object : SpanSizeLookup() {
    override fun getSpanSize(position: Int): Int {
        return when (adapter.getItem(position)) {
            is HeaderItem, is FooterItem -> 2
            else -> 1
        }
    }
}

GridLayoutManagerspanSizeLookup 을 새로 확장하여 getSpanSize 를 재정의해주는 방법이었습니다.

리사이클러 뷰에 위치하는 각각 아이템의 타입을 가져와서 헤더와 푸터인 경우에는 spanSize 를 2로 반환하게 끔하여 두 개의 열을 다 차지하도록 해주었습니다. 그러면 실제 리사이클러 뷰에서 헤더와 푸터는 한 행에서 나타나게 되는 것이죠.

1.2. 특정 해상도를 넘어가면 2개 열로 나누어 아이템을 보여주기

다음은 특정 해상도를 넘길 경우 2개 열로 나누어 아이템을 보여주는 부분 이었습니다. 이 부분은 사실 GridLayoutManager를 인스턴스화 할 때 디바이스 너비를 얻어와 설정해 주면 되는 부분이죠.

1.1 에서 본 코드에서 특정 해상도 이상일 때만 GridLayoutManager(context, 2) 으로 인스턴스화 하고 아닐 경우에는 GridLayoutManager(context, 1) 또는 LinearLayoutManager 를 사용하는 방법도 있겠죠.

2. 특정 해상도를 넘어가기 전에는 match_parent, 그 이후에는 가운데로 몰아서 보여주기

가장 구글링도 많이하고 시간을 많이 할해한 부분은 특정 해상도 이상일 때 아이템 카드의 정렬과 너비 설정 부분이었습니다.

사실 처음에는 스택오버플로우에 질문도 남겼습니다. (현재는 자문자답 한 상태입니다 😂)

결론은 RecyclerView.ItemDecoration 을 사용하는 방법입니다:

class ItemDecoration(context: Context, private val displayWidthPx: Int) :
    RecyclerView.ItemDecoration() {
    private val horizontalMargin =
        context.resources.getDimensionPixelSize(R.dimen.span_middle_margin)
    private val maxItemSize =
        context.resources.getDimensionPixelSize(R.dimen.item_max_width)

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        super.getItemOffsets(outRect, view, parent, state)
        val spanIndex = (view.layoutParams as GridLayoutManager.LayoutParams).spanIndex

        outRect.run {
            if (parent.getChildViewHolder(view) is MyViewHolder) {
                when (displayWidthPx.pxToDp) {
                    in 1..767 -> Unit
                    else -> {
                        val displayStartEndMargin =
                            (displayWidthPx - (maxItemSize * 2) - (horizontalMargin * 2)) / 2
                        if (spanIndex % 2 == 0) {
                            right = horizontalMargin
                            left = displayStartEndMargin
                        } else {
                            left = horizontalMargin
                            right = displayStartEndMargin
                        }
                    }
                }
            }
        }
    }
}

위 코드에서 RecyclerView.ItemDecoration 를 확장하는 커스텀 데코레이션 클래스를 정의해주었습니다.

순서대로 살펴보면 horizontalMargin 은 가운데로 아이템 카드를 몰아보여줄 때 각 1열 2열 아이템 사이에 마진을 위한 값입니다. maxItemSize 는 해상도가 큰 화면에서 보여줄 아이템 카드의 최대 너비가 되겠습니다.

그리고 ItemDecoration 클래스 생성시 context 와 더불어 displayWidthPx 도 받도록 하였는데요? displayWidthPx 는 유저가 사용하는 디바이스의 디스플레이 실제 너비이어야 합니다.

이들은 모두 getItemOffsets 에서 사용되어야 하고 getItemOffsets 에서는 픽셀값을 사용하기 때문에 픽셀값을 받아왔습니다.

반응형

outRect 의 크기를 설정하여 그려질 영역을 결정하기 때문에 해당 객체에서 run 으로 적용해주었습니다.

parent.getChildViewHolder(view) is MyViewHolder 으로 현재 view 가 어떤 아이템 타입의 뷰홀더인지 파악하고 헤더와 푸터가 아닌 아이템 카드인 경우에만 적용하도록 합니다.

이후 유저 디바이스가 특정 해상도를 넘어가는 경우에만 마진값을 재 계산하여 화면에 그려질 위치를 정해주었습니다.

결론

결과적으로 원하는 리사이클러 뷰의 아이템 디스플레이 방식대로 구현을 할 수 있게 되었습니다. 처음에는 정말 이게 가능한가? 가능한지 팀원분께 질문을 해볼까? 라는 생각도 들었지만 (스택오버플로우에 질문을 올리기도 했지만 빠른 답변이 없없다..) 불가능 해보이진 않아서 계속 문제해결을 위한 답을 찾았고 결국 해결했다. 🤯

맞는 코드인지, 좀 더 효율적인 코드가 있는지 모르겠지만 그래도 뿌듯하다. 구글링 해봤을 때 관련된 티스토리나 국내 포스터가 없어서 혹시나 다른 분들에게 도움이 될까 해서 포스팅을 남기게 되었는데, 혹시나 다른 의견이나 궁금한 점이 있으시다면 덧글 남겨주시면 확인해 보도록 하겠습니다.

도움 되셨다면 공감 부탁 드려요!

2022.01.02 - [Android/Kotlin] - [코틀린] Android KTX 을 탐구해보자 🧐

2022.01.01 - [Android/Kotlin] - [코틀린] 더 좋은 Companion object 사용 방법

2021.12.18 - [Android/Jetpack Compose] - [Android] GDG Jetpack compose 코드랩 수료 후기 (feat. 굿즈)

댓글
댓글쓰기 폼