Beeeam

Sticky Header RecyclerView 본문

Android

Sticky Header RecyclerView

Beamjun 2024. 4. 22. 17:00

최근에 Coordinator Layout으로 구현된 화면 내부의 RecyclerView의 무한 스크롤을 구현하는 작업을 했습니다. RecyclerView의 최하단에 도달하면 새로운 아이템을 받아와서 추가하여 화면에 보여주는 동작을 구현해야 하는데 새로운 아이템을 화면에 보여주면 기존의 아이템들이 사라지는 문제에 직면했습니다.

이를 Coordinator Layout 때문이라는 생각을 하였고 해결하기 위해서 화면 전체를 하나의 RecyclerView로 구현한다는 생각을 했습니다. 이 과정에서 App Bar 동작도 구현을 해야 했는데 이는 Sticky Header로 구현하면 될 것이라 생각하여 Sticky Header를 구현한 RecyclerView를 구현하게 되었습니다.

Sticky Header는 Compose에서 처음 접했고 쉽게 구현했던 기억이 있었습니다. 하지만 xml을 통해서 구현하는 것은 Compose에서 구현하는 것보다 훨씬 어려웠습니다.

 

그래서 이를 정리하고 다른 분들도 참고하면 좋을 것 같아서 글을 작성하고자 합니다.

 

 

ItemDecoration

An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set.

 

공식문서에서는 ItemDecoration에 대해서 위와 같이 설명하고 있습니다. 간단하게 말하면 이 클래스는 adapter를 통해 그려지는 아이템 뷰들에 무언가를 그리는 동작을 수행합니다. 그래서 ItemDecoration 클래스를 사용하여 RecyclerView 위에 Sticky Header를 그리게 됩니다.

ItemDecoration 클래스는 onDraw, onDrawOver 함수를 제공합니다. 두 함수의 차이점은 다음과 같습니다.

- onDraw() : 아이템들이 그려지기 전에 호출

- onDrawOver() : 아이템들이 그려진 후에 호출

스크롤 중 Sticky Header로 만들 아이템이 최상단에 도달한 경우에 Header를 그릴 예정이기 때문에 onDrawOver() 함수를 사용하여 Header를 그릴 것입니다.

 

구현

RecyclerView가 구현되어 있다는 가정하에 설명 하겠습니다.

먼저 RecyclerView의 아이템이 Sticky Header로 만들 아이템인지 확인하는 함수를 선언합니다.

interface SectionCallBack {
    fun getHeaderView(list: RecyclerView, position: Int): View? 
}

 

이 인터페이스의 구현체는 Activity 내에 선언합니다.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
				...
    }

    private fun getSectionCallback(): SectionCallBack {
        return object : SectionCallBack {
            override fun getHeaderView(list: RecyclerView, position: Int): View? {
                return adapter.getHeaderView(list, position)
            }
        }
    }
}

 

adapter에는 다음과 같이 getHeaderView() 함수를 선언합니다.

fun getHeaderView(list: RecyclerView, position: Int): View? {
    val lastPosition = if (position < currentList.size) position else currentList.size - 1
    for (pos in lastPosition downTo 0) {
        if (getItem(pos).title == "Title 2") {
            return HeaderRecyclerviewBinding.inflate(LayoutInflater.from(list.context), list, false).root
        }
    }
    return null
}

RecyclerView의 최상단 아이템의 Index를 기준으로 작아지는 순서로 Sticky Header로 만들 아이템인지 확인하는 동작을 수행합니다. Sticky Header가 스크롤 되서 밖으로 사라졌는지 확인하는 동작입니다. 그래서 만약 화면 밖으로 사라졌으면 Sticky Header View를 반환하여 이를 ItemDecoration를 통해 화면에 그리게 됩니다.

 

Sticky Header를 그리는 동작은 ItemDecoration을 상속받는 클래스를 만들고 여기에 선언할 것입니다.

class StickyHeaderItemDecoration(private val sectionCallback: SectionCallBack) :
    RecyclerView.ItemDecoration() {
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)

        val topChild = parent.getChildAt(0) ?: return
        val topChildPosition = parent.getChildAdapterPosition(topChild)

        val currentHeader: View = sectionCallback.getHeaderView(parent, topChildPosition) ?: return
        fixLayoutSize(parent, currentHeader, topChild.measuredHeight)

        drawHeader(c, currentHeader)
    }

    private fun drawHeader(c: Canvas, header: View) {
        c.save()
        c.translate(0f, 0f)
        header.draw(c)
        c.restore()
    }

    private fun fixLayoutSize(parent: ViewGroup, view: View, height: Int) {
        val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
        val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.EXACTLY)
        val childWidth: Int = ViewGroup.getChildMeasureSpec(
            widthSpec,
            parent.paddingLeft + parent.paddingRight,
            view.layoutParams.width
        )
        val childHeight: Int = ViewGroup.getChildMeasureSpec(
            heightSpec,
            parent.paddingTop + parent.paddingBottom,
            height
        )
        view.measure(childWidth, childHeight)
        view.layout(0, 0, view.measuredWidth, view.measuredHeight)
    }
}

onDrawOver() 함수는 RecyclerView에 작은 스크롤이라도 발생하면 호출되는데 이때 RecyclerView의 최상단의 뷰에 대해서 Sticky Header로 만들 아이템인지 확인하는 동작(sectionCallback.getHeaderView() 함수)을 수행하고 반환되는 View를 화면에 그립니다.

 

그리고 Activity에서 RecyclerView를 초기화할 때 다음과 같이 우리가 만든 ItemDecoration 클래스를 추가해줘야 합니다.

binding.recyclerview.addItemDecoration(StickyHeaderItemDecoration(getSectionCallback()))

 

 

전체 코드는 여기서 확인할 수 있습니다.

https://github.com/BEEEAM-J/Sticky_Header_RecyclerView

 

GitHub - BEEEAM-J/Sticky_Header_RecyclerView

Contribute to BEEEAM-J/Sticky_Header_RecyclerView development by creating an account on GitHub.

github.com

 

'Android' 카테고리의 다른 글

MVI Pattern without Orbit  (0) 2024.06.17
Bottom Navigation in Multi Module with deeplink  (0) 2024.03.26
MVVM ViewModel vs AAC ViewModel  (0) 2024.02.15
명령형 vs 선언형, Jetpack Compose  (0) 2024.02.04
MVI  (0) 2024.01.24