RecyclerView系列二:RecyclerView.ItemDecoration的详解使用

前言

在很早很早以前(long long ago),ListView鼎盛的时代有一个属性叫做divider。但是在RecycleView上面就是找不到他,那怎么办呢???直到后来有一天发现他变身了,变成了ItemDecoration。实在是扯不下去了,直接开始吧!
这篇博客酝酿了好长时间,希望不会让各位看官失望。

任务

了解ItemDecoration的原理,自己可以添加分割线,每个 ItemView 上叠加一个角标,自定义 RecyclerView 中的头部或者是粘性头部。

分析和实战

1.具体的使用

RecyclerView的简单使用可以参考前一篇RecyclerView系列一:简单使用
我们在页面中放两个RecyclerView,上面一个下面一个,用来对比。如下图所示:

现在开始处理:写TextItemDecoration类让他继承RecyclerView.ItemDecoration,主要的代码如下所示:

    class TextItemDecoration extends RecyclerView.ItemDecoration {

        //设置ItemView的内嵌偏移长度(inset)
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
        }

        // 在子视图上设置绘制范围,并绘制内容
        // 绘制图层在ItemView以下,所以如果绘制区域与ItemView区域相重叠,会被遮挡
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
        }

        //同样是绘制内容,但与onDraw()的区别是:绘制在图层的最上层
        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
        }
    }

2.分析方法getItemOffsets()

分析这里的时候,我们先来盗个图,如下:


getItemOffsets()总的概括

我们所有的分析这个图就可以概括了。现在我们开始分析这个方法,在Android Studio中看super.getItemOffsets(outRect, view, parent, state);这个方法,最终我们在RecyclerView看到outRect.set(0, 0, 0, 0);这一行代码。
那么我们就拿outRect开刀。TextItemDecoration中代码如下:

        //设置ItemView的内嵌偏移长度(inset)
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            //只是添加下面这一行代码
            outRect.set(50, 50, 50, 50);
        }

把这个·ItemDecoration·放在下面的RecyclerView上面,代码如下:

 recyclerView2.addItemDecoration(new TextItemDecoration());

运行效果,如下图所示:


示意图

再来一张单独的图片,如下所示:


sc3.png
  • 如上图所示,RecyclerView 中的 ItemView 外面会包裹着一个矩形(outRect)。
  • 内嵌偏移长度:该矩形(outRect)与 ItemView的间隔.
  • 默认的情况下,top、left、right、bottom都是0,所以矩形和ItemView就重叠了。
2.1源码分析(直接上源码):

下面的代码都是在RecyclerView中,可以在RecyclerView里面找到源码:

    //测量所有的子view的宽和高,得到这个子view的Rect。然后就能得到这一块真正的宽和高。
    //注意还有padding的值。
    public void measureChild(View child, int widthUsed, int heightUsed) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 
        final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
        widthUsed += insets.left + insets.right;
        heightUsed += insets.top + insets.bottom;
        final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
                getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
                canScrollHorizontally());
        final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
                canScrollVertically());
        if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
            child.measure(widthSpec, heightSpec);
        }
    }

    //得到每个子view相应的Rect,
    //mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
    //上面的代码就是设置相应的四个值,就和我们的TextItemDecoration类里面的代码对应起来了。
    Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            return lp.mDecorInsets;
        }

        if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
            // changed/invalid items should not be updated until they are rebound.
            return lp.mDecorInsets;
        }
        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
            mTempRect.set(0, 0, 0, 0);
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            //下面就是详细的赋值
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }


3.onDraw()

我们先来看看我们自定义的TextItemDecoration类里面的onDraw()代码,如下所示:

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

很明显上面传递了一个Canvas 参数对象,所以它拥有了绘制的能力。

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

第二点会出现如下的情况:


示意图

出现上面的问题解决方案是getItemOffsets()与onDraw()一块使用。说的我一愣一愣的,最主要的问题onDrow()的时候,是怎样得到相应的点。


sc5.jpg

英雄莫怕,请看上面的代码onDraw(Canvas c, RecyclerView parent, RecyclerView.State state)

看第二个参数:RecyclerView parent 这就是我们的突破点。(这里有一个疑问点???我们第4节处理。)
  int childCount = parent.getChildCount();
  View child = parent.getChildAt(i);
  上面的代码就能解决我们的问题。

上代码实战:要实现的效果如下:


sc6.png

