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

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

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

前言

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

目录


前置知识

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


1. 简介

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


2. 使用示例

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

  • 黑色分割线 drawable:


    
    

  • 白色分割线 drawable:


    
    

  • 调用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 联系人分类


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

你可能感兴趣的:(Android | 一篇文章带你玩转 RecyclerView.ItemDecoration)