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