안드로이드 RecyclerView GridLayoutManager 항상 일정한 아이템 간격 적용하기 (Column Space) - ItemDecoration

임현주·2022년 10월 31일
3
post-thumbnail

보통 갤러리 어플들을 확인해보면 Grid 형태에, 일정한 간격으로 margin이 적용되어있는 모습을 확인할 수 있다.

RecyclerView을 이용해 이를 구현할 수 있는데 해당 아이템에 xml 코드에서 margin 값을 적용하게 되면 고정값을 지니게 되므로 아이템이 이어지는 부분끼리는 margin 값이 2배가 되어 위의 갤러리처럼 일정한 간격을 유지할 수 없어진다.

일정 간격을 유지하기 위해서는 아이템이 맞닿는 부분에 계산 과정을 한 번 거쳐 margin 값을 적용해줄 필요가 있다. 이를 위해 ItemDecoration을 사용해 볼 것이다.

item.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="?attr/selectableItemBackground">

    <androidx.cardview.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

            <ImageView
                android:id="@+id/wearImageView"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:adjustViewBounds="true"
                android:src="@color/gray_500"
                android:scaleType="centerCrop"
                app:layout_constraintDimensionRatio="w,1:1"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                tools:src="@color/gray_200" />

        </androidx.constraintlayout.widget.ConstraintLayout>

    </androidx.cardview.widget.CardView>

</androidx.constraintlayout.widget.ConstraintLayout>

먼저 RecyclerView Adapter에서 바인딩할 레이아웃을 작성한다.
(Adapter는 해당 포스팅에 따로 작성하지 않을 것입니다! 만약 작성법을 찾으신다면 참고해주세용)

최상위 레이아웃인 ConstraintLayout을 android:layout_width= "match_parent" 로 설정해준 이유는 아이템의 크기를 반응형으로 보여주기 위함이다. 고정값이어도 상관 없지만 필자의 경우, 모든 디바이스에서 아이템끼리의 margin 간격이 반드시 16dp로 유지하면서 디바이스 크기에 의해 width 크기가 결정되도록 하기 위해 이렇게 설정했다.

ImageView의 모양을 정사각형(1:1) 비율로 적용하고 싶다면 app:layout_constraintDimensionRatio="w,1:1" 을 작성해주면 된다.

ItemDecoration

ItemDecoration 클래스는 말 그대로 아이템을 꾸며주는 역할을 하는 RecyclerView 내부의 추상 클래스이다. 주로 아이템 간 구분선이나 여백을 설정할 때 자주 응용된다. ItemDecoration는 총 3개의 함수를 제공해준다.

  • onDraw() : 항목을 배치하기 전에 호출된다.
  • onDrawOver() : 모든 항목이 배치된 후에 호출된다.
  • getItemOffsets() : 각 항목을 배치할 때 호출된다.

이 중에 getItemOffsets()을 이용할 것이다.

internal class GridSpacingItemDecoration(
    private val spanCount: Int, // Grid의 column 수
    private val spacing: Int // 간격
) : ItemDecoration() {

    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val position: Int = parent.getChildAdapterPosition(view)

        if (position >= 0) {
            val column = position % spanCount // item column
            outRect.apply {
                // spacing - column * ((1f / spanCount) * spacing)
                left = spacing - column * spacing / spanCount
                // (column + 1) * ((1f / spanCount) * spacing)
                right = (column + 1) * spacing / spanCount
                if (position < spanCount) top = spacing
                bottom = spacing
            }
        } else {
            outRect.apply {
                left = 0
                right = 0
                top = 0
                bottom = 0
            }
        }
    }
}   

Grid 아이템의 컬럼(RecyclerView에서 spanCount) 개수와 간격을 전달 받아 계산할 수 있도록 작성했다.

binding.recyclerView.addItemDecoration(
	GridSpacingItemDecoration(spanCount = 2, spacing = 16f.fromDpToPx())
)

...

// 해당 함수는 util 패키지에 작성한 확장함수입니다 :)
fun Float.fromDpToPx(): Int = 
    (this * Resources.getSystem().displayMetrics.density).toInt()

이런식으로 호출해주면 끝❗️

결과

작업중인 APP에 적용해 데려와보았다 :)

GridSpacingItemDecoration(spanCount = 3, spacing = 16f.fromDpToPx())

요건 spanCount을 3으로 전달한 경우인데, 앞에서 언급한 것처럼 여백 16dp는 고정으로 가져가고 디바이스 크기에 의해 width 크기가 결정되는 것을 볼 수 있다.

profile
🐰 피드백은 언제나 환영합니다

1개의 댓글

comment-user-thumbnail
2023년 12월 21일

좋은 글 감사합니다 참고가 되었습니다!
혹시 화면의 크기에 따라 span count 를 동적으로도 가져갈 수 있을까요?

답글 달기