ItemDecoration深入解析与实战(一)——源码分析

一 概述

ItemDecorationRecyclerView 中的一个抽象静态内部类。

An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set. This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.

这是官网对 ItemDecoration 的描述,简单来说就是可以为 RecyclerView的每一个 ItemView 进行一些特殊的绘制或者特殊的布局。从而我们可以为 RecyclerView 添加一些实用好玩的效果,比如分割线,边框,饰品,粘性头部等。

此文会分析ItemDecoration 的使用及原理,然后进行一些Demo的实现,包括分割线,网格布局的边框,以及粘性头部。

二 方法

1. 方法概述

ItemDecoration中的实际方法只有6个,其中有3个是重载方法,都被标注为 @deprecated,即弃用了,这些方法如下

修饰符 返回值类型 方法名 标注
void public onDraw(Canvas c, RecyclerView parent, State state)
void public onDraw(Canvas c, RecyclerView parent) @deprecated
void pulbic onDrawOver(Canvas c, RecyclerView parent, State state)
void public onDrawOver(Canvas c, RecyclerView parent) @deprecated
void public getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
void public getItemOffsets(Rect outRect, View view, RecyclerView parent) @deprecated

2. getItemOffsets

除了 getItemOffsets 方法,其他方法的默认实现都为空,而 getItemOffsets 的默认实现方法也很简单:

        @Deprecated
        public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) {
            outRect.set(0, 0, 0, 0);
        }

两个getItemOffsets方法最终都是调用了上面实现,就一行代码,如果我们自定义过 ItemDecoration 的话,就会知道,我们可以为 outRect 设置四边的大小来为 itemView 设置一个偏移量.
这个偏移量有点类似于 View 的margin,看下面的图1:

RecyclerView&Child.png

图片很清晰的表示了 ItemView 的结构(该图不是特别精确,后面会说到),这是只有一个 Child 的情况,我们从外往里看:

  1. 最外的边界即 RecyclerView 的边界
  2. 红色部分是 RecyclerView 的 Padding,这个我们应该能理解
  3. 橙色部分是我们为 ItemView 设置的 Margin,这个相信写过布局都能理解
  4. 蓝色部分就是我们在 getItemOffsets方法中给 outRect对象设置的值
  5. 最后的的黄色部分就是我们的 ItemView 了

总体就是说,getItemOffsets中设置的值就相当于 margin 的一个存在。"图说无凭",接下来就结合源码讲解一下这个图的"依据"。首先看一下 getItemOffsets在哪里被调用了:

 Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        ...
        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;
 }

RecyclerView源码中,这是 getItemOffsets唯一被调用的地方,代码也很简单,就是将 RecyclerView中所有的(即通过addDecoration()方法添加的) ItemDecoration 遍历一遍,然后将我们设在 getItemOffsets 中设置的四个方向的值分别累加并存储在insets这个Rect当中。那么这个 insets又在哪里被调用了呢,顺着方法继续跟踪下去:

public void measureChildWithMargins(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()
                    + lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
            canScrollHorizontally());
    final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
           getPaddingTop() + getPaddingBottom()
                    + lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
            canScrollVertically());
    if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
        child.measure(widthSpec, heightSpec);
    }
}

我们看到,在 measureChildWithMargins方法中,将刚刚得到的 insets 的值与 Recyclerview 的 Padding 以及当前 ItemView 的 Margin 相加,然后作为 getChildMeasureSpec的第三个参数传进去:

