自定义View 之 RecyclerView.ItemDecoration

2,831 阅读6分钟

作者:点先生 时间:2019.1.26

是这么一回事

年底了,赶项目,于是忙了一个月业务,忙了一个月没有营养的东西。为啥说没营养,因为就是很简简单单的展示,没有啥东西可写。我差点要搬出11月份的腾讯面试经历了,就在这时我给自己挖了个坑。 我本人的自定义View的能力是很差的,之前也没有写过,一直都用android自带或者github上写好的东西。所以这个坑挖的还是值。

坑的来源

之前我们有一个报警消息展示界面,是这样的;

有个功能是这样的,红点显示未读,点击一下就能消灭红点。
问题就来了:后台表示不能提供是否已读的状态,我表示我这边本地存储报警消息状态并不合理。然后我就骚了一波,说接口不用改,我自己这边处理。其实我想的就是仿微信朋友圈里面的文字分割线“以下是已读内容”,这样就不用处理每一条消息了,哈哈哈哈哈哈哈。

两种方案与思路

一开始我想到了两种方案:
A :类似于添加head,footer,写个新的viewholder进去。
优点:网文较多;布局复杂的情况下比较好管理修改;
缺点:修改的东西比较多。
B:自定义RecyclerView.ItemDecoration
优点:修改东西较少;自定义的优点;
缺点:自定义的缺点;\

思路:无论是A方案还是B方案,我都需要知道这个分割线的position,在这里我是将上一次请求到的数据中最新一条的createTime存入SP中,我将通过这个值去对比每一次请求下来的数据集的createTime,当他相等时,这个item的position,就应该是分割线的position。(这里选择对比条件是一定要选择一个唯一,不重复的)。

在A方案中,adapter得到list后,可以找到分割线的position,然后在此position返回TextDivider的Viewholder。麻烦在于position之后的数据,TextDivider之后的每一个数据的position都必须+1。每一次都得重新去算。每次滑动都会算,这里处理起来可能不是很方便,而且会增加许多属性帮助确定真正的position。弃之

所以我选择了B方案。也是对自己个机会去学习自定义view。

“懒惰是第一生产力” —— 沃·兹基朔德

RecyclerView.ItemDecoration

public class TextDivider extends RecyclerView.ItemDecoration {
    public void onDraw(Canvas c, RecyclerView parent, State state){}
    public void onDrawOver(Canvas c, RecyclerView parent, State state){}
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state){}
}

创建一个类去继承RecyclerView.ItemDecoration,有三个方法需要重写;
执行顺序是getItemOffsets(),onDraw(),onDrawOver();

看名字,ItemDecoration是一个装饰者,并且是给每一个item加一个装饰。我们常用场景是写个分割线,各种分割线,希望大家能通过我这篇文章,对ItemDecoration有更多新的骚操作。

我们先来说关于这三个方法的用法。

getItemOffsets

第一个参数Rect,看名字不是不太容易知道有啥用。其实它就是我们当前item的矩形。我们可以通过这个参数获取到他的top、bottom、left、right。也可以给这几个属性赋值。当我们不给这几个参数赋值时,默认为0;

当我们设置了rect的参数之后,就有了上图左边的效果,如果不赋值,默认就是右边这个样子。

onDraw与onDrawOver

这就是当灵魂画家的部分了,用canvas可以画你想画的东西。
parent帮助你获取当前item的属性。
state获取当前recycleView的状态。
这两个方法的区别在于先后顺序。

onDraw画的东西会被item布局挡住;
item布局里的东西会被onDrawOver挡住;
明白了吧?

左边的圆就是onDraw画的,右边的圆就是onDrawOver画的
tips!!! 上一个的item可能会被下一个item的onDraw东西给挡住,所以在画的时候一定要控制好你的范围。

代码!安排!

    private int bottomDevider;//分割线宽度
    private int topDevider;//文字分割宽度
    private String textString;//分割线的文字

    Rect textBounds = new Rect();

    private Paint dividerPaint;
    private Paint textPaint;

    private Long lastReadMsgDate;//上次获取数据集的最新数据的createtime

