在Android开发中RecyclerView是我们高频使用的一个组件,用来展示大量的数据。我们不仅要熟练使用它,还要对它的实现有一个认知。本片文章介绍RecyclerView的绘制流程,也就是onMeasure、onLayout、onDraw这三个方法中主要做了些什么工作,let's go!
OnMeasure
我们知道该方法是测量当前View及子View的宽高,但是查看RecyclerView的源码发现代码很长,它不仅仅做了测量的工作,还做了一些其他的工作,我们一起来看一下
if (mLayout == null) {
//第一种情况
}
if (mLayout.isAutoMeasureEnabled()) {
//第二种情况,通常会进入到这种情况
}else{
// 第三种情况
}
根据条件分支将onMeasure方法分成了三种情况,我们挨个来讨论一下
- 情况一:
mLayout == null
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
void defaultOnMeasure(int widthSpec, int heightSpec) {
final int width = LayoutManager.chooseSize(widthSpec,getPaddingLeft() + getPaddingRight(),ViewCompat.getMinimumWidth(this));
final int height = LayoutManager.chooseSize(heightSpec,getPaddingTop() + getPaddingBottom(),ViewCompat.getMinimumHeight(this));
setMeasuredDimension(width, height);
}
public static int chooseSize(int spec, int desired, int min) {
final int mode = View.MeasureSpec.getMode(spec);
final int size = View.MeasureSpec.getSize(spec);
switch (mode) {
case View.MeasureSpec.EXACTLY:
return size;
case View.MeasureSpec.AT_MOST:
return Math.min(size, Math.max(desired, min));
case View.MeasureSpec.UNSPECIFIED:
default:
return Math.max(desired, min);
}
}
mLayout == null时,从传入的参数可以看出来RecyclerView没有考虑子View就决定了自己的大小,是一个比较粗糙的测量,具体的大小还需要根据之后多次测量的结果来定
- 情况二
mLayout.isAutoMeasureEnabled() == true
mLayout.isAutoMeasureEnabled()为true时,将调用LayoutManager.onLayoutChildren(Recycler, State)来计算子View们所需要的大小,RecyclerView.LayoutManager的实现类LinearLayoutManager、StaggeredGridLayoutManager都重载了该方法并返回true,所以通常都会走入这个分支(列出了部分代码)
if (mLayout.isAutoMeasureEnabled()) {
//将测量交给LaytouManager
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
//如果width和height都已经是精确值,那么就不用再根据内容进行测量,后面步骤不再处理
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
dispatchLayoutStep2();
//如果需要二次测量的话
if (mLayout.shouldMeasureTwice()) {
dispatchLayoutStep2();
}
}
第一步调用了LayoutManager.onMeasure()方法
public void onMeasure(@NonNull Recycler recycler, @NonNull State state, int widthSpec,int heightSpec) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}
该方法直接调用了RecyclerView的默认测量方法,就是我们之前分析的第一种情况。阅读该方法的注释我们可以发现,LayoutoManager强烈推荐开启自动测量,并且如果开启了自动测量就不要重写该方法,LayoutoManager的三个默认实现也确实没重写该方法。还介绍了下测量的策略,如果宽高测量模式为UNSPECIFIED. AT_MOST则指定为EXACTLY并且RecyclerView占用可用的最大空间。
第二步
如果宽高的测量模式都为MeasureSpec.EXACTLY或者没有设置Adapter直接返回。
接下来我们继续看,mState.mLayoutStep的默认值就是State.STEP_START,进入条件语句执行dispatchLayoutStep1(),然后执行dispatchLayoutStep2(),如果需要执行二次测量的话在执行一次dispatchLayoutStep2()。
我们重点看dispatchLayoutStep1()和dispatchLayoutStep2(),与这两个方法息息相关的一个变量是mState.mLayoutStep,该变量得意义是决定了dispatchLayoutStep1()、dispatchLayoutStep1()、dispatchLayoutStep2()这三个方法该执行哪一步了,它的取值有三个
| mLayoutStep | 描述 |
|---|---|
| STEP_START | 默认值或者dispatchLayoutStep3()已经执行了 |
| STEP_LAYOUT | dispatchLayoutStep1()已经执行了 |
| STEP_ANIMATIONS | dispatchLayoutStep2()已经执行了 |
对这三个方法的具体分析,我们放到onLayout中处理,先说一下结论dispatchLayoutStep1()处理了Adapter的数据更新以及准备动画前的数据;dispatchLayoutStep2()进行itemView的布局
-
情况三:
mLayout.isAutoMeasureEnabled() == false我们通常使用的
LayoutManager都返回true,除非我们自定义,所以暂不分析这种情况
总结一下
onMeasure所做的工作,假设LayoutManager为LinearLayoutManager
- 测量一下自己,可能需要多次测量
- 如果宽高不都为
MeasureSpec.EXACTLY模式则执行
dispatchLayoutStep1(),处理Adapter更新以及准备动画前的数据dispatchLayoutStep2()进行itemView的布局
OnLayout
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
就是执行了dispatchLayout
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
return;
}
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
// First 2 steps are done in onMeasure but looks like we have to run again due to
// changed size.
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// always make sure we sync them (to ensure mode is exact)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
流程也很清晰
- 如果
Adapter和LayoutManager没设置就不进行布局,RecyclerView也就只能显示一片空白 - 如果之前
onMeasure中执行了dispatchLayoutStep1()和dispatchLayoutStep2()则不再执行这两个方法,不过dispatchLayoutStep2()可能需要被再次调用 - 执行
dispatchLayoutStep3()
现在我们来查看下dispatchLayoutStep系列方法到底干了些什么
dispatchLayoutStep1()
/**
* The first step of a layout where we;
* - process adapter updates
* - decide which animation should run
* - save information about current views
* - If necessary, run predictive layout and save its information
*/
private void dispatchLayoutStep1() {
processAdapterUpdatesAndSetAnimationFlags();
if (mState.mRunSimpleAnimations){
//...
}
if (mState.mRunPredictiveAnimations){
//...
}
mState.mLayoutStep = State.STEP_LAYOUT;
}
以上代码缩减了很多,从该方法的注释可以看出来,dispatchLayoutStep1()主要处理了Adapter更新以及准备动画所需数据,而processAdapterUpdatesAndSetAnimationFlags()就是用来处理Adapter更新和动画的Flag处理,我们看一下这个方法里面
private void processAdapterUpdatesAndSetAnimationFlags() {
if (mDataSetHasChangedAfterLayout) {
// Processing these items have no value since data set changed unexpectedly.
// Instead, we just reset it.
mAdapterHelper.reset();
if (mDispatchItemsChangedEvent) {
mLayout.onItemsChanged(this);
}
}
为动画的flag进行赋值
mState.mRunSimpleAnimations = ...
mState.mRunPredictiveAnimations = ...
}
首先处理Adapter的更新(Adapter.notifyDataSetChanged()或者RecyclerView.swapAdapter(Adapter, boolean)代表Adapter有更新),就是简单的把之前记录的每一个item的操作重置一下,因为数据集的更改导致之前存的信息都没有意义了,下边的代码是为动画的标志位赋值,我们调用RecyclerView.setAdapter和Adapter.notifyDataSetChanged()是不会触发动画的,所以我们先不考虑动画相关的东西。
我们继续来看dispatchLayoutStep1()的内容,下面是两个if条件,涉及两个变量mState.mRunSimpleAnimations和mState.mRunPredictiveAnimations这两个变量在要执行动画时才为true,所以先不考虑里面的内容。
最后执行mState.mLayoutStep = State.STEP_LAYOUT,代表dispatchLayoutStep1()已经执行完毕了
总结
dispatchLayoutStep1()处理了Adapter更新以及准备动画所需数据
- dispatchLayoutStep2()
private void dispatchLayoutStep2() {
mLayout.onLayoutChildren(mRecycler, mState);
mState.mLayoutStep = State.STEP_ANIMATIONS;
}
方法很简洁,调用了LayoutManager.onLayoutChildren(Recycler recycler, State state),该方法就是进行子View布局的实质方法,不过是一个空实现,子类必须去实现这个方法,声明如下
public void onLayoutChildren(Recycler recycler, State state) {
Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, Statestate) ");
}
最后mState.mLayoutStep = State.STEP_ANIMATIONS代表dispatchLayoutStep2()已经执行完毕了
总结
dispatchLayoutStep2()调用LayoutManager.onLayoutChildren来进行子View的布局
- dispatchLayoutStep3()
private void dispatchLayoutStep3() {
mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
//记录layout之后View的信息,并触发动画
//...
}
//...一些清理工作
}
首先将mState.mLayoutStep = State.STEP_START,标志dispatchLayoutStep3()已经执行了,然后mState.mRunSimpleAnimations这个变量表示是否执行动画,第一次布局的时候是不需要动画的所以不会进入这个分支,动画我们之后在讲,最后做一些清理的工作。
总结
dispatchLayoutStep3()触发动画
总结一下
onLayout所做的工作,大体流程如下
dispatchLayoutStep1()处理了Adapter更新以及准备动画所需数据dispatchLayoutStep2()调用LayoutManager.onLayoutChildren来进行子View的布局dispatchLayoutStep3()触发动画
draw
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);
}
//...处理clipToPadding="false"的情况
}
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和onDraw对RecyclerView的分割线进行了绘制,当然分割线需要我们自己去实现具体的绘制内容,同时我们也知道了ItemDecoration中onDraw和onDrawOver的区别
至此我们已经完成了RecyclerView三大流程的源码分析,上面列出的代码大多都经过了精简,省去了很多细节,不过刚开始阅读源码时,我们只要把握整体的流程就好,抛开细节来看,以上的整体流程并不难理解。但是有一个很重要的方法没有细讲,就是LayoutManager.onLayoutChildren(),该方法才是布局子View的核心,我们对该方法进行单独的一波分析,以LinearLayoutManager(只考虑竖直方向)为例来看一下
LinearLayoutManager.onLayoutChildren()
在进行子View的布局中利用了一些帮助类来帮助布局,我们需要先了解一下这些帮助类
- LayoutState
| 属性 | 解释 |
|---|---|
mOffset |
偏移多少个像素点之后开始布局 |
mAvailable |
当前布局方向上可用的空间 |
mCurrentPosition |
要布局子View在Adapter中的代表的位置 |
mInfinite |
布局的View数量没有限制 |
- AnchorInfo
| 属性 | 解释 |
|---|---|
mPosition |
锚点位置 |
mCoordinate |
锚点坐标信息 |
mValid |
是否可用 |
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) by checking children and other variables, find an anchor coordinate and an anchor
// item position.
// 2) fill towards start, stacking from bottom
// 3) fill towards end, stacking from top
// 4) scroll to fulfill requirements like stack from bottom.
//新建一个LayoutState
ensureLayoutState();
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION || mPendingSavedState != null) {
mAnchorInfo.reset();
//更新锚点信息
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}
//...计算LinearLayoutManager所需的额外空间
//锚点信息准备好了
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
//将现有的子View都缓存起来
detachAndScrapAttachedViews(recycler);
if (mAnchorInfo.mLayoutFromEnd){
//...
}else{
// fill towards end
//将锚点的信息保存到mLayoutState中
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
// fill towards start
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
if (mLayoutState.mAvailable > 0) {
fill(recycler, mLayoutState, state, false);
}
}
}
以上代码省略了很多有用信息,包括对LayoutState内部一些有用属性的赋值等。由代码刚开始的注释可了解到该方法内部执行逻辑
- 找到锚点位置和锚点坐标
- 从锚点开始,往上填充布局子View,直到填满区域
- 从锚点开始,往下填充布局子View,直到填满区域
- 滚动以满足需求,如从底部堆叠
关键是锚点,对于LinearLayoutManager来说,它不一定是从最高点或者最低点开始布局,有可能是从中间某个点开始布局的,如图所示

