深入 RecyclerView 源码探究二:测量和布局

文章目录
  1. 1. 测量过程
  2. 2. 布局过程
  3. 3. 小结
  4. 4. 参考文献

Life is not about how to live through the storm, but how to dance in the rain.

RecyclerView 的基本使用,只需要提供一个 Adapter 处理数据集与 itemView 的绑定和一个 LayoutManager 测量并给 itemView 布局。本篇在深入 RecyclerView 源码探究一:宏观设计的基础上继续展开,分析 RecyclerView 的测量和布局过程。

RecyclerView 将其测量与布局过程委托给 LayoutManager 处理,且其对子控件的测量与布局是逐个处理的,即执行完一个子控件的测量与布局,再去执行下一个。以下分两部分来看:

测量过程

首先看测量过程,结合源码,简单呈现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
// 1. 没有 LayoutManager
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.mAutoMeasure) {
// 2. 有 LayoutManager,开启自动测量
} else {
// 3. 有 LayoutManager,未开启自动测量
}
}

综上所示,onMeasure() 方法内部分为三种情况:

  • 没有 LayoutManager;
  • 有 LayoutManager,开启自动测量;
  • 有 LayoutManager,未开启自动测量。

三种情况分别详细分析如下:

I. 没有 LayoutManager

执行 defaultOnMeasure() 方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
void defaultOnMeasure(int widthSpec, int heightSpec) {
// calling LayoutManager here is not pretty but that API is already public and it is
// better than creating another method since this is internal.
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);
}

在 onMeasure() 被调用且 layout manager 设置之前使用,计算并设置了 RecyclerView 的长宽值,其调用了 chooseSize() 方法的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
}

直接根据测量值和模式返回最适合的尺寸大小。

II. 有 LayoutManager,开启自动测量

RecyclerView 的 23.2.0 版本前,为其设置 wrap_content 值,其中的内容改变时,RecyclerView 并不能改变大小以适应内部的内容,直到后来加入了自动测量机制解决这个问题。如今常用的三个 LayoutManager 构造函数中,都设置开启了自动测量,故可以自由地为 RecyclerView 设置 wrap_content 值。自动测量部分的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
// ...
if (mLayout.mAutoMeasure) {
// 获取模式,执行 onMeasure() 方法
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
final boolean skipMeasure = widthMode == MeasureSpec.EXACTLY
&& heightMode == MeasureSpec.EXACTLY;
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
if (skipMeasure || mAdapter == null) {
return;
}
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
// set dimensions in 2nd step. Pre-layout should happen with old dimensions for
// consistency.
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();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
// ...
}
}

首先,确定宽高模式。然后,执行 LayoutManager 的 onMeasure() 方法。接着,开启布局流程,计算出所有 Child 的边界,由该值计算并设置出 RecyclerView 所需的 width 和 height。最后,检查是否需要再次测量,若 RecyclerView 仍有非精确的宽和高,并且至少有一个 Child 也有非精确的宽和高,则需要重新测量。注意,此自动绘制过程中执行了布局流程,之后布局时会检查是否进行过,若有,则跳过执行过的布局流程,不重复操作。

III. 有 LayoutManager,未开启自动测量

我们使用系统提供的三种 LayoutManager 时,默认是开启自动测量的,除了初始化 LayoutManager 后,设置方法setAutoMeasureEnable(false),这样才会走到非自动测量流程。若开发者自定义 LayoutManager,且未在初始化时开启自动测量,则会走到非自动测量流程。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
// ...
if (mLayout.mAutoMeasure) {
// ...
} else {
// 若 RecyclerView 已经设置了固定 Size,则执行 LayoutManager 的 onMeasure() 方法
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// 若在测量的过程中有数据有更新,则先处理更新的数据
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
}
}

若尺寸固定,则直接执行 LayoutManager 的 onMeasure() 方法;若尺寸不固定,测量的过程中,有数据更新先处理更新的数据,最后才会调用 onMeasure() 方法。

总之,无论是否启用 mAutoMeasure,都会执行到 LayoutManager 的 onMeasure() 方法。

布局过程

其次,再来看看布局过程,源码如下:

1
2
3
4
5
6
7
@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;
}

