利用ItemDecoration实现悬浮头部

在我们的日常开发中,RecyclerView已经被使用的越来越广泛,今天来讲一讲使用ItemDecoration来实现项目中需要的悬浮头部的效果。我们使用listView就可以知道,直接从xml文件中使用 android:divider 这个属性就可以直接设置listVie中itemw的分割线,可以设置分割线的drawable,但是在recyclerView中却没有这个属性了,有时候为了图方便,直接在RecyclerView的item里面通过设置view的方式设置分割线,Google其实并不推荐这种做法的,因为这样设置了之后,一些notifyItemInsert等这样的效果就失去了,因为,为了更灵活的定制分割线,Google给我们提供了一个类RecyclerView.ItemDecoration,如果想要实现自定义分割线的话需要去继承这个类,然后实现他的几个方法。具体如下:

  1. 如果懒的实现分割线,Google给我们提供了一个默认的分割线,DividerItemDecoration(),里面两个参数,一个context,一个是分割线的方向,横向或者纵向DividerItemDecoration.Vertical或者DividerItemDecoration.Horizantal

  2. 我们点进源码就可以看到,RecyclerView.ItemDecoration并不复杂,是一个抽象类,并且只有几个方法,主要的方法分别为 getItemOffsets、onDraw、onDrawOver,其余的都是已废弃的,也是相互调用的方法,三个方法如下,

public abstract static class ItemDecoration {
       
        public void onDraw(Canvas c, RecyclerView parent, State state) {
            onDraw(c, parent);
        }

        @Deprecated
        public void onDraw(Canvas c, RecyclerView parent) {
        }

        public void onDrawOver(Canvas c, RecyclerView parent, State state) {
            onDrawOver(c, parent);
        }

        @Deprecated
        public void onDrawOver(Canvas c, RecyclerView parent) {
        }

        @Deprecated
        public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
            outRect.set(0, 0, 0, 0);
        }

        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
            getItemOffsets(outRect, ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition(),
                    parent);
        }
    }

 

getItemOffsets方法包含4个参数,其中outRect 若不设置则是一个全为 0 的 Rect。view 指 RecyclerView 中的 Item。parent 就是 RecyclerView 本身,state 就是一个状态。

    @Deprecated
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        outRect.set(0, 0, 0, 0);
   
    }

可以看这张图,绿色区域代表 RecyclerView 中的一个 ItemView,而外面橙色区域也就是相应的 outRect,也就是 ItemView 与其它组件的偏移区域,等同于 margin 属性,通过复写 getItemOffsets() 方法,然后指定 outRect 中的 top、left、right、bottom 就可以控制各个方向的间隔了。注意的是这些属性都是偏移量,是指偏移 ItemView 各个方向的数值。当然,这个方法只是设置item的偏移量,具体要设置背景什么的要看下面的几个方法。

 这里写图片描述

我们知道,onDraw()方法是自定义View必不可少的方法,具体就是绘制出自己想要的外观,里面三个参数canvas、recyclerView以及状态,这个方法是配合前面一个 getItemOffsets方法一起绘制的,getItemOffsets 撑开了 ItemView 的上下左右间隔区域,而 onDraw 方法通过计算每个 ItemView 的坐标位置与它的 outRect 值来确定它要绘制内容的区间。需要注意的是,onDraw方法是在绘制每一个itemView之前进行绘制的,如果绘制不当的话,itemView的内容就很可能会覆盖掉我们在onDraw方法里绘制的内容。

假设,我们要设计一个高度为 1 px 的分割线,那么我们就需要在每个 ItemView top位置上方画一个 1 px 高度的矩形,然后填充颜色为红色。 代码也挺简单。

/**
     * 针对每一个ItemView设置偏移
     * outRect  全为0的一个矩形Rect
     * view     RecyclerView中的Item
     * parent   RecyclerView本身
     * state    Item的状态
     */
    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        super.getItemOffsets(outRect, view, parent, state)
        //设置偏移的高度
        outRect.top = 1
    }

需要注意的一点是 getItemOffsets 是针对每一个 ItemView,而 onDraw 方法却是针对 RecyclerView 本身,所以在 onDraw 方法中需要遍历屏幕上可见的 ItemView,分别获取它们的位置信息,然后分别的绘制对应的分割线。

/**
     * 针对 RecyclerView 本身,需要遍历屏幕上可见的Item
     * 在Item之前绘制
     * 通过计算每个Item的坐标位置与outRect确定绘制内容的区间
     */
    override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDraw(c, parent, state)
}

 接下来是onDrawOver方法,可以看到,onDrawOver和onDraw方法差别并不大,方法只是名字不一样而已,区别就是onDraw是绘制在itemView的内容之前,而onDrawOver则是在绘制itemView之后进行绘制,可以覆盖itemView的内容之上,因此,我们可以制造出我们想要的如时光轴效果,但是,我们今天要研究的是实现悬浮头部,就是每个item都有自己的头部,当上移至移出屏幕时,头部依然悬浮在最上方,常见的就是微信联系人那种效果了。

