본문 바로가기
안드로이드/코틀린

안드로이드 리사이클러뷰 데코

by 나이아카 2022. 1. 7.

 이번 게시글은 지극히 주관적인 사유로 쓰는 글입니다. (물론 이전 게시글도 전부 지극히 개인적인 목적이긴 하지만...)

 회사에서 작업을 하다보니 조금씩 다른 margin을 이용한 gridLayout 리사이클러뷰를 사용하게 되었습니다. 그래서 그냥 xml에서 마진과 패딩을 먹여 작업을 할까 했었는데 그렇게 되면 기존에 리사이클러뷰를 감싸고 있는 Layout에 먹인 패딩이나 마진과 중복되어 원하는 값을 찾는 것이 매우 번거로워졌습니다.

 그래서 그런 부분을 신경쓰지 않고 리사이클러뷰 내부에서만 돌리는 방법을 위해 ItemDeco라는 친구를 결국 사용하게 되었네요.(이때까지 작업하기 귀찮아서 어떻게든 안쓰던 건 비밀...)

 하여튼! 안만들게 되었으면 모를까, 만들게 되었으니 한 번 만들어놓고 계속해서 쓰려면 되도록 많은 수치들이 미리 지정되어 있지 않고 클래스 내에서 직접 지정할 수 있게 만드는 것이 목표였습니다.(그리고 이때까지 왜 피했나 싶을 정도로 간단했었더랬죠...)

 코드에 설명 첨부하겠습니다.


class RecyclerViewGridItemDeco(
    private val context: Context,
    private val bottomMargin: Int = 0,
    private val topMargin: Int = 0,
    private val rightMargin: Int = 0,
    private val leftMargin: Int = 0,
    private val gridCount: Int = 1
) : RecyclerView.ItemDecoration()

 먼저 클래스를 만들기 위해 RecyclerView의 ItemDecoration을 상속 받습니다. 그리고 저는 4가지 방향 모두에 여백을 주어야 하기 때문에 파라미터로 네 방향의 값을 받습니다. 물론 원하지 않는 방향에는 여백을 줄 필요가 없기 때문에 원하지 않는 방향의 경우, 파라미터를 주지 않을 수 있게 default 값을 0으로 지정해두었습니다. 또한 gridLayout의 deco이기 때문에 한 라인에 몇 개의 아이템이 들어가는지도 알아야 합니다.(사실 LinearLayout이라면 이런 데코가 필요하지도 않습니다...)

 

val lastIndex : Int = if(state.itemCount % gridCount ==0) state.itemCount - gridCount
    else state.itemCount - state.itemCount % gridCount

 그 후, 마지막 라인의 시작 아이템의 포지션을 찾기 위해 값을 계산합니다. 당연하게도 grid의 아이템은 0부터 시작하기 때문에 매 줄의 첫번째 아이템의 포지션은 항상 한 줄에 속한 아이템 개수(gridCount)로 나눌 경우 나머지가 0입니다. 그리고 아이템의 개수를 gridCount로 나누었을 때 나누어 떨어지면 맨 마지막 줄의 아이템이 가득 차게 된다는 것을 의미합니다. 만약 마지막 줄의 아이템이 전부 찬 상태가 아닌데 그냥 gridCount만큼 반복하게 만들면 이후 마지막 라인을 구분할 때 에러를 일으키게 됩니다.

 

when (parent.getChildAdapterPosition(view)) {
    in 0 until gridCount -> outRect.bottom = bottomMargin.dpToPx(context)
    in lastIndex until state.itemCount -> outRect.top = topMargin.dpToPx(context)
    else -> {
        outRect.top = topMargin.dpToPx(context)
        outRect.bottom = bottomMargin.dpToPx(context)
    }
}

 

 0부터 gridCount까지는 첫줄로 판단합니다.(당연한 소리지만...) 첫줄은 항상 윗쪽 여백이 존재하면 안됩니다. 첫줄의 위쪽에 여백이 존재하는 경우 리사이클러뷰를 감싸고 있는 레이아웃을 통해 작성해둔 패딩이나 마진이 달라질 수 있기 때문이죠. 

 그리고 이전에 계산해둔 lastIndex부터 마지막 아이템까지는 맨 아래쪽 아이템으로 판단합니다. 이 아래쪽 아이템은 아랫부분에 여백이 존재하면 안되기 때문에 윗쪽에만 값을 줍니다.

 나머지는 중간 부분이기 때문에 위아래 둘 다 값을 전달해야 합니다. 또한 값은 dp로 나타내는 것이 좋으니 int 값을 dp에 맞도록 조정하는 dpToPx 코드를 이용합니다. dpToPx 역시 직접 만들었습니다만, 이는 다른 여러 코드를 이용해도 좋습니다.