很简单,进入 diapatchLayout() 方法里看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void dispatchLayout() {
// ...
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();
}

忽略各种判断条件,明显看出 RecyclerView 的布局过程有三步,dispatchLayout() 方法中分了三种情况处理:

  • 未执行过布局流程
  • 执行过布局流程,而后 size 产生变化
  • 执行过布局流程,可直接同步之前的数据

无论如何,都会完成 dispatchLayoutStep1、dispatchLayoutStep2 和 dispatchLayoutStep3 这三步,区分开来是为了避免重复计算。三步分别分析如下:

I. dispatchLayoutStep1

这一步里,会处理 Adapter 的更新,决定播放的动画,保存当前 View 的信息和必要的情况下,进行下一布局操作并保存其信息。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
private void dispatchLayoutStep1() {
// 省略代码部分为判断状态、更改状态以及保存一些信息
// ...
// 处理 Adapter 的更新,计算动画的类型
// ...
if (mState.mRunSimpleAnimations) {

// Step 0: Find out where all non-removed items are, pre-layout
// 运行动画时,找出需要进行上一布局操作的 ViewHolder,保存其边界信息,若有内容的更新,即非插入删 除的更新,则保存这些更新的 ViewHolder
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
final ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
// ...
final ItemHolderInfo animationInfo = mItemAnimator
.recordPreLayoutInformation(mState, holder,
ItemAnimator.buildAdapterChangeFlagsForAnimations(holder),
holder.getUnmodifiedPayloads());

// ...
// 在这里,ViewHolder 添加到旧的变化 holders list 中,但不是唯一的地方,还有另外一种 case:
// * ViewHolder 当前被隐藏但并未删除
// * Adapter 里隐藏的 item 发生变化
// * Layout manager 决定以先前传过来的布局形式给 item 布局
// 当这样的 case 得知存在时,RecyclerView 将展示 view,并添加其到旧的变化 holders
// list 中
mViewInfoStore.addToOldChangeHolders(key, holder);
}
}
if (mState.mRunPredictiveAnimations) {

// Step 1: 执行先前的布局:这里会使用 items 旧的位置。Layout manager 应该给每样布局,甚至
// 是删除的 items (尽管未向 container 中添加回删除的 items)。这使得看得见的 views 先前布局的位置以部分真实布局存在

// Save old positions so that LayoutManager can run its mapping logic.
saveOldPositions();
final boolean didStructureChange = mState.mStructureChanged;
mState.mStructureChanged = false;
// temporarily disable flag because we are asking for previous layout
mLayout.onLayoutChildren(mRecycler, mState);
// ...
// we don't process disappearing list because they may re-appear in post layout pass.
clearOldPositions();
} else {
clearOldPositions();
}
onExitLayoutOrScroll();
resumeRequestLayout(false);
mState.mLayoutStep = State.STEP_LAYOUT;
}

II. dispatchLayoutStep2

在这里,执行最终状态的真正布局,这一步在必要的情况下会执行多次。

1
2
3
4
5
6
7
8
9
10
11
12
private void dispatchLayoutStep2() {
// ...
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
// ...
// onLayoutChildren may have caused client code to disable item animations; re-check
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
resumeRequestLayout(false);
}

注意到 dispatchLayoutStep1 和 dispatchLayoutStep2 中都调用到了 onLayoutChildren() 方法,兼容包里,提供了三种 LayoutManager 的实现,这里以常用的 LinearLayoutManager 来举例说明,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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.
// ...
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// calculate anchor position and coordinate
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
// ...
if (mAnchorInfo.mLayoutFromEnd) {
// ...
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
final int lastElement = mLayoutState.mCurrentPosition;
if (mLayoutState.mAvailable > 0) {
extraForStart += mLayoutState.mAvailable;
}
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
// ...
}
// ...
}

以垂直布局来说明,mAnchorInfo 为布局锚点信息,包括子控件在 Y 轴上起始绘制偏移量 (coordinate),itemView 在 Adapter 中的索引位置 (position) 和布局方向 (mLayoutFromEnd),该部分代码的功能即确定布局锚点,以此为起点,然后向开始和结束方向填充 itemView。结构图如下:

