概述
源码一万多行,是ViewPager的三倍多,如果不能把握住主线,会很容易迷失在源码的海洋。RecyclerView做得比ViewPager好的地方在于,它进行良好的封装和解耦,每一个类完成自己的功能,结构十分清晰。但是对于我们来说,如果不能对这些模块先有一个大概的的了解,那么阅读源码的常常会因为不清楚一个类的作用而不知所云。所以这篇文章的主线是解读RecyclerView的测量、布局和绘制,同时会对涉及的类做一个大概的介绍。
我们还是按照一般在Activity或Fragment中调用的API来尝试分析主线流程。一般我们会使用如下的代码(先不看Decoration和Animator相关):
mRecyclerView = (RecyclerView) findViewById(R.id.recyclerView);
//LayoutManager
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(OrientationHelper. VERTICAL);
recyclerView.setLayoutManager(layoutManager);
//Adapter
mAdapter = new MyAdapter(...);
mRecyclerView.setAdapter(mAdapter);
//Decoration
mRecyclerView.addItemDecoration( new DividerGridItemDecoration(this ));
//Animator
mRecyclerView.setItemAnimator( new DefaultItemAnimator());
构造器中初始化了一些数据,这里重点注意下AdapterHelper和ChildHelper两个类,后面我们再说。
public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
...
mItemAnimator.setListener(mItemAnimatorListener);
//初始化AdapterHelper
initAdapterManager();
//初始化ChildrenHelper
initChildrenHelper();
...
}
看到setLayoutManager(),重新设置LayoutManager,并请求重绘。重绘的请求交到主Handler中等待执行。
public void setLayoutManager(LayoutManager layout) {
if (layout == mLayout) {
return;
}
stopScroll();
//清除工作
...
// this is just a defensive measure for faulty item animators.
mChildHelper.removeAllViewsUnfiltered();
mLayout = layout;
if (layout != null) {
//持有RecyclerView
mLayout.setRecyclerView(this);
if (mIsAttached) {
mLayout.dispatchAttachedToWindow(this);
}
}
mRecycler.updateViewCacheSize();
//请求重绘
requestLayout();
}
一般我们会新建LinearLayoutManager或者StaggeredGridLayoutManager,注意这里每个LayoutManager会有一个成员mAutoMeasure,这个变量在初始化的时候被设为true,会影响到后面RecyclerView的测量。
//LinearLayoutManager.java
public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
setOrientation(orientation);
setReverseLayout(reverseLayout);
//置为true
setAutoMeasureEnabled(true);
}
//StaggeredGridLayoutManager.java
public StaggeredGridLayoutManager(int spanCount, int orientation) {
mOrientation = orientation;
setSpanCount(spanCount);
setAutoMeasureEnabled(mGapStrategy != GAP_HANDLING_NONE);
mLayoutState = new LayoutState();
createOrientationHelpers();
}
继续看到setAdapter(),最终会调用到setAdapterInternal()
private void setAdapterInternal(Adapter adapter, boolean compatibleWithPrevious,
boolean removeAndRecycleViews) {
//注销监听者,也就是RecyclerView的一个内部类实例
if (mAdapter != null) {
mAdapter.unregisterAdapterDataObserver(mObserver);
mAdapter.onDetachedFromRecyclerView(this);
}
if (!compatibleWithPrevious || removeAndRecycleViews) {
//清空所有View,包括已添加的和Recycler中的
removeAndRecycleViews();
}
//将AdapterHelper中数据清空
mAdapterHelper.reset();
final Adapter oldAdapter = mAdapter;
mAdapter = adapter;
if (adapter != null) {
//Adapter作为Observable注册Observer
adapter.registerAdapterDataObserver(mObserver);
adapter.onAttachedToRecyclerView(this);
}
if (mLayout != null) {
mLayout.onAdapterChanged(oldAdapter, mAdapter);
}
mRecycler.onAdapterChanged(oldAdapter, mAdapter, compatibleWithPrevious);
mState.mStructureChanged = true;
//让所有View、ViewHolder、Decorator变为无效
markKnownViewsInvalid();
}
onMeasure()
构造器、setLayoutManager()、setAdapter()做了一些“准备”工作,接下来RecycleView会进入绘制流程。
进入绘制流程后,ViewRootImpl会下发measure、layout和draw过程。我debug了onMeasure和onLayout,下面从整体上来说明下RecyclerView的子View测量布局过程,尽量做到详略得当。后面都以LinearLayoutManager为例,不再说明。
首先是onMeasure()。这里mLayout.mAutoMeasure = true(如果为false,那么你的自定义LayoutManager要重写onMeasure()方法)。由于我们一般情况下我们的RecyclerView的宽高是MATCH_PARENT,所以在为RecyclerView设置了一个宽高后,跳出onMeasure()。如果宽高是Wrap_Content,那么会走接下来的流程,由于接下来的流程和onLayout()中有重复,所以我说下大致的意思:先执行dispatchLayoutStep2()方法添加子View、测量子View、布局子View,然后调用mLayout.setMeasuredDimensionFromChildren()根据子View设置自己的宽高,这个逻辑也很好理解,23.2.0之后支持了RecyclerView的Wrap_Content属性。
只对本身进行了一个宽高设置后,子View也没有测量我们就跳出了onMeasure()函数,看来onLayout()中就是问题的关键了。
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
...
//会进入这个分支
if (mLayout.mAutoMeasure) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
//把宽高先设置成了EXACTLY模式下的宽高
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
//注意,如果我们的RecyclerView的宽高是MATCH_PARENT的话,这里直接结束onMeasure
if (skipMeasure || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
//进行子View的相关工作
dispatchLayoutStep2();
// 根据子View重新测量自身,这里是为了支持Wrap_Content
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
...
}
...
}
onLayout()
我们直接进入dispatchLayout()。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
总体上来看一共有三个步骤dispatchLayoutStep1()、dispatchLayoutStep2()、dispatchLayoutStep3(),每一个函数执行完后会修改mState.mLayoutStep(有3个状态)为下一个步骤的状态。
void dispatchLayout() {
...
if (mState.mLayoutStep == State.STEP_START) {
//步骤1
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
//步骤2
dispatchLayoutStep2();
}
...
//步骤3
dispatchLayoutStep3();
onExitLayoutOrScroll();
}
State
注释给的解释是这个类包含了当前RecyclerView状态的有用信息,包括滚动的最终位置或者焦点等。
这个类我理解为RecyclerView为各个子模块提供的上下文 , 它保存了一些RecyclerView当前需要的状态,并且在子模块需要这些信息的时候,传递State实例过去。这里要说一个重要的思想:数据集中的count和界面中的count不一定相等,数据集发生变化后,RecyclerView通过Layout达成界面和数据集的同步,也因此会产生动画。
一些重要的解释注释在代码中。public static class State { ... //如果在滑动,记录的是重点position private int mTargetPosition = RecyclerView.NO_POSITION; //当前处于哪个Layout阶段,还有STEP_LAYOUT,STEP_ANIMATIONS,从名字也可以看出,第二个阶段是真正布局的阶段 private int mLayoutStep = STEP_START; private SparseArray<Object> mData; //对应于RecyclerView中Item的数量 //和LayoutManager中的getItemCount返回的数量不太一样,LM返回的数量一定是数据集的数量 int mItemCount = 0; //上一次Layout后Item的数量(因为Layout后数据集中的数据就和界面同步了) private int mPreviousLayoutItemCount = 0; //在prelayout阶段从adpter中删除的item的数量 private int mDeletedInvisibleItemCountSincePreviousLayout = 0; //是否有结构变化(指增删,更新不算) private boolean mStructureChanged = false; //是否在prelayout阶段 private boolean mInPreLayout = false; //支持的动画模式 private boolean mRunSimpleAnimations = false; private boolean mRunPredictiveAnimations = false; //是否正在测量中 private boolean mIsMeasuring = false; ... //Returns the total number of items that can be laid out. //返回的是数据集中的数量 public int getItemCount() { return mInPreLayout ? (mPreviousLayoutItemCount - mDeletedInvisibleItemCountSincePreviousLayout) : mItemCount; } }
dispatchLayoutStep2()
我们先略去dispatchLayoutStep1()和dispatchLayoutStep3(),单纯来看下RecyclerView是如何布局子View的。(其实是因为1和3目前还没有搞得很清楚)
private void dispatchLayoutStep2() {
//RecyclerView改写了requestLayout(),调用这个方法后,布局的时候调用addView添加子View并不会引起新一轮绘制流程了
eatRequestLayout();
...
//获取数据集中个数
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
// Step 2: Run layout
mState.mInPreLayout = false;
//重点
mLayout.onLayoutChildren(mRecycler, mState);
...
}
转入LinearLayoutManager的onLayoutChildren(),在这之前先说两个子模块LayoutState和OrientationHelper。
LayoutState
保存了布局时所需要的信息,随着布局的进行信息将会更新,布局完成后将被清空。重要的内容注释在代码中static class LayoutState { //方向 final static int LAYOUT_START = -1; final static int LAYOUT_END = 1; //布局开始的地方,会随着不断布局子View更新 int mOffset; //在布局方向上还需布局的距离(Pixels) int mAvailable; //下一个数据在数据集中的position int mCurrentPosition; //数据集遍历的方向 int mItemDirection; //这次layout的方向 int mLayoutDirection; //在scrolling状态下被构造,代表不创建新View的情况下可以滑动多少距离 int mScrollingOffset; //用于prelayout int mExtra = 0; boolean mIsPreLayout = false; //上一次滑动的距离 int mLastScrollDelta; //用于LLM想布局特殊的View,从这个List获取 List<RecyclerView.ViewHolder> mScrapList = null; boolean mInfinite; //数据集中是否还有更多数据 boolean hasMore(RecyclerView.State state) { return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount(); } //很关键的方法,向Recycler索取一个View,由于解耦得非常好,所以这里我们只需要理解为能根据mCurrentPosition和ViewType获取一个View就可以,至于怎么获得,在下一篇再说 View next(RecyclerView.Recycler recycler) { if (mScrapList != null) { return nextViewFromScrapList(); } final View view = recycler.getViewForPosition(mCurrentPosition); mCurrentPosition += mItemDirection; return view; } ... }
OrientationHelper
Linearlayout的布局方向有两个,这个类把布局方向封装,内部实现了对方向的转换,即转换成了方向无关的“Start”和“End”,以方便布局的时候使用。我们看一下“纵向的Helper”。可以看到对外提供了很方便的接口。public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) { return new OrientationHelper(layoutManager) { @Override public int getEndAfterPadding() { return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom(); } @Override public int getEnd() { return mLayoutManager.getHeight(); } @Override public void offsetChildren(int amount) { mLayoutManager.offsetChildrenVertical(amount); } @Override public int getStartAfterPadding() { return mLayoutManager.getPaddingTop(); } @Override public int getDecoratedMeasurement(View view) { final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); return mLayoutManager.getDecoratedMeasuredHeight(view) + params.topMargin + params.bottomMargin; } @Override public int getDecoratedMeasurementInOther(View view) { final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); return mLayoutManager.getDecoratedMeasuredWidth(view) + params.leftMargin + params.rightMargin; } @Override public int getDecoratedEnd(View view) { final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin; } @Override public int getDecoratedStart(View view) { final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); return mLayoutManager.getDecoratedTop(view) - params.topMargin; } ... @Override public void offsetChild(View view, int offset) { view.offsetTopAndBottom(offset); } @Override public int getEndPadding() { return mLayoutManager.getPaddingBottom(); } ... }; }
看完了这两个子模块,可以真正进入布局的过程了,注释也写了,主要有4步:找到锚点,向start去fill,向end去fill,scroll以满足要求。我留下一些关键的步骤并注释。
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//如果mPendingSavedState不空 或者 mPendingScrollPosition != NO_POSITION 就回收所有View
//mPendingSavedState和状态保存有关,会保存和恢复锚点相关信息
//mPendingScrollPosition代表scroll to a position时会设置这个值并且触发layout
if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) {
if (state.getItemCount() == 0) {
//这里回收的是 已经添加到RV中且不是hidden的子View(hidden先理解为要删除但正在播放动画的View)
//至于回收的定义是什么,回收时做了什么,下篇再说,这里Recycler已经解耦得很好,只需要知道被回收
removeAndRecycleAllViews(recycler);
return;
}
}
//从状态保存的信息中恢复锚点
if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
}
//新建State和OrientationHelper
ensureLayoutState();
mLayoutState.mRecycle = false;
// resolve layout direction
resolveShouldLayoutReverse();
//重置锚点信息,AnchorInfo也是一个数据结构,保存了一些简单的信息
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
//更新锚点信息,可能从3种方式回去锚点,这里第一次布局直接从0或state.getItemCount() - 1开始
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
//如果有滑动的目标position,那么LLM会额外布局一些空间(应该是提升用户体验)
int extraForStart;
int extraForEnd;
//从源码来看,这里获取的是出去padding的height,也就是大约一页面的距离
final int extra = getExtraLayoutSpace(state);
//如果上一次是内容向下滑动,那么这一次额外布局应该接在末尾
if (mLayoutState.mLastScrollDelta >= 0) {
extraForEnd = extra;
extraForStart = 0;
} else {
extraForStart = extra;
extraForEnd = 0;
}
//考虑padding
extraForStart += mOrientationHelper.getStartAfterPadding();
extraForEnd += mOrientationHelper.getEndPadding();
...//没看懂的但不影响大局的删除了
//回调
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
//这里又有一种回收,我们先不管,只需要知道也是针对于和上面一样的“可见的”View就行了
detachAndScrapAttachedViews(recycler);
mLayoutState.mIsPreLayout = state.isPreLayout();
//真正开始布局
if (mAnchorInfo.mLayoutFromEnd) {
...
} else {
//由于是从锚点向上下fill,我们只看一半就好了
// fill towards end
//更新State准备向下fill
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
//关键函数,后面单独拉出
fill(recycler, mLayoutState, state, false);
...
// fill towards start
...
}
//填补可能产生的gap
if (getChildCount() > 0) {
...
}
}
整个流程看完了,梳理一下无非是:更新了锚点信息,可能计算extra,回收View,然后就开始从锚点fill。那么这个fill函数就是重点,我们单独拉出来看一下。整个函数主要做了这么一件事:在循环中layoutChunk(),更新信息以及回收超出边界的View。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// max offset we should set is mFastScroll + available
final int start = layoutState.mAvailable;
//回收滑出边界的View(同样回收逻辑忽略)
if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
//一个简单的类,记录了一些信息
LayoutChunkResult layoutChunkResult = new LayoutChunkResult();
//如果还有剩余空间且可以获得更多的View
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
//关键函数,等下解释
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutChunkResult.mFinished) {
break;
}
//更新Offset
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
//更新.mAvailable和remainingSpace
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
//回收超出边界的子View
if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
}
return start - layoutState.mAvailable;
}
我们来看这个方法,终于看到了子View 相关的动作:从State中获取一个View,添加,测量,布局。
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//获取一个View
View view = layoutState.next(recycler);
...
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
//添加View,注意这里会引起RV的重绘,但是记得上面在讲Step2的时候有一个 eatRequestLayout()
//会阻止requestlayout请求,也就是说循环添加子View的时候并不会触发RV的重绘。
//还要说一下,这里的在ChildHelper中注册了信息然后添加到了ViewGroup中
addView(view);
} else {
addView(view, 0);
}
}
//测量
measureChildWithMargins(view, 0, 0);
...
//布局
layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
right - params.rightMargin, bottom - params.bottomMargin);
。。。
}
小结
看到这里,step2算是结束了,step3涉及到动画,不影响理解RV的绘制流程,这里先打住。
总结一下,初次布局,如果是Match_Parent,会直接跳出onMeasure()进入onLayout()(如果是wrap_content,那么要先进行子View的测量布局,然后确定自身大小)。onLayout中分为3步,step2为真正对子View进行操作的地方,流程大致是找到锚点,以锚点为基准获取子View,添加子View,测量子View,布局子View,同时还有可能回收超出边界的View。之后的onDraw方法就不再说了。
RV的绘制流程算是走完了,但还有很多问题没有解决:step1和3到底做了什么,动画是如何产生的,ChildHelper与AdapterHelper的功能是什么,回收机制是什么样的,从Recycler获取View又是什么样的流程。
这些问题我弄清楚整合以后会在后面几篇给出我的见解~