(周期计划-1)RecyclerView分割线的实现

​> 2018年技术周期计划:周期计划-1(2018/1/1-2018/1/7)

写在前面

以前给RecyclerView加分割线,都是简单的在Item的布局中自己添加想要的效果。这种做法当然是没有什么毛病,不过说到底还是有一些投机取巧。毕竟谷歌提供了正经的分割线的绘制方式。

至于使用哪种方式其实个人任务都无伤大雅本身就是单纯为了完成业务、实现效果。结果最重要,至于使用了什么手段,萝卜青菜各有所爱吧。

这一章的内容,差不多是通过继承RecyclerView.ItemDecoration来完成自定义分割线;下一章的内容是关于分割线在RecyclerView的一种封装思想。

正文

让我们先效果:


(周期计划-1)RecyclerView分割线的实现
123.gif

对应代码如下:

代码非常的简单,效果本身也没有什么意义。仅仅是为了展示这三个方法对应的效果

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        c.drawRect(0, 0, 100, 100, onDrawPaint);
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        c.drawRect(100, 0, 200, 100, onDrawOverPaint);
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        outRect.top = 100;
    }

onDraw()

onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)

此方法的效果所绘制的效果只会在Item布局的下边,因此如果我们Item长宽都全屏的话我们是看不到这个onDraw绘制的效果。

这里我们能看到,回调方法里有Canvas,因此我们爱咋画咋画,此外还有RecyclerView对象以及状态的回调,所以我们可以做的事情很多。当然这里不先展开,只是为了先笼统的感受一下这三个方法的效果。

onDrawOver()

onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state)

此方法和onDraw()的回调参数没什么区别,只是效果是在Item的上边,也就是说始终悬浮在上边。

getItemOffsets()

getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)

getItemOffsets相对比较的特殊,因为它的回调参数中有一个变化比较大,后边俩个就不说了。
这里解释一下前面俩个:

  • outRect可以理解为用于控制Item内边距的对象。就像上述代码中outRect.top = 100,给Item增加了一个100px的paddingTop的效果。

  • view就是Item对象

梳理:

其实看到这,我们基本上就能够清楚ItemDecoration的使用方法。所谓的分割线就是我们画的一个背景,通过Item的间距已经移动来显示出不同的效果,仅此而已。无非是我们在实现效果的时候,需要根据需求重写onDraw()/onDrawOver()完成分割线的绘制,通过getItemOffsets()来确定分割线的显示。


简单实战:虚线分割

依然先看一下效果:


(周期计划-1)RecyclerView分割线的实现
23.gif

如果我们能够理解上诉的三个回调方法,其实代码非常的简单:

    //在构造方法中初始化画笔
    Paint dashPaint = new Paint();
    dashPaint.setAntiAlias(true);
    dashPaint.setColor(Color.BLACK);
    dashPaint.setStrokeWidth(2);
    dashPaint.setStyle(Paint.Style.STROKE);
    dashPaint.setPathEffect(new DashPathEffect(new float[]{10, 5}, 0));

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        final int left = 0;
        final int right = parent.getWidth();
        int viewSize = parent.getChildCount();
        for (int i = 0; i < viewSize; i++) {
            final Path path = new Path();
            final View childView = parent.getChildAt(i);
            final int bottom = childView.getBottom() + 50;
            path.moveTo(left, bottom);
            path.lineTo(right, bottom);
            c.drawPath(path, dashPaint);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        int position = parent.getChildAdapterPosition(view);
        if (position % 2 == 0) {
            outRect.bottom = 100;
        }
    }

思路:

代码思路非常的简单,getItemOffsets()设置一个合适的内边距,这里我们只是给偶数位置的Item设置的底边距;内边距设置好了之后,那就是onDraw()中根据位置画线,仅此而已。


进阶实现

这个效果算是比较常见的一种需求:(请无视掉粗糙的效果)

(周期计划-1)RecyclerView分割线的实现
34.gif

思路:

思路说起来很简单,就是给Item是同一个省下的数据设置内间距,并且间距的位置显示省的信息。然后悬浮在Item的最上边,此外还存在省份Item的挤压效果。

说是如此,我们具体还要去实现。

1、为设置间距,思考数据结构

因为我们需要在getItemOffsets()方法中给需要设置间距的Item设置间距,因此我们需要一个合适的判断,这里我们从Adapter中的数据结构入手。

public class CityModel {
    public String cityName;
    public String provinceName;
    public boolean isFirst = false;
}
  • cityName很容易理解,就是城市的名字。

  • provinceName是省的名字,这样设计的考虑是,每个市都会对应一个省,同一个省下的市是需要显示一个省即可。

  • isFirst是为了,让ItemDecoration进行判断是否需要设置内边距。因为我们只在同一个省的一系列数据的顶部进行设置内边距,因此如果是true那就设置内边距。