上段代码中,fill() 方法的作用就是填充 itemView,结构图中,图三即为 fill() 方法调用两次的情况。其中,图三更为普遍,也是 RecyclerView 实现填充 itemView 算法时的情况。但是,mAnchorInfo 在赋值过程 (updateAnchorInfoForLayout) 中,只会出现从左往右数图一和图二的情况。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable)
{

// ...
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = new LayoutChunkResult();
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
// ...
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// ...
// 若如下,会消耗可利用的空间:
// 1. layoutChunk 没要求被忽略
// 2. 布局时破坏子控件
// 3. 未做预先布局
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;
}
if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
}
// ...
}

走进 layoutChunk() 方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
}
}
// ...
measureChildWithMargins(view, 0, 0);
// ...
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
layoutDecorated(view, left + params.leftMargin, top + params.topMargin,
right - params.rightMargin, bottom - params.bottomMargin);
// ...
}

addView() 即 ViewGroup 里的 addView() 方法,measureChildWithMargins() 即用来测量子控件的大小,进入 layoutDecorated() 方法,源码如下:

1
2
3
4
5
public void layoutDecorated(View child, int left, int top, int right, int bottom) {
final Rect insets = ((RecyclerView.LayoutParams)child.getLayoutParams()).mDecorInsets;
child.layout(left + insets.left, top + insets.top, right - insets.right,
bottom - insets.bottom);
}

在 RecyclerView 使用的坐标系里,其包括任何现有的 ItemDecoration,为给定的子 View 布局。

III. dispatchLayoutStep3

第二步分析了很多,来看第三步。这一步要做的主要是:保存动画 views 的信息,触发动画和清除必要的垃圾。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void dispatchLayoutStep3() {
// 设置状态
// ...
if (mState.mRunSimpleAnimations) {
// 需要动画的情况,找出 ViewHolder 现在的位置,并且处理改变动画,最后触发动画
// ...
final ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);

// ...
// 执行变化的动画
// 若 item 变化了,但是其更新版本消失,则会创建一种有冲突的情况
// 由于被标记为消失的 view 很可能会滚出边界,执行变化的动画。所有的 views 一旦在动画完成时,都会被自动清除
// ...
final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
oldChangeViewHolder);
// we add an remove so that any post info is merged.
mViewInfoStore.addToPostLayout(holder, animationInfo);
ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
}
// 清除状态和无用的信息,最后再恢复一些信息,如焦点等
// ...
}

小结

1.在 RecyclerView 的测量和布局阶段,填充 itemView 的算法为:向父容器中添加子控件;测量子控件的大小,执行 setMeasuredDimensionFromChildren() 方法,由子控件的大小确定 RecyclerView 的大小;给子控件布局,给锚点布局并向当前布局方向平移子控件大小,重复直至 RecyclerView 的可绘制空间消耗完或者子控件填充完毕。其中,可绘制空间即父容器对 RecyclerView 布局大小的要求,通过 MeasureSpec.getSize() 方法获取。

2.由 onMeasure() 方法以及对 skipMeasure 的判断,若支持 WRAP_CONTENT,则子控件的测量和布局会在 RecyclerView 的测量方法中提前执行完成,即先确定子控件的大小和位置,再设置 RecyclerView 的大小;若是其他情况,如测量模式为 EXACTLY,子控件的测量和布局会延迟到 RecyclerView 的布局 (onLayout()) 过程中执行。

3.RecyclerView 本身并未对内部的 View 执行布局,而是交给 LayoutManager 做具体的布局操作,此外,开发者可以通过实现重写 onLayoutChildren() 方法,自定义 LayoutManager。

4.RecyclerView 的布局过程中,有着很多动画相关的处理,其数据改变的动画是在布局过程的第三步中统一触发,而不是在一调用 notify 之后就立马触发的。

至此,关于 RecyclerView 的测量和布局过程探究完毕,后续将从绘制和滑动机制方面展开探究。

本人才疏学浅,如有疏漏错误之处,望读者中有识之士不吝赐教,谢谢。

1
Email: [email protected] / WeChat: Wolverine623

您也可以关注我个人的微信公众号码农六哥第一时间获得博客的更新通知,或后台留言与我交流

参考文献

1.http://blog.csdn.net/qq_23012315/article/details/50807224

2.http://www.jianshu.com/p/898479f103b6