Android | 一篇文章带你玩转 RecyclerView.ItemDecoration

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。本文 「Android 路线」| 导读 —— 从零到无穷大 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

  • 在 Android 开发中,RecyclerView 十分常用,结合 ItemDecoration 还能实现很多意向不到的效果;
  • 这篇文章将总结 ItemDecoration 用法、源码解析和示例,希望能帮上忙。

目录


前置知识

这篇文章的内容会涉及以下前置 / 相关知识,贴心的我都帮你准备好了,请享用~


1. 简介

ItemDecoration 是 RecyclerView 的一个抽象静态内部类,负责装饰 Item 视图,例如添加间距、高亮或者分组边界等。


2. 使用示例

首先,我们使用官方提供的 DividerItemDecoration 来演示 ItemDecoration 用法。在这里,我们为 RecyclerView 设置了两条分割线,具体代码如下:

  • 黑色分割线 drawable:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <size android:height="5dp" />
    <solid android:color="#FFFFFF" />
</shape>
  • 白色分割线 drawable:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <size android:height="5dp" />
    <solid android:color="#000000" />
</shape>
  • 调用RecyclerView#addItemDecoration()添加分割线:
val rv: RecyclerView = findViewById(R.id.rv);
rv.layoutManager = LinearLayoutManager(this)
1、添加第一个ItemDecoration
rv.addItemDecoration(DividerItemDecoration(this, VERTICAL).apply {
    setDrawable(ContextCompat.getDrawable(this@MainActivity, R.drawable.shape_divider_1)!!)
})

2、添加第二个ItemDecoration
rv.addItemDecoration(DividerItemDecoration(this, VERTICAL).apply {
    setDrawable(ContextCompat.getDrawable(this@MainActivity, R.drawable.shape_divider_2)!!)
})
rv.adapter = TestAdapter()

效果如下:

小结:

  • 1、使用 DividerItemDecoration 时调用 setDrawable() 设置分割线;
  • 2、调用 RecyclerView#addItemDecoration() 添加 ItemDecoration ;
  • 3、可以添加多个 ItemDecoration ,按添加顺序生效。

3. 自定义 ItemDecoration

这一节我们来讨论如何自定义 ItemDecoration,我们关注 ItemDecoration 的三个抽象方法,具体描述如下:

public abstract static class ItemDecoration {

    1、设置 Item 视图的边距
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
    }
    
    2、在 ItemView 的下层图层绘制,绘制内容会被 ItemView 遮挡
    public void onDraw(Canvas c, RecyclerView parent, State state) {
    }

    3、在 ItemView 的上层图层绘制,绘制内容会遮挡 ItemView
    public void onDrawOver(Canvas c, RecyclerView parent, State state) {
    }
}
方法 描述
getItemOffsets() 设置 Item 视图边距(多个 ItemDecoration 累加)
onDraw() 绘制背景图层(多个 ItemDecoration 叠加)
onDrawOver() 绘制浮层(多个 ItemDecoration 叠加)

3.1 设置 Item 视图边距

RecyclerView 的每一项 ItemView 都绘制在一个 矩形区域 内,通过修改getItemOffsets(Rect outRect...)第一个参数 outRect 的top、left、right、bottom属性值,可以控制 ItemView 在相对于矩形区域的间距,如以下示意图所示:

getItemOffsets() 示意图

我们以前面提到的 DividerItemDecoration 作为例子演示如何实现 getItemOffsets(),这部分源码比较好理解:纵向布局时,将图片高度作为 bottom 边距,横向布局时,它将图片宽度作为 right 边距:

DividerItemDecoration.java

private Drawable mDivider;

public void setDrawable(Drawable drawable) {
    if (drawable == null) {
        throw new IllegalArgumentException("Drawable cannot be null.");
    }
    mDivider = drawable;
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
    if (mDivider == null) {
        outRect.set(0, 0, 0, 0);
        return;
    }
    if (mOrientation == VERTICAL) {
        纵向布局时,将图片高度作为 bottom 边距
        outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
    } else {
        横向布局时,将图片宽度作为 right 边距
        outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
    }
}

我们简单分析下 RecyclerView 源码:既然是设置间距,大概率与测量流程有关。在 RecyclerView 的内部类 LayoutManager 中可以找到相关源码:

RecyclerView.LayoutManager 内部类

1、测量子 View
public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
    RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)child.getLayoutParams();

    1.1 计算 ItemDecoration 占用的边距
    Rect insets = this.mRecyclerView.getItemDecorInsetsForChild(child);

    1.2 将 ItemDecoration 占用的边距计算到 RecyclerView 已经占用的宽高
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;

    1.3 计算子 View 的 MeasureSpec
    int widthSpec = getChildMeasureSpec(this.getWidth(), this.getWidthMode(), this.getPaddingLeft() + this.getPaddingRight() + widthUsed, lp.width, this.canScrollHorizontally());
    int heightSpec = getChildMeasureSpec(this.getHeight(), this.getHeightMode(), this.getPaddingTop() + this.getPaddingBottom() + heightUsed, lp.height, this.canScrollVertically());
    if (this.shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
        1.4 递归测量
        child.measure(widthSpec, heightSpec);
    }
}