fun Int.dpToPx(context: Context): Int {
	return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), context.resources.displayMetrics).toInt()
}

 

val gridLayout = view.layoutParams as GridLayoutManager.LayoutParams

when (gridLayout.spanIndex) {
    0 -> outRect.right = rightMargin.dpToPx(context)
    gridCount - 1 -> outRect.left = leftMargin.dpToPx(context)
    else -> {
        outRect.right = rightMargin.dpToPx(context)
        outRect.left = leftMargin.dpToPx(context)
    }
}

 gridLayout의 spanIndex는 현재 데코가 작성중인 아이템이 자신의 라인에서 몇 번째에 위치하고 있는지를 알려주는 값입니다. 이 값이 0이라면 각 라인의 맨 앞에 있는 아이템을 의미하며, gridCount -1은 맨 뒤 아이템을 의미하는 것입니다.

 맨 앞의 값은 당연히 왼쪽(start)값에 여백을 주지 말아야 하며, 맨 뒤 아이템은 오른쪽(end)값에 여백을 주지 말아야 합니다. 나머지는 상관없이 양쪽 모두에게 여백을 주면 됩니다.

 이렇게 작성하면 간단하게 리사이클러뷰 외부에서 작성한 패딩이나 마진을 건들지 않고 원하는 만큼 아이템의 간격을 떨어뜨릴 수 있습니다. 단 좌우 양 아이템의 간격이 14일 때 아이템의 마진 파라미터는 right에 7, left에 7로 반반을 주어 처리해야 왼쪽 아이템에서 7, 오른쪽 아이템에서 7만큼 떨어뜨려 총 간격을 14로 만들 수 있음을 유의해야 합니다.

 

마지막으로 완성 코드입니다.

class RecyclerViewGridItemDeco(
    private val context: Context,
    private val bottomMargin: Int = 0,
    private val topMargin: Int = 0,
    private val rightMargin: Int = 0,
    private val leftMargin: Int = 0,
    private val gridCount: Int = 1
) : RecyclerView.ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        super.getItemOffsets(outRect, view, parent, state)
        val gridLayout = view.layoutParams as GridLayoutManager.LayoutParams
        val lastIndex : Int = if(state.itemCount % gridCount ==0) state.itemCount - gridCount
        else state.itemCount - state.itemCount % gridCount
        when (parent.getChildAdapterPosition(view)) {
            in 0 until gridCount -> outRect.bottom = bottomMargin.dpToPx(context)
            in lastIndex until state.itemCount -> outRect.top = topMargin.dpToPx(context)
            else -> {
                outRect.top = topMargin.dpToPx(context)
                outRect.bottom = bottomMargin.dpToPx(context)
            }
        }
        when (gridLayout.spanIndex) {
            0 -> outRect.right = rightMargin.dpToPx(context)
            gridCount - 1 -> outRect.left = leftMargin.dpToPx(context)
            else -> {
                outRect.right = rightMargin.dpToPx(context)
                outRect.left = leftMargin.dpToPx(context)
            }
        }
    }
}

 엄청 간단한 코드입니다. 사실 데코를 좀 더 넣으면 좀 더 일반화된 아이템이 되겠지만, 당장 회사에서 요구하는 건 올바르게 떨어진 여백이니 이정도만 작성해놨습니다.

 이렇게 하나씩 만들어놓고 나중에 값을 넣어 사용하는 클래스들을 쓰다 보니 시간이 날 때 UI 라이브러리 같은 것들을 제작하는 것도 재밌을 거 같은 생각이 듭니다!

댓글