点赞关注,不再迷路,你的支持对我意义重大!
🔥 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 在相对于矩形区域的间距,如以下示意图所示:
我们以前面提到的 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 的范围内会被遮挡,如以下示意图所示:
继续以 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() 与 onDraw() 类似,区别在于绘制的图层不同,实战中用的比较少,此处不过多展开。
4. 示例讲解
Editing...
4.1 万能分割线
4.2 快递时间轴
4.3 联系人分类
创作不易,你的「三连」是丑丑最大的动力,我们下次见!