- 确定锚点信息
- 第一次布局,锚点信息肯定是不可用的,进入更新锚点的条件语句中,里面调用
updateAnchorInfoForLayout(recycler, state, mAnchorInfo)更新锚点信息
- 第一次布局,锚点信息肯定是不可用的,进入更新锚点的条件语句中,里面调用
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
AnchorInfo anchorInfo) {
// 第一种计算方式
if (updateAnchorFromPendingData(state, anchorInfo)) {
if (DEBUG) {
Log.d(TAG, "updated anchor info from pending information");
}
return;
}
// 第二种计算方式
if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
if (DEBUG) {
Log.d(TAG, "updated anchor info from existing children");
}
return;
}
if (DEBUG) {
Log.d(TAG, "deciding anchor info for fresh state");
}
// 第三种计算方式
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
可以看出来有三种计算锚点信息的方法,每个方法里的代码虽然多却不难理解
- 方法一
private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) {
if (state.isPreLayout() || mPendingScrollPosition == RecyclerView.NO_POSITION) {
return false;
}
//...
}
mPendingScrollPosition == RecyclerView.NO_POSITION这个条件判断默认情况是true的,只有在调用scrollToPosition或者在onRestoreInstanceState恢复之前记录的mPendingScrollPosition时才会有其他值(上面省略代码就是计算mPendingScrollPosition不为默认值的锚点信息,本文没有分析),所以默认情况该方法没有计算锚点信息,往下走
- 方法二
updateAnchorFromChildren这个方法根据子View来确定锚点信息- 如果没有子View则,直接返回
false,表示没有计算出锚点信息 - 有子View的话,一般会选择屏幕中可见的子View的position为锚点。这里会选取屏幕上第一个可见View,也就是positon=1的子View作为参考点,
anchorInfo.mCoordinate被赋值为1号子View上面的Decor的顶部位置
- 如果没有子View则,直接返回
该方法的详细分析可以看这篇文章RecyclerView源码解析
- 方法三
最后兜底的方法,直接将anchorInfo.mCoordinate赋值为padding,如果没有设置padding,则anchorInfo.mCoordinate = 0,anchorInfo.mPosition = 0(mStackFromEnd == false的情况,该值默认是false)
锚点信息的计算主要是为
mPosition、mCoordinate这两个变量赋值,这样我们就知道了从哪个点开始填充子View和子View对应的数据在Adapter中的位置
更新锚点信息之后,源码中有一长段代码用来计算LinearLayoutManager需要的“额外空间”,这段代码我也没懂,就不分析了,不过并不影响我们把握整体布局流程。锚点信息都准备好之后,updateLayoutStateToFillEnd()将锚点信息保存到mLayoutState中,然后调用fill()方法开始填充子View了,mAnchorInfo.mLayoutFromEnd将填充分为两种情况
true: 从Adapter最后一项开始,从下往上布局false: 从Adapter第一项开始,从上往下布局(默认情况) 如图所示(虚线表示屏幕外边的ItemView)
默认情况为false,从上往下开始布局,然后进入关键的fill()方法
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
//可用空间
final int start = layoutState.mAvailable;
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
//保存了每一子View消耗的空间
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//循环布局子View
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
//核心方法
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
//消费子View占据的空间
remainingSpace -= layoutChunkResult.mConsumed;
}
//...
}
return start - layoutState.mAvailable;
}
fill的核心思路就是在一个循环里不断地进行子View布局,结束条件是没有可用空间或者数据源没有数据了,layoutChunk负责填充,每填充一个子View,剩余空间就减相对应View占据的空间(竖直方向上来说就是高度),然后填充下一个,最后返回的是本次布局所填充的区域大小。
我们进入layoutChunk来看具体的实现
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//获取一个子View
View view = layoutState.next(recycler);
if (view == null) {
if (DEBUG && layoutState.mScrapList == null) {
throw new RuntimeException("received null view when unexpected");
}
// if we are laying out views in scrap, this may return null which means there is
// no more items to layout.
result.mFinished = true;
return;
}
//将View添加到RecyclerView中
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
//测量子View的大小包括margins和decorations
measureChildWithMargins(view, 0, 0);
//将占据的空间保存到LayoutChunkResult之中,供外层循环使用
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
if (isLayoutRTL()) {
right = getWidth() - getPaddingRight();
left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
} else {
left = getPaddingLeft();
right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
bottom = layoutState.mOffset;
top = layoutState.mOffset - result.mConsumed;
} else {
top = layoutState.mOffset;
bottom = layoutState.mOffset + result.mConsumed;
}
} else {
top = getPaddingTop();
bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
right = layoutState.mOffset;
left = layoutState.mOffset - result.mConsumed;
} else {
left = layoutState.mOffset;
right = layoutState.mOffset + result.mConsumed;
}
}
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
//将子View放到合适的位置
layoutDecoratedWithMargins(view, left, top, right, bottom);
if (DEBUG) {
Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
+ (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
+ (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
}
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();
}
- 第一步
View view = layoutState.next(recycler)获取一个itemView,这里涉及到RecyclerView的缓存机制,我们后边的篇章在讨论。 - 第二步 是将
itemView添加到RecyclerView中,又分为两种情况addView很好理解,方法内部做了一些位置正确性、避免重复添加等逻辑判断,然后调用ViewGroup的addView来实现。addDisappearingView代表该View即将从屏幕上消失,比如划出屏幕或者调用Adapter.notifyItemRemoved,该方法和上面的addView都会调用内部的addViewInt(View child, int index, boolean disappearing),只不过是最后一个参数不一样而已。
- 第三步 测量
itemView的大小,measureChildWithMargins(view, 0, 0)这个方法内部除了自身大小之外,还需要考虑margin和decorations(我们常说的分割线)的大小。测量之后把消耗的空间保存到LayoutChunkResult之中,供外层循环使用。 - 第四步 将
itemView放到合适的位置,计算位置时layoutState.mOffset跟我们之前算的锚点坐标息息相关,如果是第一个itemView,则layoutState.mOffset和锚点坐标是一样的,大家可以通过调试来观察数据对应关系。当然布局时还了考虑margin和decorations(我们常说的分割线)
以上将fill()方法分析完成之后,LinearLayoutManager.onLayoutChildren的核心
已经分析完毕了,最后还有一个layoutForPredictiveAnimations,从该方法的注释来看,是为了动画做一些布局,也不是必须执行的,就不再分析了,如果有读者清楚这块内容的,希望能告知我一下。
至此,LinearLayoutManager.onLayoutChildren分析完毕,但是该方法注释的最后一条,贴一下原文 4) scroll to fulfill requirements like stack from bottom.我并没有看到它体现在哪,可能是上文省略的一些细节中包含,总之这一点我并不明白,如果有读者清楚,希望能告知我一下。
总结
RecyclerView的绘制流程我们分析完了,总结一下
onMeasure跟LayoutManager是否开启自动测量是有关系的,如果支持自动测量的话,可能会进行预布局,默认实现的三个LayoutManager都是支持自动测量的,如果自定义LayoutManager的话要注意这一点onLayout中主要是dispatchLayoutStep1()、dispatchLayoutStep1()、dispatchLayoutStep1()这三个方法按顺序调用,第一个和第三个主要处理了动画相关,第二个将布局的任务交给LayoutManagerdraw和onDraw调用了ItemDecoration中的方法,我们实现这些方法来自定义分割线
最后,由于作者水平有限,如果以上分析有出错的地方,欢迎提出,我及时进行改正,以免误导其他人
相关资料