RecyclerView 源码分析
写在前面:
RecyclerView 是一个越用越优雅的控件,相信大家对于 RecyclerView 的使用也已经比较熟悉了。其功能的高度解耦化,规范 ViewHolder 的写法,以及对动画友好支持,都是它与传统控件 ListView 的区别。而无论 ListView 还是 RecyclerView,本质上都是在有限的屏幕之上,展示大量的内容。所以复用的逻辑,就成了它们最最重要的核心原理,本文主要目的就是探究 RecyclerView 的复用原理。
RecyclerView 的几大模块
LayoutManager
负责 RecyclerView 中,控制 item 的布局方向RecyclerView.Adapter
为 RecyclerView 承载数据ItemDecoration
为 RecyclerView 添加分割线ItemAnimator
控制 RecyclerView 中 item 的动画
刚刚我们提到 RecyclerView 的高度解耦,就是通过以上对象各司其职,来实现 RecyclerView 的基本功能。RecyclerView 无论多么复杂,本质上也是一个自定义 View,本文的重点就是缓存原理分析,不过在此之前,我们还是简单地分别介绍下以上中各个模块的源码。
ItemDecoration
public void addItemDecoration(ItemDecoration decor, int index) {
if (mLayout != null) {
mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll or"
+ " layout");
}
if (mItemDecorations.isEmpty()) {
setWillNotDraw(false);
}
if (index < 0) {
mItemDecorations.add(decor);
} else {
// 指定添加分割线在集合中的索引
mItemDecorations.add(index, decor);
}
markItemDecorInsetsDirty();
// 重新请求 View 的测量、布局、绘制
requestLayout();
}
mItemDecorations
是一个 ArrayList
,我们将 ItemDecoration
也就是分割线对象,添加到其中。接着我们看下 markItemDecorInsetsDirty
这个方法做了些什么。
void markItemDecorInsetsDirty() {
final int childCount = mChildHelper.getUnfilteredChildCount();
for (int i = 0; i < childCount; i++) {
final View child = mChildHelper.getUnfilteredChildAt(i);
((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
}
mRecycler.markItemDecorInsetsDirty();
}
这个方法首先遍历了 RecyclerView 和 LayoutManager 的所有子 View,将其子 View 的 LayoutParams 中的 mInsetsDirty
属性置为 true。接着调用了 mRecycler.markItemDecorInsetsDirty()
,Recycler 是 RecyclerView 的一个内部类,就是它管理着 RecyclerView 的复用逻辑。这个我们一会再细谈。
void markItemDecorInsetsDirty() {
final int cachedCount = mCachedViews.size();
for (int i = 0; i < cachedCount; i++) {
final ViewHolder holder = mCachedViews.get(i);
LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams();
if (layoutParams != null) {
layoutParams.mInsetsDirty = true;
}
}
}
mCachedViews
见名知意,也就是 RecyclerView 缓存的集合,相信你也看到了,RecyclerView 的缓存单位是 ViewHolder。我们在 ViewHolder 中取出 itemView,然后获得 LayoutParams,将其 mInsetsDirty 字段一样置为 true。
mInsetsDirty 字段的作用其实是一种优化性能的缓存策略,添加分割线对象时,无论是 RecyclerView 的子 view,还是缓存的 view,都将其置为 true,接着就调用了 requestLayout
方法。
这里简单说一下 requestLayout
方法用一种责任链的方式,层层向上传递,最后传递到 ViewRootImpl,然后重新调用 view 的 measure、layout、draw 方法来展示布局。
我们在 RecyclerView 中搜索 mItemDecorations 集合,看看他是在什么时刻操作 ItemDecoration 这个分割线对象的。
在 onDraw
中:
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
在 draw
方法中:
@Override
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);
}
}
可以看到在 View 的以上两个方法中,分别调用了 ItemDecoration 对象的 onDraw
onDrawOver
方法。
这两个抽象方法,由我们继承 ItemDecoration 来自己实现,他们区别就是 onDraw
在 item view 绘制之前调用,onDrawOver
在 item view 绘制之后调用。
所以绘制顺序就是 Decoration 的 onDraw,ItemView的 onDraw,Decoration 的 onDrawOver。
(好像越写越多…收不住了…)
我们在 onDraw
和 onDrawOver
方法中就可以绘制 drawable 对象了。此时分割线就展现出来了。还记得刚才的 mInsetsDirty
字段吗?在添加分割线的时候,无论是 RecyclerView 子 View,还是缓存中的 View,其 LayoutParams 中的 mInsetsDirty 属性,都被置为 true。 我们来解释一下这个字段的作用:
Rect getItemDecorInsetsForChild(View child) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (!lp.mInsetsDirty) {
// 当 mInsetsDirty 为 false,说明 mDecorInsets 缓存可用
return lp.mDecorInsets;
}
if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
// changed/invalid items should not be updated until they are rebound.
return lp.mDecorInsets;
}
final Rect insets = lp.mDecorInsets;
insets.set(0, 0, 0, 0);
final int decorCount = mItemDecorations.size();
for (int i = 0; i < decorCount; i++) {
mTempRect.set(0, 0, 0, 0);
mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
insets.left += mTempRect.left;
insets.top += mTempRect.top;
insets.right += mTempRect.right;
insets.bottom += mTempRect.bottom;
}
lp.mInsetsDirty = false;
return insets;
}
来解释一下这段代码,首先 getItemDecorInsetsForChild
方法是在 RecyclerView 进行 measureChild 时调用的。目的就是为了取出 RecyclerView 的 ChildView 中的分割线属性 — 在 LayoutParams 中缓存的 mDecorInsets 。而 mDecorInsets 就是 Rect 对象, 其保存记录的是所有添加分割线需要的空间累加的总和,由分割线的 getItemOffsets
方法影响。
最后在 measureChild 方法里,将分割线 ItemDecoration 的尺寸加入到 itemView 的 padding 中。
但是大家都知道缓存并不是总是可用的,mInsetsDirty 这个 boolean 字段来记录它的时效性,当 mInsetsDirty 为 false 时,说明缓存可用,直接取出可以,当 mInsetsDirty 为 true 时,说明缓存的分割线属性就需要重新计算了。
到此,关于 RecyclerView 添加分割线 ItemDecoration 的源码分析,也就基本结束了。
ItemAnimator
如果有人问我,在什么情况下你绝对会选择 RecyclerView,而不是 ListView?如果需求对 Item 的动画有一定要求,这绝对是我选择 RecyclerView 的重要原因之一。ListView 如果要做 Item 的增删动画,那可要费很大劲儿,而 RecyclerView 自身对动画就有很好的支持。
public void setItemAnimator(ItemAnimator animator) {
if (mItemAnimator !=