No title

RecyclerView渲染流程

image-20201027104137833

RecyclerView继承自ViewGroup,其展示流程入口就还得是onMeasure、onLayout方法,下面将从这两个方法作为入口,探究RecyclerView展示的原理。

recyclerView-onMeasure

image-20201027104712356

onMeasure实现原理

onMeasure方法的代码很长,这里放上一段精简过的伪代码:

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.isAutoMeasureEnabled()) {
···
//调用LayoutManager的onMeasure方法来进行测量工作
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
···
//如果width和height都已经是精确值,那么就不用再根据内容进行测量,后面步骤不再处理
if (skipMeasure || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
//布局的第一部 主要进行一些初始化的工作
dispatchLayoutStep1();
}
...
//开启了自动测量,需要先确认子View的大小与布局
dispatchLayoutStep2();

//再根据子View的情况决定自身的大小
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

if (mLayout.shouldMeasureTwice()) {
...
//如果有父子尺寸属性互相依赖的情况,要改变参数重新进行一次
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
···
} else if (mState.mRunPredictiveAnimations) {
setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
return;
}
···
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
···
}
}

这里的mLayout对象是LayoutManager,measure过程主要分为了这样几种情况:

1、LayoutManager为空,使用默认测量方案通过defaultOnMeasure方法完成

2、有LayoutManager,开启了自动测量。

这种情况是最复杂的,需要根据子View的布局来调整自身的大小。需要知道子View的大小和布局。所以RecyclerView将布局的过程提前到这里来进行了

3、有LayoutManager,而且关闭了自动测量功能。

关闭测量的情况下不需要考虑子View的大小和布局。直接按照正常的流程来进行测量即可。如果直接已经设置了固定的宽高,那么直接使用固定值即可。如果没有设置固定宽高,那么就按照正常的控件一样,根据父级的要求与自身的属性进行测量

defaultOnMeasure实现

image-20201027111253599

chooseSize方法中根据measureMode来确认RecyclerView的宽高,然后调用setMeasuredDimension方法设置测量的宽高。这里并没有对子View进行measure,也就是说在LayoutManager为空的情况下,测量的宽高是不含item的,显示出的效果就是空白,这也能很好的解释当你忘记调用setLayoutManager方法的时候,并不能符合预期的展示而是一片空白。

LayoutManager自动测量

自动测量的原理如下:

当RecyclerView的宽高都为EXACTLY时,可以直接设置对应的宽高,然后返回,结束测量。如果宽高的测量规则不是EXACTLY的,则会在onMeasure()中开始布局的处理,这里首先要介绍一个很重要的类:RecyclerView.State ,这个类封装了当前RecyclerView的有用信息。State的一个变量mLayoutStep表示了RecyclerView当前的布局状态,包括STEP_START、STEP_LAYOUT 、 STEP_ANIMATIONS三个,而RecyclerView的布局过程也分为三步,其中,STEP_START表示即将开始布局,需要调用dispatchLayoutStep1来执行第一步布局,接下来,布局状态变为STEP_LAYOUT,表示接下来需要调用dispatchLayoutStep2里进行第二步布局,同理,第二步布局后状态变为STEP_ANIMATIONS,需要执行第三步布局dispatchLayoutStep3。

这三个步骤的工作也各不相同,step1负责记录状态,step2负责布局,step3则与step1进行比较,根据变化来触发动画.在开启自动测量的情况下如果没有设置固定宽高,那么会执行setp1和step2。在step2执行完后就可以调用 setMeasuredDimensionFromChildren 方法,根据子类的测量布局结果来设置自身的大小

先不进行分析step1,step2和step3的具体功能。直接把 onLayout 的代码也贴出来,看一下这3步是如何保证都能够执行的

//RecyclerView.java
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
dispatchLayout();
}

