通用RecycleView吸顶控件

1,458 阅读2分钟

有需求要实现吸顶效果, 掘金里翻到一篇大佬的文章, 借鉴了一下.

基本思路:

  1. 使用RecycleView.ItemDecoration 重写onDrawOver()方法
  2. 为了通用性, 不使用Paint等graphics接口绘制. 直接获取RecycleView的itemView绘制在onDrawOver给定的Canvas上.
  3. 在无法获取itemView时, 通过反射创建itemView
  4. 反射创建出来的itemView, 需要走一遍measure()layout()否则无法绘制

遗留问题:

  • 吸顶Item根布局中的<android:background="">属性无法绘制, 可以添加子View, 并设置background替代根布局的背景属性

完整代码有点长, 贴一些关键代码吧:


    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)
        val position = (parent.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
        
        //获取标题的View
        val gid = callback.getGroupId(position)

        val titleView = mTitleViewSet[gid]?: mTitleViewSet.run{

            val gp = callback.getCurrTopGroupPosition(position)
            
            val holder = parent.findViewHolderForAdapterPosition(gp)
            if(holder != null){
                //已经绘制过
                mTitleViewSet.put(gid, holder.itemView)
                
            }else {
                //没有绘制过, 创建一个
                setupUnDrawView(parent, gp, gid, state)
                
            }
            
            mTitleViewSet[gid]
        }
        
       //判断& 计算 bottom高度 和大佬文章一致... 代码省略, 
       
       //将获取到的View 绘制在canvas上. 
       titleView.draw(c)
   }
/**
 * 利用反射, 设置RecycleView还没有绘制过的吸顶View
 */
private fun setupUnDrawView(parent: RecyclerView, gp: Int, gid: Long, state: RecyclerView.State) {
    if (mDecoration == null) {
        mDecoration = ArrayList()
        var i = 0
        var d: ItemDecoration?
        while (parent.getItemDecorationAt(i).also { d = it } != null) {
            if (d != null && d != this@TopSeekDecoration) {
                //收集 其他边距Decoration, 用来绘制边距
                mDecoration?.add(d!!)
            }
            ++i
        }
    }

    val recycler = with(parent.javaClass
            .getDeclaredField("mRecycler")) {

        isAccessible = true
        get(parent) as RecyclerView.Recycler
    }

    val v = with(recycler.javaClass
            .getDeclaredMethod("getViewForPosition",
                    Int::class.javaPrimitiveType,
                    Boolean::class.javaPrimitiveType)) {

        isAccessible = true

        //生成view
        invoke(recycler, gp, true) as View
    }

    //计算view的宽高
    parent.layoutManager.measureChild(v, 0, 0)

    //缓存下来
    mTitleViewSet.put(gid, v)
    val margins = Rect()

    //计算边距
    mDecoration?.let {
        for (dec in it) {
            dec.getItemOffsets(margins, v, parent, state)
        }
    }

    mTitleHeight = v.measuredHeight
    
    //没有绘制过的View, 需要执行一次layout, 确认各个子布局的坐标
    v.layout(margins.left, margins.top, margins.right, margins.bottom)
    
}
/**
 * 回调
 */
interface TitleDecorationCallback {
    /**
     * 获取position对应的分组(标题)id
     * @param position
     * @return
     */
    fun getGroupId(position: Int): Long

    /**
     * 获取标题的描述, debug用
     * @param position
     * @return
     */
    fun getItemDes(position: Int): String
    
    /**
     * 获取标题所在的position
     * @param position
     * @return
     */
    fun getCurrTopGroupPosition(position: Int): Int
    
}

使用方法:

val topSeek = TopSeekDecoration(this, object : TopSeekDecoration.TitleDecorationCallback {
    override fun getCurrTopGroupPosition(position: Int): Int {
        mAdapter?.data?.run {
            for(i in position downTo 0){
                val bean = get(i)
                if(bean.isTitle){
                    return i
                }
            }
        }
        return 0
    }
    
    override fun getGroupId(position: Int): Long {
        return mAdapter?.data?.get(position).groupId
    }

    override fun getItemDes(position: Int): String {
        mAdapter?.data?.run {
            return get(position).toString()
        }
        return "未知 Item"
    }

})

mRecycleView?.addItemDecoration(topSeek)