深入理解 RecyclerView之ItemDecoration(源码和图解)

RecyclerView已经推出了两年多了,大家也都领教了它的强大,但是它也有不足之处,没有分割线!现通过官方给的样例和画一些图帮助分析RecyclerView是如何绘制间隔线的。

学习思路:

一.基本知识点
二.Item和间隔线的绘制流程
三.ItemDecoration代码讲解
1.item垂直排列分析
2.item横向排列分析
3.网格间隔线分析

一、基本知识点

偏移量的概念,看清下图每张图中设置的四个参数,分别代表了(左,上,右,下)的偏移量。红色部分就是偏移出去的部分,其实就是我们需要的间隔线,不过我们只需要一个方向的偏移量就够了,就是在下面的或者右边的偏移量,后面会细讲。


image.png

内边距padding,和外边距margin,分析代码的时候会用到。


image.png

二、Item和间隔线的绘制流程

RecyclerView绘制item和间隔线的流程:item1——间隔线——Item2——间隔线...如此往复,所以绘制完item每次绘制间隔线的时候都需要测量绘制。

先看一张图:
image.png

一定要把间隔线想象成矩形,就是图中黑色矩形

三、ItemDecoration代码讲解

RecyclerView没有像之前ListView提供divider属性,而是提供了方法recyclerView.addItemDecoration(),其中ItemDecoration需要我们自己写,有的童鞋可能会偷懒去网上找别人写好的,但是如果功能稍加改动就不之所错了,所以了解原理很重要。

ItemDecoration类主要是三个方法:

public void onDraw(Canvas c, RecyclerView parent, State state)
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
public void onDrawOver(Canvas c, RecyclerView parent, State state)

主要讲解前面两个方法。
我们自己写的类需要继承ItemDecoration且重写onDraw(),getItemOffsets()方法才能完成我们需要的间隔线。getItemOffsets()方法第一个参数可以看出,绘制线条其实就是绘制一个矩形,通过方法名可以看出是获得条目的偏移量,只需要实现outRect.set(left,top,right,bottom);四个参数就是左上右下四个方向的偏移量,
我们可以通过LinearLayoutManager.VERTICAL来判断是绘制横向线条还是纵向线条。

@Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
            State state) {
        
        if(mOrientation == LinearLayoutManager.VERTICAL){
            //item垂直排放,所以绘制水平线条,左上右偏移量都是0
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        }else{//item水平排放,所以绘制处置线条,左上下偏移量都是0
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0 );
        }
        
    }

mDivider.getIntrinsicWidth()是需要绘制的Drawable的高度。
再来看onDraw()方法

@Override
    public void onDraw(Canvas c, RecyclerView parent, State state) {
        if(mOrientation == LinearLayoutManager.VERTICAL){
              //垂直,这里的垂直是item垂直,画水平线
            drawVertical(c,parent);
        }else{//水平,这里的水平是item水平,画垂直线条
            drawHorizontal(c,parent);
        }
        
        super.onDraw(c, parent, state);
    }
    
    private void drawHorizontal(Canvas c, RecyclerView parent) {
                //这里的水平是item水平,画垂直线条
        int top = parent.getPaddingTop();
        int bottom = parent.getHeight() - parent.getPaddingBottom();
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount ; i++) {
            View child = parent.getChildAt(i);
            
            RecyclerView.LayoutParams params = (LayoutParams) child.getLayoutParams();
            int left = child.getRight() + params.rightMargin + Math.round(ViewCompat.getTranslationX(child));
            int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top , right, bottom);
            mDivider.draw(c);
        }
    }

    private void drawVertical(Canvas c, RecyclerView parent) {
        // 这里的垂直是item垂直,画水平线
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount ; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (LayoutParams) child.getLayoutParams();
            int top = child.getBottom() + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child));
            int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top , right, bottom);
            mDivider.draw(c);
        }
    }

看清楚代码里的注解,不要搞混了。

(一)item垂直排列分析

查看drawVertical(Canvas c, RecyclerView parent)方法

private void drawVertical(Canvas c, RecyclerView parent) {
        // 这里的垂直是item垂直,画水平线
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        int childCount = parent.getChildCount();
        //循环的意思是:每个item就是一个child,每个item下面有一个线条
        for (int i = 0; i < childCount ; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (LayoutParams) child.getLayoutParams();
            int top = child.getBottom() + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child));
            int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top , right, bottom);
            mDivider.draw(c);
        }
    }

本来是想把注解写在代码里的,发现写完之后代码很乱很多,显得很困难的样子,为了不造成视觉困难就在下面解析代码吧,先看最后两行很好理解,

mDivider.setBounds(left, top , right, bottom);
mDivider.draw(c);

setBounds(int left, int top, int right, int bottom),
这个四参数指的是drawable将在被绘制在canvas的哪个矩形区域内,这个区域就是下图中黑色矩形的区域!核心代码就是这四个int值left、right、top、bottom。