代码实现如下:

    class TextItemDecoration extends RecyclerView.ItemDecoration {
        private Paint mPaint;

        public TextItemDecoration() {
            this.mPaint = new Paint();
            mPaint.setColor(Color.YELLOW);
            // 画笔颜色设置为黄色
        }

        //设置ItemView的内嵌偏移长度(inset)
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            outRect.set(50, 50, 50, 50);
        }

        // 在子视图上设置绘制范围,并绘制内容
        // 绘制图层在ItemView以下,所以如果绘制区域与ItemView区域相重叠,会被遮挡
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
            // 获取RecyclerView的Child view的个数
            int childCount = parent.getChildCount();
            // 遍历每个Item,分别获取它们的位置信息,然后再绘制对应的分割线
            for (int i = 0; i < childCount; i++) {
                // 获取每个Item的位置
                final View child = parent.getChildAt(i);
                // 设置矩形(分割线)的宽度为10px
                final int mDivider = 10;
                // 矩形左上顶点 = (ItemView的左边界,ItemView的下边界)
                final int left = child.getLeft();
                final int top = child.getBottom();
                // 矩形右下顶点 = (ItemView的右边界,矩形的下边界)
                final int right = child.getRight();
                final int bottom = top + mDivider;
                // 通过Canvas绘制矩形(分割线)
                c.drawRect(left, top, right, bottom, mPaint);
            }
        }


        //同样是绘制内容,但与onDraw()的区别是:绘制在图层的最上层
        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
        }
    }

4.onDrawOver()

我们先来看看我们自定义的TextItemDecoration类里面的onDrawOver()代码,如下所示:

        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
        }

很明显onDrawOver里面的参数和onDraw里面的参数一模一样,那还要onDrawOver有什么用呢???。请看下图:


示意图

最主要的就是紫色区域,onDrawOver的使用方法和onDraw类似。现在我们在每一个条目的右上角加一个图标。效果如下:


sc8.png

代码实现如下:

    class TextItemDecoration extends RecyclerView.ItemDecoration {
        private Paint mPaint;
        private Bitmap bitmap;

        public TextItemDecoration() {
            this.mPaint = new Paint();
            mPaint.setColor(Color.YELLOW);
            // 画笔颜色设置为黄色
            bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.email);
        }

        //设置ItemView的内嵌偏移长度(inset)
        @Override
        public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            outRect.set(50, 50, 50, 50);
        }

        // 在子视图上设置绘制范围,并绘制内容
        // 绘制图层在ItemView以下,所以如果绘制区域与ItemView区域相重叠,会被遮挡
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
            // 获取RecyclerView的Child view的个数
            int childCount = parent.getChildCount();
            // 遍历每个Item,分别获取它们的位置信息,然后再绘制对应的分割线
            for (int i = 0; i < childCount; i++) {
                // 获取每个Item的位置
                final View child = parent.getChildAt(i);
                // 设置矩形(分割线)的宽度为10px
                final int mDivider = 10;
                // 矩形左上顶点 = (ItemView的左边界,ItemView的下边界)
                final int left = child.getLeft();
                final int top = child.getBottom();
                // 矩形右下顶点 = (ItemView的右边界,矩形的下边界)
                final int right = child.getRight();
                final int bottom = top + mDivider;
                // 通过Canvas绘制矩形(分割线)
                c.drawRect(left, top, right, bottom, mPaint);
            }
        }


        //同样是绘制内容,但与onDraw()的区别是:绘制在图层的最上层
        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
            int childCount = parent.getChildCount();
            for (int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);
                final int left = child.getRight() - bitmap.getWidth();
                final int top = child.getTop();
                c.drawBitmap(bitmap, left, top, mPaint);
            }
        }
    }

5.自定义 RecyclerView 中的头部或者是粘性头部

具体的思路如下图所示:


示意图

我们的操作在OutRect里面处理。下面我们用假数据处理,页面中只保留一个RecyclerView。我们只分析TextItemDecoration里面的代码。代码如下:

    class TextItemDecoration extends RecyclerView.ItemDecoration {
        private Paint mPaint;

        public TextItemDecoration() {
            this.mPaint = new Paint();
            // 画笔颜色设置为黄色
            mPaint.setColor(Color.YELLOW);
        }

        //设置ItemView的内嵌偏移长度(inset)
        @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 % 5 == 0) {
                outRect.set(0, 50, 0, 0);
            }
        }

        // 在子视图上设置绘制范围,并绘制内容
        // 绘制图层在ItemView以下,所以如果绘制区域与ItemView区域相重叠,会被遮挡
        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
            // 获取RecyclerView的Child view的个数
            int childCount = parent.getChildCount();
            // 遍历每个Item,分别获取它们的位置信息,然后再绘制对应的分割线
            for (int i = 0; i < childCount; i++) {
                if (i % 5 == 0) {
                    View child = parent.getChildAt(i);
                    int left = 0;
                    int top = child.getTop() - 50;
                    int right = child.getRight();
                    int bottom = child.getTop();
                    c.drawRect(left, top, right, bottom, mPaint);
                }
            }
        }


        //同样是绘制内容,但与onDraw()的区别是:绘制在图层的最上层
        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);
        }
    }

