ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Sticky Header Recyclerview using ItemDecoration without library - 2 (Kotlin)
    Dev/android 2020. 5. 27. 00:18

    자 라이브러리 없이 ItemDecoration을 사용해서 만드는 Sticky Header RecyclerView 2탄!

     

    이전 포스팅 1탄은 여기 참조!!

     

    지난 포스팅에서는 아이템 뷰 타입별로 뷰홀더에 바인딩을 했다면

     

    이번엔 RecyclerView에 넣을 리스트를 만들어보자

     

    지난 포스팅에 Activity에 Data 클래스를 타입으로 가진 List 만든것과 다른 리스트이다.

     

    지금 만들 리스트는 여러 타입의 뷰를 아이템으로 가진 리스트이다.

     

    AdapterItem.kt

    data class AdapterItem(var type: Int, var objects: Data)

    Data 타입과 Int 타입의 매개변수를 가진 AdapterItem 이라는 data class를 만들어 준다.

     

    이것은 지금 만들 리스트의 데이터 타입이 된다.

     

       private val recyclerItemList: ArrayList<AdapterItem> = ArrayList()

    Adapter 상단에 위와 같이 리스트를 선언해준다

     

    setListData() 메소드를 만들어서 이 리스트에 아이템을 add 할 건데 다음과 같이 해준다.

     

        private fun setListData() {
            recyclerItemList.clear()
            recyclerItemList.add(AdapterItem(TYPE_TOP, Data("", -1)))
            recyclerItemList.add(AdapterItem(TYPE_HOLDER, Data("", -1)))
            if (item.isEmpty()) {
                recyclerItemList.add(AdapterItem(TYPE_EMPTY, Data("", -1)))
            } else {
                for (data in item) {
                    recyclerItemList.add(AdapterItem(TYPE_LIST, data))
                }
            }
            notifyDataSetChanged()
        }

    이렇게 하면 recyclerItemList 에는 

    Index 0 : TYPE_TOP, DATA("",-1)

    Index 1  : TYPE_HOLDER, DATA("",-1)

    Index 2 : TYPE_LIST, DATA("",-1)

     

    총 3개의 아이템이 들어가 있다.

    (TYPE_EMPTY의 경우는 item이 없을 때 TYPE_LIST 대신 들어간다)

     

    이번엔 ItemDecoration을 상속받은 RecyclerSectionItemDecoration 을 생성한다.

    onDrawOver를 Override 해준다.

     

    RecyclerSectionItemDecoration.kt

    class RecyclerSectionItemDecoration() : ItemDecoration() {
          override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
            super.onDrawOver(c, parent, state)
            
        }
    }

    onDrawOver는 recyclerView가 그려진 뒤에 호출 된다.

    그래서 ReyclerView 위에 그릴 수 있다.

    이 점을 이용해서 우리는 상단에 Sticky Header View를 그릴 것이다.

     

    ItemDecoration에서는 adapter에 직접 접근하지 않아야 한다.

    따라서 Interfacef를 만들어서 adapter에서 필요한 정보를 가져오도록 하겠다.

     

        interface SectionCallback {
            fun isSection(position: Int): Boolean
            fun getHeaderLayoutView(list: RecyclerView, position: Int): View?
        }

    인터페이스로 SectionCallback 을 만들고

    isSection과 getHeaderLayoutView를 선언했다.

     

    isSection 은 해당 포지션이 고정될 뷰 인지 판단하고

    getHeaderLayoutView 는 해당 포지션에 해당하는 뷰를 가져온다.

     

      val topChild = parent.getChildAt(0) ?: return
    
            val topChildPosition = parent.getChildAdapterPosition(topChild)
            if (topChildPosition == RecyclerView.NO_POSITION) {
                return
            }

    topChild 에 현재 recyclerView에 보이는 뷰의 0번째를 가져와 넣는다.

    getChildAdapterPosition로 topChild 에 해당하는 position을 가져온다.

    position값이 RecyclerView.NO_POSITION인지 확인한다.

     

    다음 순서로 넘어가기 전에 Activity.kt에서 인터페이스를 구현한다.

        private fun getSectionCallback(): RecyclerSectionItemDecoration.SectionCallback {
            return object : RecyclerSectionItemDecoration.SectionCallback {
                override fun isSection(position: Int): Boolean {
                    return adapter.isHolder(position)
                }
    
                override fun getHeaderLayoutView(list: RecyclerView, position: Int): View? {
                    return adapter.getHeaderLayoutView(list, position)
                }
            }
        }

    그리고 Adapter.kt 에 다음과 같은 메소드를 만든다.

        fun getHeaderLayoutView(list: RecyclerView, position: Int): View? {
            val lastIndex =
                if (position < recyclerItemList.size) position else recyclerItemList.size - 1
            for (index in lastIndex downTo 0) {
                val model = recyclerItemList[index]
                if (model.type == TYPE_HOLDER) {
                    return LayoutInflater.from(list.context)
                        .inflate(R.layout.hold_item, list, false);
                }
            }
            return null
        }
        fun isHolder(position: Int): Boolean {
            return recyclerItemList[position].type == TYPE_HOLDER
        }

    다시 ItemDecoration 으로 와서 getHeaderLayoutView를 이용해 topChildPosition에 해당하는 뷰를 찾아온다. 

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

    fixLayoutSize에서 가져온 뷰를 측정해준다.

     

    	val contactPoint = currentHeader.bottom
            val childInContact: View = (getChildInContact(parent, contactPoint) ?: return)
    
         

    contactPoint 로 현재 topChildPosition 에 해당하는 뷰의 bottom 값을 구하고

    getChildInContact로 인접한 뷰를 구한다.

    private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? {
            var childInContact: View? = null
            for (i in 0 until parent.childCount) {
                val child = parent.getChildAt(i)
                if (child.bottom > contactPoint) {
                    if (child.top <= contactPoint) {
                        childInContact = child
                        break
                    }
                }
            }
            return childInContact
        }

    그 인접한 뷰 childInContact에 해당하는 포지션을 가져온다.

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

    childAdapterPosition 이 리스트 뷰의 최 상위에 있을 때 moveHeader로 밀려나는 것처럼 그리고

    그 외엔 상단에 고정되어 있는 것처럼 보이도록 drawHeader로 그린다.

      if (sectionCallback.isSection(childAdapterPosition)) {
                moveHeader(c, currentHeader, childInContact)
                return
            }
            drawHeader(c, currentHeader)
        private fun moveHeader(c: Canvas, currentHeader: View, nextHeader: View) {
            c.save()
            c.translate(0f, nextHeader.top - currentHeader.height.toFloat())
            currentHeader.draw(c)
            c.restore()
        }
    
        private fun drawHeader(c: Canvas, header: View) {
            c.save()
            c.translate(0f, 0f)
            header.draw(c)
            c.restore()
        }
    

     

    음.. 이거 하느라 잠도 많이 못잤는데... 하고 나니깐 까먹을까봐 무서워서 얼른 기록 남기는 중이다..

     

    github에 올려놨으니 수정할 부분은 PR 부탁드립니다..

     

    남을 이해 시키는 것이 실력인데 아직 누군가를 이해시킬 능력이 되지 않으니깐..

    주기 적으로 설명을 다시 업데이트 할 예정

     

Designed by Tistory.