系列文章:
前言
RecyclerView
是Google在2014年的IO大会中提出来的,可以认为是用来代替ListView的,其是support-v7
包中的组件,但随着AndroidX
(谷歌对 android.support.xxx
包的整理产物,因为之前support包的管理较为混乱,所以谷歌推出了AndroidX)的出现,我们需要将项目中对其的使用迁移到 AndroidX
上。RecyclerView的继承关系如下图所示:
相比ListView的两级缓存,RecyclerView做到了四级缓存,而且整体上的架构做到了解耦,每个模块分别负责不同的功能实现。其中Adapter负责提供数据,包括创建ViewHolder和绑定数据,LayoutManager负责ItemView的测量和布局,ItemAnimator负责每个ItemView的动画,这样功能性的解耦让RecyclerView使用十分方便,也方便了开发者扩展。这里需要提一下ViewHolder
,对于Adapter来说,一个ViewHolder就对应一个data。
有关RecyclerView的使用这里也不做过多分析,具体可以参考Android RecyclerView 使用完全解析。
绘制过程
类似ListView
,RecyclerView
本质上还是一个View,因此在绘制的过程中还是分为以下三步:onMeasure、onLayout、onDraw,下面先看下其onMeasure
方法:
绘制第一步:onMeasure方法
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
// 情况1
}
if (mLayout.mAutoMeasure) {
// 情况2
} else {
// 情况3
}
onMeasure方法中主要是分了三种情况,情况1是mLayout(LayoutManager对象)为null,LayoutManager负责控制 RecyclerView 中 item 的测量和布局,当LayoutManager为空时,RecyclerView是不能显示任何数据的(原因后续解释)。
另外两种情况是mLayout对象不为空,第二种情况是LayoutManager开启了自动测量,第三种情况是LayoutManager没有开启自动测量。
onMeasure情况1:LayoutManager为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);
}
这种情况下直接调用了defaultOnMeasure
方法,该方法中通过LayoutManager.choose()
方法来计算宽高值,然后调用setMeasuredDimension()设置宽高:
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);
}
}
chooseSize方法就是通过RecyclerView的不同测量模式来选取不同的值。
onMeasure情况2:LayoutManager开启了自动测量
if (mLayout.mAutoMeasure) {
// 先获取测量模式
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
// 是否可以跳过测量
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
// 调用LayoutManager.onMeasure方法测量
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
// 如果可以跳过测量或者adapter为null,则直接返回
if (skipMeasure || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// if RecyclerView has non-exact width and height and if there is at least one child
// which also has non-exact width & height, we have to re-measure.
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
// now we can get the width and height from the children.
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
}
这种情况下,首先调用LayoutManager.onMeasure
方法测量,但是Android官方的三种LayoutManager(LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager)都没有复写此方法,此方法源码:
public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
mRecyclerView.defaultOnMeasure(widthSpec, heightSpec);
}
可以看到LayoutManager.onMeasure
方法默认是调用的RecyclerView的defaultOnMeasure
方法,前面已经介绍过此方法了。
接下来判断mState.mLayoutStep
这个变量,即当前绘制状态,如果为State.STEP_START
,那么便会执行dispatchLayoutStep1
方法,随后又调用了dispatchLayoutStep2
方法,最后如果需要二次测量的话,那么会再调用一次dispatchLayoutStep2
方法。
ViewGroup的onMeasure方法的作用通常来说有两个:一是测量自身的宽高,从RecyclerView来看,它将自己的测量工作托管给了LayoutManager的onMeasure方法。所以,我们在自定义LayoutManager时,需要注意onMeasure方法的存在,不过官方提供的几个LayoutManager,都没有重写这个方法。二是测量子View的宽高,不过到目前为止我们还没有看到具体的实现。
前面提到了mState.mLayoutStep
这个变量,mState
是RecyclerView中State类
的对象,mLayoutStep
有三个状态,其三个状态正好和三个dispatchLayoutStep
方法(还有一个dispatchLayoutStep3)一一对应:
State.mLayoutStep | dispatchLayoutStep | 含义说明 |
---|---|---|
STEP_START | dispatchLayoutStep1 |
STEP_START 是State.mLayoutStep 的默认值,执行完dispatchLayoutStep1 后会将该状态置为STEP_LAYOUT
|
STEP_LAYOUT | dispatchLayoutStep2 | 表明处于layout阶段,调用dispatchLayoutStep2 对RecyclerView的子view进行layout,执行完之后会将该状态置为STEP_ANIMATIONS
|
STEP_ANIMATIONS | dispatchLayoutStep3 | 表明处于执行动画阶段,调用dispatchLayoutStep3 之后会将该状态置再次变为STEP_START
|
dispatchLayoutStep1方法
RecyclerView处于State.STEP_START
状态时,会调用dispatchLayoutStep1
方法,其源码如下:
private void dispatchLayoutStep1() {
...
processAdapterUpdatesAndSetAnimationFlags();
...
if (mState.mRunSimpleAnimations) {
// Step 0:找出所有未移除的ItemView,进行预布局
}
if (mState.mRunPredictiveAnimations) {
// Step 1:预布局
} else {
clearOldPositions();
}
onExitLayoutOrScroll();
resumeRequestLayout(false);
mState.mLayoutStep = State.STEP_LAYOUT;
}
可以看到dispatchLayoutStep1
方法主要是根据mState.mRunSimpleAnimations
和mState.mRunPredictiveAnimations
两个值做出相应逻辑处理,而processAdapterUpdatesAndSetAnimationFlags()
方法中计算了这两个值:
private void processAdapterUpdatesAndSetAnimationFlags() {
...
mState.mRunSimpleAnimations = mFirstLayoutComplete && mItemAnimator != null &&
(mDataSetHasChangedAfterLayout || animationTypeSupported ||
mLayout.mRequestedSimpleAnimations) &&
(!mDataSetHasChangedAfterLayout || mAdapter.hasStableIds());
mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations &&
animationTypeSupported && !mDataSetHasChangedAfterLayout &&
predictiveItemAnimationsEnabled();
}
此方法中我们注意下mFirstLayoutComplete
变量,mRunSimpleAnimations和mFirstLayoutComplete
有关,而mRunPredictiveAnimations
又和mRunSimpleAnimations
有关,第一次绘制流程还未完成,mFirstLayoutComplete 为false,因此mRunSimpleAnimations 和mRunPredictiveAnimations都为false,所以不会加载动画,这也是很明显的道理,布局还未加载完成,怎么会进行加载动画呢。
dispatchLayoutStep1
方法的最后一句是mState.mLayoutStep = State.STEP_LAYOUT,可见执行完之后其改变了当前绘制状态。
dispatchLayoutStep1
的其余具体逻辑和ItemAnimator
有关,后续分析ItemAnimator
再详细说明。
dispatchLayoutStep2方法
onMeasure方法执行完dispatchLayoutStep1后,接着执行dispatchLayoutStep2
方法,其中对RecyclerView的子View进行了layout:
private void dispatchLayoutStep2() {
...
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
...
mState.mLayoutStep = State.STEP_ANIMATIONS;
...
}
此方法我们重点关注mLayout.onLayoutChildren(mRecycler, mState)
这句话,onLayoutChildren 这个函数由 LayoutManager的子类实现,主要是决定了子View的布局方式,具体的相应代码逻辑可以查看Android官方LayoutManager
之一LinearLayoutManager
,后续我们再详细说明,此方法最后又将mState.mLayoutStep置为State.STEP_ANIMATIONS。
onMeasure情况3:LayoutManager没有开启自动测量
最后再来看看LayoutManager
没有开启自动测量的情况:
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
eatRequestLayout();
processAdapterUpdatesAndSetAnimationFlags();
if (mState.mRunPredictiveAnimations) {
mState.mInPreLayout = true;
} else {
// consume remaining updates to provide a consistent state with the layout pass.
mAdapterHelper.consumeUpdatesInOnePass();
mState.mInPreLayout = false;
}
mAdapterUpdateDuringMeasure = false;
resumeRequestLayout(false);
}
if (mAdapter != null) {
mState.mItemCount = mAdapter.getItemCount();
} else {
mState.mItemCount = 0;
}
eatRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
resumeRequestLayout(false);
mState.mInPreLayout = false; // clear
如果mHasFixedSize为true
(setHasFixedSize方法可以设置此变量),就直接调用LayoutManager.onMeasure
方法进行测量;如果mHasFixedSize为false
,则先判断是否有数据更新(mAdapterUpdateDuringMeasure变量),有的话先处理数据更新,再调用LayoutManager.onMeasure方法进行测量。
绘制第二步:onLayout方法
测量过程之后便是layout过程,onLayout
方法比较简单,只有下面几行,最重要的逻辑都在dispatchLayout
方法中了:
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");
// leave the state in START
return;
}
if (mLayout == null) {
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
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();
}
dispatchLayout()
方法也简洁明了,首先分别判断了mAdapter和mLayout,只要其中一个为null,则直接返回;这里需要注意到当mLayout为null时,即RecyclerView没有设置LayoutManager时,dispatchLayout方法直接返回了,因此不会处理layout过程,自然也解释了为什么不设置LayoutManager,RecyclerView就不会加载数据。
dispatchLayout()
方法中保证了RecyclerView必须经历三个过程,分别是dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3,前两个方法我们在onMeasure过程中已经提到了,下面看下dispatchLayoutStep3
源码:
private void dispatchLayoutStep3() {
...
mState.mLayoutStep = State.STEP_START;
...
}
它重新将mState.mLayoutStep
的状态置为State.STEP_START
,保证了第二次layout时仍会执行dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3三个方法,剩下的工作主要是和Item动画相关的,和ItemAnimator
有关。
如果在RecyclerView中如果开启了自动测量,在measure阶段就已经将子View布局完成了,如果没有开启自动测量,那么会在layout阶段再布局子View。
绘制第三步: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);
}
// TODO If padding is not 0 and clipChildrenToPadding is false, to draw glows properly, we
// need find children closest to edges. Not sure if it is worth the effort.
......
}
draw大概可以分为三步:
- 调用super.draw,将子View的绘制分发给View类;在View类draw方法中又会回调RecyclerView的onDraw方法:
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);
}
}
此方法中又将分割线的绘制分发给ItemDecoration的onDraw方法。
- 调用ItemDecoration的onDrawOver方法。通过这个方法,我们在每个Item上自定义一些装饰。
- 如果RecyclerView调用了setClipToPadding,会实现一种特殊的滑动效果--每个ItemView可以滑动到padding区域。
绘制流程补充举例:LinearLayoutManager.onLayoutChildren方法
在讲解ListView
的绘制过程中,我们的重心就是layoutChildren
方法,讲解了怎么对子View布局,到现在为止我们还没有进入RecyclerView对子View布局的讲解,前面描述dispatchLayoutStep2过程中,我们提到了onLayoutChildren 这个函数由LayoutManager的子类实现,那么下面我们就以LayoutManager的子类LinearLayoutManager为例,介绍下onLayoutChildren
的工作过程:
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.
// create layout state
...
// 第一步
resolveShouldLayoutReverse();
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION ||
mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// 计算锚点位置和坐标
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}
...
// 第二步
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
detachAndScrapAttachedViews(recycler);
mLayoutState.mInfinite = resolveIsInfinite();
mLayoutState.mIsPreLayout = state.isPreLayout();
// 第三步
if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
...
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
...
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
...
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
...
}
...
}
子方法的开头注释说了整个大概的流程:
- 1、确定锚点信息,找到一个锚点坐标与锚点(如果是在线性布局中,相当于找到当前界面内第一个View,与第一个view的坐标点)
- 2、根据锚点信息,进行填充
- 3、填充完后,如果还有剩余的可填充大小,再填充一次
第一步:确定锚点信息
onLayoutChildren
方法中大概做了三件事,第一步是确定锚点信息,首先执行resolveShouldLayoutReverse()
方法判断是否需要倒着绘制:
private void resolveShouldLayoutReverse() {
if (mOrientation == VERTICAL || !isLayoutRTL()) {
mShouldReverseLayout = mReverseLayout;
} else {
mShouldReverseLayout = !mReverseLayout;
}
}
默认情况下mReverseLayout
为false,是不会倒着绘制的。手动调用setReverseLayout()
方法,可以改变mReverseLayout
的值:
public void setReverseLayout(boolean reverseLayout) {
assertNotInLayoutOrScroll(null);
if (reverseLayout == mReverseLayout) {
return;
}
mReverseLayout = reverseLayout;
requestLayout();
}
接下来便是通过updateAnchorInfoForLayout()
方法来计算锚点信息,这里对锚点做一些解释,mAnchorInfo(AnchorInfo类对象)就是我们要的锚点:
AnchorInfo(锚点)
class AnchorInfo {
int mPosition;
int mCoordinate;
boolean mLayoutFromEnd;
boolean mValid;
...
}
AnchorInfo
中有四个重要的成员变量,mPositionm和mCoordinate
易懂;mValid的默认值是false,一次测量之后设为true,onLayout完成后会回调执行reset方法,又变为false;mLayoutFromEnd
变量在计算锚点过程中有如下赋值:
mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd
mShouldReverseLayout
默认是false,mStackFromEnd
默认是false,除非手动调用setStackFromEnd()
方法,两个变量都是false,异或运算之后还是false。
接下来又调用了updateAnchorInfoForLayout()
方法,此方法用来更新锚点信息,一共有三种计算方法:
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
AnchorInfo anchorInfo) {
// 第一种计算方法
if (updateAnchorFromPendingData(state, anchorInfo)) {
return;
}
// 第二种计算方法
if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
return;
}
// 第三种计算方法
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
- 第一种计算方法:如果存在未决的滚动位置或保存的状态,从该数据更新锚点信息并返回true
- 第二种计算方法:根据子View来更新锚点信息,如果一个子View有焦点,则根据其来计算锚点信息;如有一个子View没有锚点,则根据布局方向选取第一个View或最后一个View
- 第三种计算方法:前两种都未采用,则采取默认的第三种计算方式
第二步:回收子View
第二步是调用detachAndScrapAttachedViews()
方法对所有的ItemView进行回收,这部分的内容属于RecyclerView缓存机制的部分,后面解释缓存的时候再说。
第三步:子View填充(fill)
第三步是便是子View的内容填充了,首先是根据mAnchorInfo.mLayoutFromEnd来判断是否逆向填充,无论是正向还是逆向,都调用了至少两次fill()方法来进行填充;如果是正向填充的话先向下填充,再向上填充;逆向的话和正向相反,两次fill之后,如果还有剩余空间还会再调用一次fill进行补充。我们来结合一张图了解下锚点和fill之间的关系:
下面我们来看看fill
方法:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
}
...
}
fill中真正填充子View的方法是layoutChunk(),再来看看layoutChunk()方法,源码如下:
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 第一步
View view = layoutState.next(recycler);
...
// 第二步
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);
}
}
// 第三步
measureChildWithMargins(view, 0, 0);
...
// 第四步
layoutDecoratedWithMargins(view, left, top, right, bottom);
...
}
layoutChunk
方法中的主要工作可以分为以下几步:
- 1.调用LayoutState的next方法获得一个ItemView,next方法的参数是Recycler对象,Recycler正是RecyclerView的缓存核心实现,可见RecyclerView中的缓存机制从此处开始,后续分析缓存时再来具体查看
- 2.如果RecyclerView是第一次布局children(
layoutState.mScrapList == null
),会调用addView()方法将View添加到RecyclerView中 - 3.调用
measureChildWithMargins
方法,测量每个ItemView的宽高,这里考虑了margin属性和ItemDecoration的offset - 4.调用了
layoutDecoratedWithMargins
对子View完成了布局
其中有很多细节并未展开描述,后续分析缓存机制时再进行讲解。
总结
- RecyclerView的measure过程分为三种情况,每种情况都有执行过程,一般情况下会走自动测量流程
- 自动测量根据
mState.mLayoutStep
状态值,调用不同的dispatchLayoutStep
方法 - layout过程也根据
mState.mLayoutStep
状态来调用不同的dispatchLayoutStep
方法 - draw过程大概可以分为三步
- 布局子View先获取锚点信息,再根据锚点信息和布局方向进行子View填充
至此基本完成了RecyclerView三大绘制流程,还有最重要的缓存机制没有讲解,见后续。