void dispatchLayout() {
if (mAdapter == null) {//没有设置adapter,返回
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
if (mLayout == null) {//没有设置LayoutManager,返回
Log.e(TAG, "No layout manager attached; skipping layout");
// leave the state in START
return;
}
mState.mIsMeasuring = false;
//在onMeasure阶段,如果宽高是固定的,那么mLayoutStep == State.STEP_START 而且dispatchLayoutStep1和dispatchLayoutStep2不会调用
//所以这里就会调用一下
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()|| mLayout.getHeight() != getHeight()) {
//在onMeasure阶段,如果执行了dispatchLayoutStep1,但是没有执行dispatchLayoutStep2,就会执行dispatchLayoutStep2
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
//最终调用dispatchLayoutStep3
dispatchLayoutStep3();
}

可以看到,其实在 onLayout 阶段会根据 onMeasure 阶段3个步骤执行到了哪个,然后会在 onLayout 中把剩下的步骤执行.在这3个步骤中,step2就是执行了子View的测量布局的一步,也是最重要的一环,所以我们将关注的重点放在这个函数

//RecyclerView.java
private void dispatchLayoutStep2() {
//禁止布局请求
eatRequestLayout();
...
mState.mInPreLayout = false;
//调用LayoutManager的layoutChildren方法来布局
mLayout.onLayoutChildren(mRecycler, mState);
...
resumeRequestLayout(false);
}

这里调用LayoutManager的 onLayoutChildren 方法,将对于子View的测量和布局工作交给了LayoutManager。而且我们在自定义LayoutManager的时候也必须要重写这个方法来描述我们的布局错略。这里我们分析最经常使用的 LinearLayoutManager

//LinearLayoutManager.java
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

LinearLayoutManager 中的布局策略主要是以下几个方面:

  1. 通过子控件和其他的变量信息。找到一个锚点和锚点项的位置。
  2. 从锚点的位置开始,往上,填充布局子View,直到填满区域
  3. 从锚点的位置开始,往下,填充布局子View,直到填满区域
  4. 滚动以满足需求,如堆栈从底部

这里有个比较关键的词,就是 锚点(AnchorInfo) ,其实 LinearLayoutManager的布局并不是从上往下一个个进行的。而是很可能从整个布局的中间某个点开始的,然后朝一个方向一个个填充,填满可见区域后,朝另一个方向进行填充。至于先朝哪个方向填充,是根据具体的变量来确定的

锚点的选择

AnchorInfo 类需要能够有效的描述一个具体的位置信息,我们首先类内部的几个重要的成员变量

//LinearLayoutManager.java
//简单的数据类来保存锚点信息
class AnchorInfo {
//锚点参考View在整个数据中的position信息,即它是第几个View
int mPosition;
//锚点的具体坐标信息,填充子View的起始坐标。当positon=0的时候,如果只有一半View可见,那么这个数据可能为负数
int mCoordinate;
//是否从底部开始布局
boolean mLayoutFromEnd;
//是否有效
boolean mValid;

通过 AnchorInfo 就可以准确的定位当前的位置信息了,现在再看看onLayoutChildren中具体的处理:

//LinearLayoutManager.java
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
...
//确认LayoutState存在
ensureLayoutState();
//禁止回收
mLayoutState.mRecycle = false;
//计算是否需要颠倒绘制。是从底部到顶部绘制,还是从顶部到底部绘制
resolveShouldLayoutReverse();
//如果当前锚点信息非法,滑动到的位置不可用或者有需要恢复的存储的SaveState
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION || mPendingSavedState != null) {
//重置锚点信息
mAnchorInfo.reset();
//是否从end开始进行布局。因为mShouldReverseLayout和mStackFromEnd默认都是false,那么我们这里可以考虑按照默认的情况来进行分析,也就是mLayoutFromEnd也是false
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
//计算锚点的位置和坐标
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
//设置锚点有效
mAnchorInfo.mValid = true;
}
}

在需要确定锚点的时候,会先将锚点进行初始化,然后通过 updateAnchorInfoForLayout 方法来确定锚点的信息

