StickyHeader

낄낄몬스터·2024년 8월 2일
0

앱 개발 숙련

목록 보기
9/9
post-custom-banner

이런 식으로 고정된 카테고리 같은 것들을 상단에 고정시키고 한 종류가 지나면 자연스럽게 다음 카테고리를 상단에 고정시키는 역할을 하는 게 StickyHeader

추가해줘야 할 코드들

MainActivity

binding.recycleListView.addItemDecoration(StickyHeaderItemDecoration(getSectionCallback()))


private fun getSectionCallback(): StickyHeaderItemDecoration.SectionCallback {
        return object : StickyHeaderItemDecoration.SectionCallback {
            override fun isHeader(position: Int): Boolean {
                return adapter.isHeader(position)
            }

            override fun getHeaderLayoutView(list: RecyclerView, position: Int): View? {
                return adapter.getHeaderView(list, position)
            }
        }
    }

StickyHeaderItemDecoration.kt

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)  // 첫 번째 자식 뷰의 포지션
        if (topChildPosition == RecyclerView.NO_POSITION) {
            return
        }

        // 헤더 뷰를 가져옴
        val currentHeader: View = sectionCallback.getHeaderLayoutView(parent, topChildPosition) ?: return

        // 헤더 뷰의 레이아웃을 설정
        fixLayoutSize(parent, currentHeader)

        val contactPoint = currentHeader.bottom  // 현재 헤더 뷰의 아래쪽 경계 위치

        val childInContact: View = getChildInContact(parent, contactPoint) ?: return  // 접촉하는 자식 뷰를 찾음

        val childAdapterPosition = parent.getChildAdapterPosition(childInContact)
        if (childAdapterPosition == -1) {
            return
        }

        // 헤더가 다른 헤더와 접촉하는 경우 위치를 조정하고, 그렇지 않으면 기본 위치에 그림
        if (sectionCallback.isHeader(childAdapterPosition)) {
            moveHeader(c, currentHeader, childInContact)
        } else {
            drawHeader(c, currentHeader)
        }
    }

    // 접촉하는 자식 뷰를 찾는 함수
    private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? {
        for (i in 0 until parent.childCount) {
            val child = parent.getChildAt(i)
            if (child.bottom > contactPoint && child.top <= contactPoint) {
                return child
            }
        }
        return null
    }

    // 헤더가 다른 헤더와 겹칠 경우 이동
    private fun moveHeader(c: Canvas, currentHeader: View, nextHeader: View) {
        val headerTop = nextHeader.top - currentHeader.height
        c.save()
        c.translate(0f, headerTop.toFloat())
        currentHeader.draw(c)
        c.restore()
    }

    // 헤더를 기본 위치에 그림
    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) {
        // 부모 RecyclerView의 전체 너비를 EXACTLY로 측정
        val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)

        // 헤더 뷰의 높이를 XML 레이아웃에서 설정한 대로 적용하기 위해 EXACTLY로 측정
        // 40dp를 픽셀 단위로 변환해야 함
        val pixels = (40 * parent.context.resources.displayMetrics.density).toInt()
        val heightSpec = View.MeasureSpec.makeMeasureSpec(pixels, View.MeasureSpec.EXACTLY)

        // 측정 및 레이아웃 설정
        view.measure(widthSpec, heightSpec)
        view.layout(0, 0, view.measuredWidth, view.measuredHeight)
    }


    // 헤더 정보를 제공하는 인터페이스
    interface SectionCallback {
        fun isHeader(position: Int): Boolean  // 해당 포지션이 헤더인지 판단
        fun getHeaderLayoutView(list: RecyclerView, position: Int): View?  // 해당 포지션의 헤더 뷰를 반환
    }
}

MyAdapter.kt

fun isHeader(position: Int) = getItemViewType(position) == VIEW_TYPE_TITLE


    // RecyclerView 위에 그려줄 View를 반환한다.
    fun getHeaderView(list: RecyclerView, position: Int): View? {
        val lastIndex = if (position < mItems.size) position else mItems.size - 1

        for (index in lastIndex downTo 0) {
            if (getItemViewType(index) == VIEW_TYPE_TITLE) {
                val titleItem = mItems[index] as? DogItems.MyTitle
                if (titleItem != null) {
                    val binding = ItemTitleBinding.inflate(LayoutInflater.from(list.context), list, false)
                    binding.tvAgetitle.text = "${titleItem.age} 살"
                    return binding.root
                }
            }
        }

        return null
    }
profile
음악을 사랑하는 예비 앱개발자
post-custom-banner

0개의 댓글