-> 1.1 计算 ItemDecoration 占用的边距
Rect getItemDecorInsetsForChild(View child) {
    RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)child.getLayoutParams();
    if (!lp.mInsetsDirty) {
        return lp.mDecorInsets;
    } else if (this.mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
        return lp.mDecorInsets;
    } else {
        Rect insets = lp.mDecorInsets;

        1.1.1 初始化为 0
        insets.set(0, 0, 0, 0);

        遍历所有 ItemDecoration
        int decorCount = this.mItemDecorations.size();
        for(int i = 0; i < decorCount; ++i) {
            将outRech的上、下、左、右置零
            this.mTempRect.set(0, 0, 0, 0);
            1.1.2 依次调用每个ItemDecoration#getItemOffsets()为outRect赋值
            ((RecyclerView.ItemDecoration)this.mItemDecorations.get(i)).getItemOffsets(this.mTempRect, child, this, this.mState);
            1.1.3 累加每个 ItemDecoration 设置的上、下、左、右边距
            insets.left += this.mTempRect.left;
            insets.top += this.mTempRect.top;
            insets.right += this.mTempRect.right;
            insets.bottom += this.mTempRect.bottom;
        }

        lp.mInsetsDirty = false;
        return insets;
    }
}

以上代码已经非常简化了,主要关注以下逻辑:

  • 1.1 计算 ItemDecoration 占用的边距
  • 1.2 将 ItemDecoration 占用的边距计算到 RecyclerView 已经占用的宽高
  • 1.3 计算子 View 的 MeasureSpec
  • 1.4 递归测量

3.2 绘制背景图层

在 ItemView 的下层有一个背景图层,通过实现ItemDecoration#onDraw()可以在背景图层绘制。需要注意的是:如果绘制的内容在 ItemView 的范围内会被遮挡,如以下示意图所示:

onDraw() 示意图

继续以 DividerItemDecoration 作为例子,这里需要注意的是:getItemOffsets()是处理每个ItemView的,而onDraw()是针对整个RecyclerView进行绘制:

DividerItemDecoration.java

private final Rect mBounds = new Rect();

@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
    if (parent.getLayoutManager() == null || mDivider == null) {
        return;
    }
    if (mOrientation == VERTICAL) {
        纵向
        drawVertical(c, parent);
    } else {
        横向
        drawHorizontal(c, parent);
    }
}

private void drawVertical(Canvas canvas, RecyclerView parent) {
    canvas.save();
    final int left;
    final int right;
    //noinspection AndroidLintNewApi - NewApi lint fails to handle overrides.
    if (parent.getClipToPadding()) {
        left = parent.getPaddingLeft();
        right = parent.getWidth() - parent.getPaddingRight();
        canvas.clipRect(left, parent.getPaddingTop(), right, parent.getHeight() - parent.getPaddingBottom());
    } else {
        left = 0;
        right = parent.getWidth();
    }
    // RecyclerView的ChildView的个数,ChildView是可见的区域
    final int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        1、处理每个可见的ChildView
        final View child = parent.getChildAt(i);

        2、获取 Item 的矩形区域
        parent.getDecoratedBoundsWithMargins(child, mBounds);

        3、bottom 是矩形区域 bottom - ItemView 的translationY
        final int bottom = mBounds.bottom + Math.round(child.getTranslationY());

        4、top 是 bottom - 分割线高度
        final int top = bottom - mDivider.getIntrinsicHeight();

        5、设置分割线范围
        mDivider.setBounds(left, top, right, bottom);

        6、绘制分割线
        mDivider.draw(canvas);
    }
    canvas.restore();
}
横向省略...

我们简单分析下 RecyclerView 源码:既然是设置间距,大概率与绘制流程有关。我们在 RecyclerView#onDraw() 中可以找到相关源码。可以看到,RecyclerView#onDraw(...)会调用每个ItemDecoration#onDraw()进行绘制:

RecyclerView.java

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

    final int count = mItemDecorations.size();
    调用每个 ItemDecoration 的 onDraw(...)
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDraw(c, this, mState);
    }
}

3.3 绘制浮层

在 ItemView 的上层有一个浮层,通过实现ItemDecoration#onDrawOver()可以在浮层绘制。需要注意的是:如果绘制的内容在 ItemView 的范围以上会遮挡 ItemView ,如以下示意图所示:

onDrawOver() 示意图

onDrawOver() 与 onDraw() 类似,区别在于绘制的图层不同,实战中用的比较少,此处不过多展开。


4. 示例讲解

Editing...

4.1 万能分割线

4.2 快递时间轴

4.3 联系人分类


创作不易,你的「三连」是丑丑最大的动力,我们下次见!

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

推荐阅读更多精彩内容