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

前言

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

目录


1. 简介

  • 定义
    列表Item的修饰器,是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)
// 添加第一个ItemDecoration
rv.addItemDecoration(DividerItemDecoration(this, VERTICAL).apply {
    setDrawable(ContextCompat.getDrawable(this@MainActivity, R.drawable.shape_divider_1)!!)
})
// 添加第二个ItemDecoration
rv.addItemDecoration(DividerItemDecoration(this, VERTICAL).apply {
    setDrawable(ContextCompat.getDrawable(this@MainActivity, R.drawable.shape_divider_2)!!)
})
rv.adapter = TestAdapter()
  • 效果如下:
效果图
  • 小结
      1. 使用DividerItemDecoration时调用setDrawable()设置分割线
      1. 调用RecyclerView#addItemDecoration()添加ItemDecoration
      1. 可以添加多个ItemDecoration,按添加顺序累加

3. API

现在我们关注ItemDecoration提供的三个方法,具体描述如下:

public abstract static class ItemDecoration {

    // 1. 设置ItemView的边距
    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) {
    }
}

3.1 getItemOffsets(Rect outRect,...)

  • 作用:
    RecyclerView的每一项ItemView都绘制在一个矩形区域内;通过修改getItemOffsets(Rect outRect...)第一个参数outRecttop、left、right、bottom属性值,可以控制ItemView在相对于矩形区域的间距,如以下示意图所示:
getItemOffsets() 示意图
  • 源码分析:
    间距与测量流程有关,因此我们看看RecyclerView#measureChild(...),它是在LayoutManager调用的,如下所示:
// RecyclerView

public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
    RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)child.getLayoutParams();
    // 每个ItemDecoration设置的上、下、左、右边距累加起来
    Rect insets = this.mRecyclerView.getItemDecorInsetsForChild(child);
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;
    // 将累加的间距算到ItemView的padding里进行测量
    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)) {
        child.measure(widthSpec, heightSpec);
    }
}

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;
        // 初始化为0
        insets.set(0, 0, 0, 0);
        int decorCount = this.mItemDecorations.size();

        for(int i = 0; i < decorCount; ++i) {
            // 将outRech的上、下、左、右置零
            this.mTempRect.set(0, 0, 0, 0);
            // 依次调用每个ItemDecoration#getItemOffsets()为outRect赋值
            ((RecyclerView.ItemDecoration)this.mItemDecorations.get(i)).getItemOffsets(this.mTempRect, child, this, this.mState);
            // 每个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;
    }
}
  • 举例:
    以前面提到的DividerItemDecoration作为例子,在getItemOffsets()中,纵向布局时,它将图片高度作为bottom边距,横向布局时,它将图片宽度作为right边距:
// DividerItemDecoration
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);
    }
}
  • 小结:
      1. 修改getItemOffsets(Rect outRect...)第一个参数outRecttop、left、right、bottom属性值,可以控制ItemView在相对于矩形区域的间距;
      1. RecyclerView在测量时,会将添加的所有ItemDecorationtop、left、right、bottom边距累加起来,影响ItemViewpadding

3.2 onDraw(Canvas c,...)

  • 作用:
    ItemView的下层图层绘制,因此如果ItemDecoration#onDraw()绘制的内容在ItemView的范围内,将被ItemView遮挡,如以下示意图所示:
onDraw() 示意图
  • 源码分析:
    draw与绘制流程有关,因此我们看看RecyclerView#onDraw(...),如下所示:
// RecyclerView
@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);
    }
}

可以看到,RecyclerView#onDraw(...)会调用每个ItemDecoration#onDraw()进行绘制;与getItemOffsets()不同的是,getItemOffsets()是处理每个ItemView的,而onDraw()是针对整个RecyclerView进行绘制

  • 举例:
    以前面提到的DividerItemDecoration作为例子,
// DividerItemDecoration

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++) {
        // 处理每个可见的ChildView
        final View child = parent.getChildAt(i);
        // 获取Item的矩形区域
        parent.getDecoratedBoundsWithMargins(child, mBounds);
        // bottom是矩形区域bottom减去ItemView的translationY
        final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
        // top是bottom减分割线高度
        final int top = bottom - mDivider.getIntrinsicHeight();
        // 设置分割线范围
        mDivider.setBounds(left, top, right, bottom);
        // 绘制分割线
        mDivider.draw(canvas);
    }
    canvas.restore();
}
// 横向省略...
  • 小结:
      1. onDraw()ItemView的下层图层绘制;
      1. onDraw()是针对整个RecyclerView绘制的,使用时需要先遍历RecyclerView的所有可见的ChildView,分别获取它们的位置信息,然后再绘制内容。

3.3 onDrawOver(Canvas c,...)

  • 作用:
    ItemView上层图层绘制,因此ItemDecoration#onDrawOver()绘制的内容会覆盖ItemView,如以下示意图所示:
onDrawOver() 示意图
  • onDrawOver()onDraw()类似,区别在于绘制的图层不同,实战中用的比较少,此处不过多展开。

4. 示例讲解

Editing...

4.1 万能分割线

4.2 快递时间轴

4.3 联系人分类


推荐阅读


2020 永远不要放弃希望,祝愿大家都能够平安健康!武汉加油!

推荐阅读更多精彩内容