/**
     * Item之后绘制
     * 当前的item
     * 1、不是屏幕上第一个可见的Item,但是是组内第一个Item,此时需要绘制
     * 2、不是屏幕上第一个可见的Item,而且不是组内第一个Item,此时不需要绘制
     * 3、是屏幕上第一个可见的Item,需要绘制,位置固定
     */
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)
}

 接下来看看如何实现,首先,新建一个类继承RecyclerView.ItemDecoration,然后,依然在我们的getItemOffsets方法里面为我们要绘制的头部设置合适的区间,如果想绘制文字,要先测量出文字的高度,然后再设置outRect的top值,具体初始化的代码如下,

init {
        mPaint.color = Color.YELLOW
        mPaint.isDither = true

        mTvPaint.color = Color.RED
        mTvPaint.isDither = true
        mTvPaint.textSize = TypedValue.applyDimension(COMPLEX_UNIT_SP,12f,context.resources.displayMetrics)
        val rect = Rect()
        mTvPaint.getTextBounds("王",0,1,rect)
        fontMetricsInt = mTvPaint.fontMetricsInt
        mTvHeight = rect.height()
        Log.e(TAG,"文字高度-> $mTvHeight")
    }

 

    override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
        super.getItemOffsets(outRect, view, parent, state)
        //设置偏移的高度为文字的高度
        outRect.top = mTvHeight
    }

紧接着,在onDraw方法里,绘制出想要的文字及背景,代码如下,这里只是简单的绘制,需要注意的是,由于android坐标系的原因,我们在drawRect的时候,top的值应该为view.top-tvHeight,因为向下为正,然后对我们想要绘制的效果进行分析:

当当前的itemView为屏幕上第一个可见的 ItemView,此时需要绘制,而且该起始位置应该依附在 RecyclerView 的内容起始位置,因为只有这样才会表现出悬浮的效果。因此,我们可以对代码进行这样编写,注释写的比较清楚了

/**
     * Item之后绘制
     * 当前的item
     * 1、不是屏幕上第一个可见的Item,但是是组内第一个Item,此时需要绘制
     * 2、不是屏幕上第一个可见的Item,而且不是组内第一个Item,此时不需要绘制
     * 3、是屏幕上第一个可见的Item,需要绘制,位置固定
     */
    override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
        super.onDrawOver(c, parent, state)
        val childCount = parent.childCount
        for(i in 0 until childCount){
            var view = parent.getChildAt(i)
            var index = parent.getChildAdapterPosition(view)
            var left = parent.paddingLeft.toFloat()
            var right = (parent.width - parent.paddingRight).toFloat()
            //不是屏幕上第一个可见的Item
            if (i!=0){
                var top = (view.top - mTvHeight).toFloat()
                var bottom = view.top.toFloat()
                //
                drawHeader(view.top,c,left,top,right,bottom)
            }else{
//                屏幕上第一个可见的Item  此时因为要悬浮,所以要以recyclerView的顶部为准,而不是item了,位置要注意下
                var top = parent.paddingTop
                var sugTop = view.bottom - mTvHeight
                // 当 ItemView 与 Header 底部平齐的时候,判断 Header 的顶部是否小于
                // parent 顶部内容开始的位置,如果小于则对 Header.top 进行位置更新,
                //否则将继续保持吸附在 parent 的顶部
                if (sugTop<=top){
                    top = sugTop
                }
                var bottom = top + mTvHeight
                c.drawRect(left, top.toFloat(), right, bottom.toFloat(),mPaint)
                //之前写的是  bottom/2   改成  top + mTvHeight/2
                val baselineY = bottom/2 +(fontMetricsInt.bottom-fontMetricsInt.top)/2-fontMetricsInt.bottom
                Log.e(TAG,"onDrawOver-> $left,$top,$right,$bottom")
                Log.e(TAG,"onDrawOver基线-> $baselineY")
                c.drawText("王", left, baselineY.toFloat(),mTvPaint)
            }
        }
    }

这里有几个点需要注意:

1.由于我们这里只是简单的对每一个item都设置的header,因为在判断的时候只判断了位置为0和不为0。不为0的时候按照正常的情况进行绘制,为0的时候此时我们就要绘制在recyclerView的顶部,此时的位置应该以recycerView为准,区别就是:

var top = parent.paddingTop

然后这里的baseline应该是val baselineY = bottom/2 +(fontMetricsInt.bottom-fontMetricsInt.top)/2-fontMetricsInt.bottom

因为文字的center就直接是bottom/2,即(top+mTvHeight)/2

2.在实现该效果之后,运行发现有一点小bug,发现顶上去的效果不是很理想,是因为文字的高度没有计算正确,因为之前考虑的文字的中心位置是bottom/2,在加入吸顶的代码之后,因为我们修改了top的值,所以会引起一些小的误差,在这里文字中心位置修改为top+mTvHeight/2,然后测试就可以了

 

 

 

 

 

你可能感兴趣的:(利用ItemDecoration实现悬浮头部)