效果图如下:


示意图

黄条才是我要的真爱,这怎么乱套了??问题就出在如下的代码里面

        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
            // 获取RecyclerView的Child view的个数
            int childCount = parent.getChildCount();
            // 遍历每个Item,分别获取它们的位置信息,然后再绘制对应的分割线
            for (int i = 0; i < childCount; i++) {
                if (i % 5 == 0) {
                    View child = parent.getChildAt(i);
                    int left = 0;
                    int top = child.getTop() - 50;
                    int right = child.getRight();
                    int bottom = child.getTop();
                    c.drawRect(left, top, right, bottom, mPaint);
                }
            }
        }

上面就是有问题的代码:

int childCount = parent.getChildCount();
得到的是这一屏幕有多少个孩子。不是说得到的总的孩子的数目。

我们修改一下onDraw里面的代码,如下:

        @Override
        public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDraw(c, parent, state);
            // 获取RecyclerView的Child view的个数
            int childCount = parent.getChildCount();
            // 遍历每个Item,分别获取它们的位置信息,然后再绘制对应的分割线
            for (int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);
                int index = parent.getChildAdapterPosition(child);
                if (index % 5 == 0) {
                    int left = 0;
                    int top = child.getTop() - 50;
                    int right = child.getRight();
                    int bottom = child.getTop();
                    c.drawRect(left, top, right, bottom, mPaint);
                }
            }
        }

效果图如下:


示意图

这距离我们想要的效果越来跃进了。能不能在黄条在最上面的时候停留呢??还有就是推上去??不出所料所有的操作都是在这里面了。
下面我们直接上代码,思考留给我亲爱的读者:

        @Override
        public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
            super.onDrawOver(c, parent, state);

            // 获取RecyclerView的Child view的个数
            int childCount = parent.getChildCount();
            // 遍历每个Item,分别获取它们的位置信息,然后再绘制对应的分割线
            for (int i = 0; i < childCount; i++) {
                View child = parent.getChildAt(i);
                int index = parent.getChildAdapterPosition(child);
                if (index % 5 == 0) {
                    int item = (index) / 5;
                    if (i < 5) {
                        if (i == 1 && child.getTop() < 100) {
                            int left = 0;
                            int top = child.getTop() - 100;
                            int right = child.getRight();
                            int bottom = child.getTop() - 50;
                            c.drawRect(left, top, right, bottom, mPaint);
                            c.drawText("这是条目" + item + "视图i是+" + i + "顶部的高低" + child.getTop() + "index++" + index, left, top + 50, textPaint);
                        } else {
                            int left = 0;
                            int top = 0;
                            int right = child.getRight();
                            int bottom = 50;
                            c.drawRect(left, top, right, bottom, mPaint);
                            if (i == 0) {
                                c.drawText("这是条目" + (item + 1) + "视图i是+" + i + "顶部的高低" + child.getTop() + "index++" + index, left, top + 50, textPaint);
                            } else {
                                c.drawText("这是条目" + (item) + "视图i是+" + i + "顶部的高低" + child.getTop() + "index++" + index, left, top + 50, textPaint);
                            }
                        }

                    }
                    if (i != 0) {
                        int left = 0;
                        int top = child.getTop() - 50;
                        int right = child.getRight();
                        int bottom = child.getTop();
                        c.drawRect(left, top, right, bottom, mPaint);
                        c.drawText("这是条目" + item + "视图i是+" + i + "顶部的高低" + child.getTop() + "index++" + index, left, top + 50, textPaint);
                    }
                }
            }
        }

我们的效果图如下:


示意图

我把要打印的都给小伙伴们打印出来了。具体的优化看自己的需求优化就可以了。

总结

本篇文章介绍了RecyclerView.ItemDecoration的使用,还有它的原理。其实还是挺简单的。我相信简单的自定义小伙伴应该都会了。

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

推荐阅读更多精彩内容