//LinearLayoutManager.java
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
//从挂起的数据更新锚点信息 这个方法一般不会调用到
if (updateAnchorFromPendingData(state, anchorInfo)) {
return;
}
//重点方法 从子View来确定锚点信息(这里会尝试从有焦点的子View或者列表第一个位置的View或者最后一个位置的View来确定)
if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
return;
}
//进入这里说明现在都没有确定锚点(比如设置Data后还没有绘制View的情况下),就直接设置RecyclerView的顶部或者底部位置为锚点(按照默认情况,这里的mPosition=0)。
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}

锚点的确定方案主要有3个:

  1. 从挂起的数据获取锚点信息。一般不会执行。
  2. 从子View来确定锚点信息。比如说notifyDataSetChanged方法的时候,屏幕上原来是有View的,那么就会通过这种方式获取
  3. 如果上面两种方法都无法确定,其实就是没有子View让我们作为参考。比如说第一次加载数据的时候,RecyclerView一片空白则直接使用0位置的View作为锚点参考position。

那么当有子View的时候,我们通过 updateAnchorFromChildren 方法来确定锚点位置

//LinearLayoutManager.java
//从现有子View中确定锚定。大多数情况下,是起始或者末尾的有效子View(一般是未移除,展示在我们面前的View)。
private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) {
//没有数据,直接返回false
if (getChildCount() == 0) {
return false;
}
final View focused = getFocusedChild();
//优先选取获得焦点的子View作为锚点
if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
//保持获取焦点的子view的位置信息
anchorInfo.assignFromViewAndKeepVisibleRect(focused);
return true;
}
if (mLastStackFromEnd != mStackFromEnd) {
return false;
}
//根据锚点的设置信息,从底部或者顶部获取子View信息
View referenceChild = anchorInfo.mLayoutFromEnd ? findReferenceChildClosestToEnd(recycler, state) : findReferenceChildClosestToStart(recycler, state);
if (referenceChild != null) {
anchorInfo.assignFromView(referenceChild);
...
return true;
}
return false;
}

通过子View确定锚点坐标也是进行了3种情况的处理

  1. 没有数据,直接返回获取失败
  2. 如果某个子View持有焦点,那么直接把持有焦点的子View作为锚点参考点
  3. 没有子View持有焦点,一般会选择最上(或者最下面)的子View作为锚点参考点

一般情况下,都会使用第三种方案来确定锚点,所以我们这里也主要关注一下这里的方法。按照我们默认的变量信息,这里会通过 findReferenceChildClosestToStart 方法获取可见区域中的第一个子View作为锚点的参考View。然后调用 assignFromView 方法来确定锚点的几个属性值

//LinearLayoutManager.java
public void assignFromView(View child) {
if (mLayoutFromEnd) {
//如果是从底部布局,那么获取child的底部的位置设置为锚点
mCoordinate = mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper.getTotalSpaceChange();
} else {
//如果是从顶部开始布局,那么获取child的顶部的位置设置为锚点坐标(这里要考虑ItemDecorator的情况)
mCoordinate = mOrientationHelper.getDecoratedStart(child);
}
//mPosition赋值为参考View的position
mPosition = getPosition(child);
}

mPostion这个变量很好理解,就是子View的位置值,那么 mCoordinate 是个什么,看getDecoratedStart 是怎么处理的就知道了

//LinearLayoutManager.java
//创建mOrientationHelper。我们按照垂直布局来进行分析
if (mOrientationHelper == null) {
mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
}
//OrientationHelper.java
public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
return new OrientationHelper(layoutManager) {
@Override
public int getDecoratedStart(View view) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)view.getLayoutParams();
return mLayoutManager.getDecoratedTop(view) - params.topMargin;
}
}
}

布局的填充

回到主线 onLayoutChildren 函数。当我们的锚点信息确认以后,剩下的就是从这个位置开始进行布局的填充

if (mAnchorInfo.mLayoutFromEnd) {//从end开始布局
//倒着绘制的话,先从锚点往上,绘制完再从锚点往下
//设置绘制方向信息为从锚点往上
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
final int firstElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForEnd += mLayoutState.mAvailable;
}
//设置绘制方向信息为从锚点往下
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;