image.png
int left = parent.getPaddingLeft();

很好理解,父组件RecyclerView左内边距,理解为左侧开始的地方。

int right = parent.getWidth() - parent.getPaddingRight();

宽度减去右内边距,理解为右侧结束的地方。

int top = child.getBottom() + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child));
image.png

需要竖着滑动,垂直方向的属性动画的平移,要加上ViewCompat.getTranslationY(child),Math.round()四舍五入。
上图中有三条红线,第一条就是child.getBottom的位置,第二条跟第一条之间的距离就是params.bottomMargin(图中稍稍有误),不要被三条红线误导了,黑色的矩形框才是我们的间隔线!!!所以bottom就很好写了,它就是top加间隔线的高度啦!就是图中第三条线。此时间隔线绘制结束,尽管去调用吧~如果理解了画横线的过程,那么画竖线就很简单了,其实就是把上图逆时针旋转90度,然后做分析。

(二)item横向排列分析

这次我们反过来做,先不看代码,先来看图分析left、right、top、bottom这几个值该怎么写。


image.png

可以想到top和bottom很好写:
1、int top = parent.getPaddingTop();
2、int bottom = parent.getHeight- parent.getPaddingBottom();

来看图中第一条红线的位置:child.getRight
第一条红线和第二条红线的距离:params.rightMargin,不难写出:
3、int left = child.getRight() + params.rightMargin + Math.round(ViewCompat.getTranslationX(child));
左右滑动需要加上水平方向动画的平移ViewCompat.getTranslationX(child)

right就是left加上mDivider的宽度~
4、int right = left + mDivider.getIntrinsicHeight();
查看drawHorizontal(Canvas c, RecyclerView parent) 方法做一下对比:

private void drawHorizontal(Canvas c, RecyclerView parent) {
         //这里的水平是item水平,画垂直线条
        int top = parent.getPaddingTop();
        int bottom = parent.getHeight() - parent.getPaddingBottom();
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount ; i++) {
            View child = parent.getChildAt(i);
            
            RecyclerView.LayoutParams params = (LayoutParams) child.getLayoutParams();
            int left = child.getRight() + params.rightMargin + Math.round(ViewCompat.getTranslationX(child));
            int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top , right, bottom);
            mDivider.draw(c);
        }
    }

是不是完全一样?

(三)网格间隔线分析

网格间隔线虽然有点复杂,但如果慢慢分析,还是可以写出来的,要注意细节。其实就是横线和竖线都画,对于某一个item来说就是右边添加偏移量和下边添加偏移量。

@Override
    @Deprecated
    public void getItemOffsets(Rect outRect, int itemPosition,
            RecyclerView parent) {
        int right = mDivider.getIntrinsicWidth();
        int bottom = mDivider.getIntrinsicHeight();
        outRect.set(0, 0, right, bottom);
    }

onDraw(Canvas c, RecyclerView parent, State state)方法里就不用做判断if(mOrientation == LinearLayoutManager.VERTICAL){},因为横竖线都要画的。

drawVertical(c,parent);// 绘制水平间隔线
drawHorizontal(c,parent);//绘制垂直间隔线

先上图,一定要画图。


image.png
private void drawVertical(Canvas c, RecyclerView parent) {
        //绘制垂直间隔线(垂直的矩形)
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (LayoutParams) child.getLayoutParams();
            int left = child.getRight() + params.rightMargin;
            int right = left + mDivider.getIntrinsicWidth();
            int top = child.getTop() - params.topMargin;
            int bottom = child.getBottom() + params.bottomMargin;
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

获得的图形是这样的:


image.png

绘制横线间隔线:

private void drawHorizontal(Canvas c, RecyclerView parent) {
        // 绘制水平间隔线
        int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = parent.getChildAt(i);
            RecyclerView.LayoutParams params = (LayoutParams) child.getLayoutParams();
            int left = child.getLeft() - params.leftMargin;
            int right = child.getRight()+ params.rightMargin;
            int top = child.getBottom() + params.bottomMargin;
            int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }
image.png

最终绘制结果是这样的:


image.png

哎呦?线条交叉的地方为什么没有绘制呢?其实如果分割线非常的细这样写是没有问题的,已经实现了网格的分割。如果非要写的完美把交叉区域也写上话,也很简单——只需要在drawVertical或者drawHorizontal的其中一个加入mDivider的宽或高即可(如果两个里面都加的话就会出现重叠绘制交叉处的bug);
那么我们来修改drawVertical方法里的right就行了:
int right = child.getRight()+ params.rightMargin+mDivider.getIntrinsicWidth();

当然,在实际开发的过程中会涉及到分别判断出首行,末行,首列,末列,根据是否绘制边界线(drawBorderLine)来进行处理偏移,这里就不多说了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容