作者:点先生 时间: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 <span class="hljs-built_in">set</span>CreateTimeListener(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);
            //文字居中线
            <span class="hljs-built_in">float</span> x = (view.getRight() - view.getLeft())/2;
            //文字所占用的边框top,bottom位置
            top = view.getTop() - topDevider;
            bottom = view.getTop() - bottomDevider;
            //获取文字的Bounds
            textPaint.getTextBounds(textString, 0, textString.length(), textBounds);
            //计算文字的基线
            <span class="hljs-built_in">float</span> y = ((bottom + top)/2) + (textBounds.height()/2);

            c.drawText(textString, x, y, textPaint);
        }<span class="hljs-keyword">else</span> {
            <span class="hljs-built_in">float</span> top = view.getBottom();
            <span class="hljs-built_in">float</span> 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 里面这么多属性,它又不知道我要什么东西,全都回调给我,也太傻逼了。

  • Android

    开放手机联盟(一个由 30 多家科技公司和手机公司组成的团体)已开发出 Android,Android 是第一个完整、开放、免费的手机平台。

    293 引用
感谢    赞同    分享    收藏    关注    反对    举报    ...