public static int getChildMeasureSpec(int parentSize, int parentMode, int padding,
    int childDimension, boolean canScroll) {
    int size = Math.max(0, parentSize - padding);
    int resultSize = 0;
    int resultMode = 0;
    //...省略部分代码
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

getChildMeasureSpec方法的第三个参数标注为 padding ,在方法体这个 padding 的作用就是计算出 size 这个值,这个 size是就是后面测量中 Child(ItemView) 能达到的最大值。

也就是说我们设置的 ItemView 的 Margin 以及ItemDecoration.getItemOffsets中设置的值到头来也是跟 Parent 的 Padding 一起来计算 ItemView 的可用空间,也就印证了上面的图片,在上面说了该图不精确就是因为

  • parent-padding
  • layout_margin
  • insets(all outRect)

他们是一体的,并没有划分成一段一段这样,图中的outRect也应该改为insets,但是图中的形式可以更方便我们理解。

3. onDraw

    public void onDraw(Canvas c, RecyclerView parent, State state) {
        onDraw(c, parent);
    }

    /**
     * @deprecated Override {@link #onDraw(Canvas, RecyclerView, RecyclerView.State)}
     */
    @Deprecated
    public void onDraw(Canvas c, RecyclerView parent) {
    }

onDraw方法有两个重载,一个被标注为 @deprecated,即弃用了,我们知道,如果重写了 onDraw,就可以在我们上面的 getItemOffsets中设置的范围内绘制,知其然还要知其所以然,我们看下源码里面是怎样实现的
#RecyclerView.java

    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }

ReyclerViewonDraw方法中,将会把所有 DecorationonDraw方法调用一遍,而且会把Recyclerview#onDraw(Canvas)方法中的Canvas传递给Decoration#onDraw,也就是说我们在Decoration中拿到了整个 RecyclerView 的 Canvas,那么我们基本就可以随意绘制了,但是我们使用中会发现,我们绘制的区域如果在 ItemView 的范围内就会被盖住,这是为什么呢?

由于View的绘制是先执行 draw(Canvas)再到onDraw(Canvas)的,我们复习一波自定义View的知识,看下View的绘制流程:
#View.java

    public void draw(Canvas canvas) {
      
        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)

        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);   //注释1

            // Step 4, draw the children
            dispatchDraw(canvas);        //注释2
            ...
            // we're done...
            return;
        }
    }

我们直接看注释1与注释2那段,可以看到,View的绘制是先绘制自身(onDraw调用),然后再绘制child,所以我们在 Decoration#onDraw中绘制的界面会被 ItemView 遮挡也是理所当然了。

所以我们在绘制中就要计算好绘制的范围,使绘制范围在上面彩图中蓝色区域内,即getItemOffsets设置的范围内,避免没有显示或者过分绘制的情况。

4.onDrawOver

    public void onDrawOver(Canvas c, RecyclerView parent, State state) {
        onDrawOver(c, parent);
    }

    /**
     * @deprecated
     * Override {@link #onDrawOver(Canvas, RecyclerView, RecyclerView.State)}
     */
    @Deprecated
    public void onDrawOver(Canvas c, RecyclerView parent) {
    }

onDrawOveronDraw非常类似,也是两个重载,一个被弃用了,看名称我们就基本能知道这个方法的用途,它是用于补充 onDraw 的一个方法,由于onDraw会被 ItemView 覆盖,所以我们想要绘制一些漂浮在RecyclerView顶层的装饰就无法实现,所以就有了这个方法,他是在 ItemView 绘制完毕后才会被调用的,看下源码的实现:
#RecyclerView.java

@Override
    public void draw(Canvas c) {
        super.draw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
        ...
    }

super.draw(c) 就是我们在上面分析的View#draw(Canvas)方法,会调用一系列的绘制流程,包括onDraw(ItemDecoration的onDraw)以及dispatchDraw(ItemView的绘制),走完这些流程后才会调用Decoration#onDrawOver方法.

到此,我们就可以得出 onDraw>dispatchDraw(ItemView的绘制)>onDrawOver的执行流程。

5. 总结

  1. getItemOffsets用于提供一些空间(类似Margin)给 onDraw绘制
  2. onDraw方法绘制的内容如果在 ItemView 的区域则可能被覆盖(没效果)
  3. onDraw>dispatchDraw(ItemView的绘制)>onDrawOver从左到右执行

三 实战

实战将会从易到难进行几个小的Demo练习。
由于这篇文章内容已经比较充实了,就把实战部分放到下篇讲解。

感谢你的阅读,由于水平有限,如有错误恳请提醒。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容