本文是RecyclerView源码分析系列的第三篇,这篇文章我们来分析ItemDecoration。
本文结合RecyclerView源码剖析: 基本显示一起看,效果更好。
添加ItemDecoration
public void addItemDecoration(@NonNull ItemDecoration decor) {
addItemDecoration(decor, -1);
}
public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
// ...
// 1.保存ItemDecoration
if (index < 0) {
mItemDecorations.add(decor);
} else {
mItemDecorations.add(index, decor);
}
// 2.标记ItemDecoration区域为dirty
markItemDecorInsetsDirty();
// 3.请求重新布局
requestLayout();
}
RecyclerView
首先用ArrayList mItemDecorations
保存ItemDecoation
,然后标记ItemDecoraction
的区域需要刷新,最后请求重新布局。
测量
这里的测量不是指的ItemDecoration
的测量,而是指的是带有ItemDecoration
的子View测量。因为RecyclerView
需要知道子View的ItemDecoration
占据的空间,从而来测量子View。
ItemDecoration
的测量是由LayoutManager#measureChildWithMargins()
或LayoutManager#measureChild()
实现的,这两个方法非常类似,差别只在于是否考虑margin
。
我们以LayoutManager#measureChildWithMargin()
为例分析
public void measureChildWithMargins(@NonNull View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 1. 调用ItemDecoration#getItemOffsets()获取ItemDecoration的Rect
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
// 计算ItemDecoration已经使用的宽高
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
// 2. 获取子View测量MeasureSpec
// 第二个参数指的是已使用的宽高,其中包括margin,ItemDecoration使用的宽高
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
// 3. 测量子View
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
首先调用getItemDecorInsetsForChild()
来获取子View的ItemDecoration
区域
Rect getItemDecorInsetsForChild(View child) {
// ...
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
// 遍历所有的ItemDecoration
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
// 获取每一个ItemDecoration的绘制区域
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
// 累加每一个ItemDecoration所占据的区域
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
原来是调用每一个ItemDecoration#getItemOffsets()
方法获取ItemDecoration
需要绘制的区域,然后累加每一个ItemDecoration
的left
, top
, right
, bottom
值,作为所有ItemDecoration
的Rect
。
获取了所有ItemDecoration
的Rect
后,测量子View就考虑了RecyclerView
的padding
,子View的margin
,以及子View的ItemDecoration
所占用的宽高。
这里用一副图来解释这里的测量逻辑
布局
这里说的布局也不是指ItemDecoration
的布局,而是指带有ItemDeocation
的子View的布局,它是由LayoutManager#layoutDecoratedWithMargins
实现的。
public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, int right,
int bottom) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 获取ItemDecoration的Rect
final Rect insets = lp.mDecorInsets;
child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
right - insets.right - lp.rightMargin,
bottom - insets.bottom - lp.bottomMargin);
}
这里就是按照上面那副图进行布局的,大家对照这看看就明白了。
测量和布局都需要大家有自定义View的测量和布局的基本功。
绘制
这次就真的是ItemDecoration
的绘制
public void draw(Canvas c) {
super.draw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
// 按顺序逐个绘制
mItemDecorations.get(i).onDrawOver(c, this, mState);
}
// over scroll绘制 ...
}
public void onDraw(Canvas c) {
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
// 按顺序逐个绘制
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
这里需要你有一点点自定义View绘制的基本功,绘制顺序如下
- 调用
ItemDecoration#onDraw()
绘制。 - 绘制子View。
- 调用
ItemDecoration#onDrawOver()
绘制。
这里我们需要注意一点,由于这里是按照添加ItemDecoration
的顺序进行遍历绘制,因此我们在添加多个ItemDecoration
的时候,要注意添加的顺序,否则可能造成显示错乱。
思考
我们再来思考一个问题,在测量,布局和绘制的过程中,似乎没有定义margin
和ItemDecoration
的顺序,那么到底是ItemDecoration
靠近子View,还是margin
靠近子View?从ItemDecoration
的命名可以猜测,它应该是靠近子View,而margin
应该在外侧。但是我们不一定要遵守这种默认的规则,既然RecyclerView
没有强制定义顺序,那么就留给我们更大的灵活性,因此可以根据实际情况进行灵活调整。
结束
ItemDecoration
的原理比较简单,但是它在实际中的应用却是很广泛的,在下一篇文章中,我们将使用ItemDecoration
来实现一个类似微信联系人的效果。
如果你觉得我的文章写的还行,可以点个赞以及关注我。