以上就是我们为这个效果设置的数据结构。

2、自定义ItemDecoration

这里直接上代码:

public class MyCityItemDecoration extends RecyclerView.ItemDecoration {
    private Paint mTextPaint,mBgPant;
    private int mTopHeight=100;
    public MyCityItemDecoration() {
        //简单初始化画笔
        mTextPaint = new Paint();
        mTextPaint.setColor(Color.WHITE);
        mTextPaint.setTextSize(36);

        mBgPant = new Paint();
        mBgPant.setColor(Color.BLACK);
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        final int itemCount = state.getItemCount();
        final int childCount = parent.getChildCount();
        final int left = parent.getLeft() + parent.getPaddingLeft();
        final int right = parent.getRight() - parent.getPaddingRight();
        String preProvinceName;
        String currentProvinceName = null;
        for (int i = 0; i < childCount; i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(view);
            preProvinceName = currentProvinceName;
            currentProvinceName = mListener.getProvince(position);
            /**
             *  这里为什么没有使用mListener.getIsFirst()来进行判断。
             *  因为每一次的滑动都会造成这个方法的回调,因此我们每次都会重新去我们的Canvas进行重绘。
             *  如果我们单纯通过isFirst去判断,那么会造成这种情况:划出第一个数据时,我们的isFirst
             *  将不在为true,引起重绘之后,那么我们的分割线也将会消失。
             */
            if (currentProvinceName == null || TextUtils.equals(currentProvinceName, preProvinceName))
                continue;
            int viewBottom = view.getBottom();
            //分割线悬浮:因为bottom用于我们的分割线绘制,而它在非挤压状态下永远不会小于分割线高度(mTopHeight)
            float bottom = Math.max(mTopHeight, view.getTop());

            if (position + 1 < itemCount) {
                /**
                 *  挤压效果的计算:
                 *      挤压效果只会出现在下一组数据的分割线接触到当前悬浮的分割线。
                 *      我们只需要处理悬浮分割线慢慢上移即可,因为下一组分割线是正常移动的。而我们悬浮的分割线
                 *      被固定到了那个高度(bottom),因此我们来改变bottom的值即可。
                 */
                if (mListener.getIsFirst(position+1) && viewBottom < bottom) {
                    bottom = viewBottom;
                }
            }
            //绘制背景
            c.drawRect(left, bottom - mTopHeight, right, bottom, mBgPant);
            Paint.FontMetrics fm = mTextPaint.getFontMetrics();
            //绘制文字
            float baseLine = bottom - (mTopHeight - (fm.bottom - fm.top)) / 2 - fm.bottom;
            c.drawText(currentProvinceName, left, baseLine, mTextPaint);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        //通过判断我们的数据结构的isFirst是否为true来决定是否需要设置内边距
        if (mListener.getIsFirst(parent.getChildAdapterPosition(view))) {
            outRect.top = mTopHeight;
        }
    }

    /**
     * 为什么需要写一个回调,那是因为:
     *     我们需要Adapter中的数据结构进行判断,然而我们没办法在这个类中获取到Adapter的数据结构。
     *     因此我们需要一个回调,让外部去为我们提供这个值。
     */
    private GetDataListener mListener;
    public void setGetDataListener(GetDataListener listener) {
        mListener = listener;
    }
    public interface GetDataListener {
        boolean getIsFirst(int position);
        String getProvince(int position);
    }
}

代码解释在注释中,其实看到这,我们差不多能够发现一个问题,所谓的画分割线和我们自定义View时没有什么特别大的区别...还是有区别的,比自定义View简单...

尾声

2018年的第一次的技术周期技术的上部分到这就接近于尾声了,本章主要是梳理分割线的用法,并引入一个小例子。下部分将结合公司的一段关于分割线封装的代码,让我们从代码封装的层面上升到封装思想层面上。

本菜开源的一个自己写的Demo,希望能给Androider们有所帮助,水平有限,见谅见谅…

https://github.com/zhiaixinyang/PersonalCollect

2018年7月2号,我正式开始了自己的Android工作,为了能够让自己能够好好完成工作,并且能够快速得到技术提升。所以打算以公众号的方式去敦促自己学习,我会把自己日常的学习笔记发布到公众号上,如果可以,共同进步!~


(周期计划-1)RecyclerView分割线的实现
个人公众号
上一篇:从 Spark 到 Kubernetes — MaxCompute 的云原生开源生态实践之路


下一篇:linux下搭建iredmail邮件服务器