除了textBounds ,其他都很容易理解是干嘛的。

    public TextDivider(Context context) {
        dividerPaint = new Paint();
        textPaint = new Paint();
        //设置分割线颜色
        dividerPaint.setColor(context.getResources().getColor(R.color.whitesmoke));
        textPaint.setColor(context.getResources().getColor(R.color.vpi__bright_foreground_disabled_holo_dark));
        textString = "--------------以-下-是-已-阅-读-内-容--------------";
        textPaint.setTextSize(32);
        textPaint.setTextAlign(Paint.Align.CENTER);
        //设置分割线宽度
        bottomDevider = context.getResources().getDimensionPixelSize(R.dimen.space_2);
        topDevider = 100;
        lastReadMsgDate = Long.parseLong(SPM.getStr(BaseApp.getContext(), LC.CONSTANT, LC.LAST_REMIND_MSG_DATA, "0"));
    }

textPaint.setTextAlign(Paint.Align.CENTER); 这句代码是让所写的文字,居于原点水平居中。

    private CreateTimeListener mListener;

    public void setCreateTimeListener(CreateTimeListener listener) {
        mListener = listener;
    }
    public interface CreateTimeListener {
        long getCreateTime(int position);
    }

这是接口用来从外部获取当前item的createTime。

    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.bottom = bottomDevider;
        if(lastReadMsgDate == mListener.getCreateTime(parent.getChildAdapterPosition(view))){
            outRect.top = topDevider;
        }
    }

给每个item下方增加一段距离,用于画普通的分割线。
在需要画文字分割线的上方增加一段距离,用于画文字分割线

    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        final int childCount = parent.getChildCount();
        final int left = parent.getLeft() + parent.getPaddingLeft();
        final int right = parent.getRight() - parent.getPaddingRight();
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(view);
            if(lastReadMsgDate == mListener.getCreateTime(position)){
                float top = view.getBottom();
                float bottom = view.getBottom() + bottomDevider;
                c.drawRect(left, top, right, bottom, dividerPaint);
                top = view.getTop() - bottomDevider;
                bottom = view.getTop();
                c.drawRect(left, top, right, bottom, dividerPaint);

                //文字居中线
                float x = (view.getRight() - view.getLeft())/2;
                //文字所占用的边框top,bottom位置
                top = view.getTop() - topDevider;
                bottom = view.getTop() - bottomDevider;
                //获取文字的Bounds
                textPaint.getTextBounds(textString, 0, textString.length(), textBounds);
                //计算文字的基线
                float y = ((bottom + top)/2) + (textBounds.height()/2);

                c.drawText(textString, x, y, textPaint);
            }else {
                float top = view.getBottom();
                float bottom = view.getBottom() + bottomDevider;
                c.drawRect(left, top, right, bottom, dividerPaint);
            }
        }
    }

在画文字分割线的时候我觉得比较烦的就是算距离。
通常我们用canvas画东西的时候的原点,在左上角。

而文字分割线的原点在第一个字的左下角偏左一点点的距离。

文字垂直居中

关于点先生有多帅就不多讲了。这里说一说文字居中的问题。
本帅了解也不是很深, 就只找到了一种方法让它居中。
水平居中很简单,上面已经说到过了。

item的原点在左上角蓝色圆的位置,文字要想垂直居中,原点应该在紫色圆的位置。 找到紫圆的Y轴坐标就可以了。
((bottom + top)/ 2) + (文字所占的高度 / 2)

文字所占高度,就是最后的难点了。 各种get方法都找不到文字高度,最后在画文字时候传的一个参数Rect给找到方法了。

  textPaint.getTextBounds(textString, 0, textString.length(), textBounds);

跟上文说的一样,就是矩形,这里传进去的textBounds就是Rect,穿进去之后可以获取到当前文字的一些属性,问题迎刃而解。

在recycleView使用处调用也很简单。

        textDivider = new TextDivider(getContext());
        textDivider.setCreateTimeListener(new TextDivider.CreateTimeListener() {
            @Override
            public long getCreateTime(int position) {
                if (cacherRmindMsgList.size()==0) return 0L;
                else return cacherRmindMsgList.get(position).getCreateTime();
            }
        });
        recyclerView.addItemDecoration(textDivider);

嘻嘻!

后续

做完之后有个疑问。为啥获取文字属性的没有一个叫get***()的方法!
还要我亲自传一个参数进去接受这些东西。给个回调接口也好啊!

打脸也挺快,自己亲手写过的接口隔离原则都差点忘了。
Rect里面这么多属性,它又不知道我要什么东西,全都回调给我,也太傻逼了。