前言
- 在
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()
- 效果如下:
- 小结
- 使用
DividerItemDecoration
时调用setDrawable()
设置分割线
- 使用
- 调用
RecyclerView#addItemDecoration()
添加ItemDecoration
- 调用
- 可以添加多个
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...)
第一个参数outRect
的top、left、right、bottom
属性值,可以控制ItemView
在相对于矩形区域的间距,如以下示意图所示:
- 源码分析:
间距与测量流程有关,因此我们看看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);
}
}
- 小结:
- 修改
getItemOffsets(Rect outRect...)
第一个参数outRect
的top、left、right、bottom
属性值,可以控制ItemView
在相对于矩形区域的间距;
- 修改
-
RecyclerView
在测量时,会将添加的所有ItemDecoration
的top、left、right、bottom
边距累加起来,影响ItemView
的padding
。
-
3.2 onDraw(Canvas c,...)
- 作用:
在ItemView
的下层图层绘制,因此如果ItemDecoration#onDraw()
绘制的内容在ItemView
的范围内,将被ItemView
遮挡,如以下示意图所示:
- 源码分析:
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();
}
// 横向省略...
- 小结:
-
onDraw()
在ItemView
的下层图层绘制;
-
-
onDraw()
是针对整个RecyclerView
绘制的,使用时需要先遍历RecyclerView
的所有可见的ChildView
,分别获取它们的位置信息,然后再绘制内容。
-
3.3 onDrawOver(Canvas c,...)
- 作用:
在ItemView
上层图层绘制,因此ItemDecoration#onDrawOver()
绘制的内容会覆盖ItemView
,如以下示意图所示:
-
onDrawOver()
与onDraw()
类似,区别在于绘制的图层不同,实战中用的比较少,此处不过多展开。
4. 示例讲解
Editing...
4.1 万能分割线
4.2 快递时间轴
4.3 联系人分类
推荐阅读
- Android | 一文带你全面了解 AspectJ 框架
- Android | 使用 AspectJ 限制按钮快速点击
- Android | 这是一份详细的 EventBus 使用教程
- 开发者 | 浅析App社交分享的5种形式
- 开发者 | WGS84、GCJ-02、BD-09都是什么鬼?
- 开发者 | 几个提高远程办公效率的小建议
- 开发者 | 那些令人“奔溃”的 UI 验收
- Dart | 彻底理解Dart中的库与访问可见性
网友评论