if (mLayoutState.mAvailable > 0) {
extraForStart = mLayoutState.mAvailable;
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
} else {//从起始位置开始布局
// 更新layoutState,设置布局方向朝下
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
//开始填充布局
fill(recycler, mLayoutState, state, false);
//结束偏移
endOffset = mLayoutState.mOffset;
//绘制后的最后一个view的position
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
//更新layoutState,设置布局方向朝上
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
//再次填充布局
fill(recycler, mLayoutState, state, false);
//起始位置的偏移
startOffset = mLayoutState.mOffset;

if (mLayoutState.mAvailable > 0) {
extraForEnd = mLayoutState.mAvailable;
updateLayoutStateToFillEnd(lastElement, endOffset);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
}
}

可以看到,根据不同的绘制方向,这里面做了不同的处理,只是填充的方向相反而已,具体的步骤是相似的。都是从锚点开始往一个方向进行View的填充,填充满以后再朝另一个方向填充。填充子View使用的是 fill() 方法

//在LinearLayoutManager中,进行界面重绘和进行滑动两种情况下,往屏幕上填充子View的工作都是调用fill()进行
int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
//可用区域的像素数
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
//将滑出屏幕的View回收掉
recycleByLayoutState(recycler, layoutState);
}
//剩余绘制空间=可用区域+扩展空间。
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
//循环布局直到没有剩余空间了或者没有剩余数据了
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
//初始化layoutChunkResult
layoutChunkResult.resetInternal();
//重点方法 添加一个child,然后将绘制的相关信息保存到layoutChunkResult
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (layoutChunkResult.mFinished) {//如果布局结束了(没有view了),退出循环
break;
}
//根据所添加的child消费的高度更新layoutState的偏移量。mLayoutDirection为+1或者-1,通过乘法来处理是从底部往上布局,还是从上往底部开始布局
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null || !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
//消费剩余可用空间
remainingSpace -= layoutChunkResult.mConsumed;
}
...
}
//返回本次布局所填充的区域
return start - layoutState.mAvailable;
}

fill 方法中,会判断当前的是否还有剩余区域可以进行子View的填充。如果没有剩余区域或者没有子View,那么就返回。否则就通过 layoutChunk 来进行填充工作,填充完毕以后更新当前的可用区域,然后依次遍历循环,直到不满足条件为止

循环中的填充是通过 layoutChunk 来实现的

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
//通过缓存获取当前position所需要展示的ViewHolder的View
View view = layoutState.next(recycler);
if (view == null) {
//如果我们将视图放置在废弃视图中,这可能会返回null,这意味着没有更多的项需要布局。
result.mFinished = true;
return;
}
LayoutParams params = (LayoutParams) view.getLayoutParams();
if (layoutState.mScrapList == null) {
//根据方向调用addView方法添加子View
if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) {
//这里是即将消失的View,但是需要设置对应的移除动画
addDisappearingView(view);
} else {
addDisappearingView(view, 0);
}
}
//调用measure测量view。这里会考虑到父类的padding
measureChildWithMargins(view, 0, 0);
//将本次子View消费的区域设置为子view的高(或者宽)
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
//找到view的四个边角位置
int left, top, right, bottom;
...
//调用child.layout方法进行布局(这里会考虑到view的ItemDecorator等信息)
layoutDecoratedWithMargins(view, left, top, right, bottom);
//如果视图未被删除或更改,则使用可用空间
if (params.isItemRemoved() || params.isItemChanged()) {
result.mIgnoreConsumed = true;
}
result.mFocusable = view.isFocusable();
}

这里主要做了5个处理:

  1. 通过 layoutState 获取要展示的View
  2. 通过 addView 方法将子View添加到布局中
  3. 调用 measureChildWithMargins 方法测量子View
  4. 调用 layoutDecoratedWithMargins 方法布局子View
  5. 根据处理的结果,填充LayoutChunkResult的相关信息,以便返回之后,能够进行数据的计算

如果只是考虑第一次数据加载,那么到目前为止,我们的整个页面通过两次 fill 就能够将整个屏幕填充完毕了

Author: zhf
Link: http://yoursite.com/2020/10/27/RecyclerView渲染流程/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
  • 支付寶