RecycleView的绘制流程
RecycleView继承自ViewGroup,绘制流程肯定也是遵循View的,测量(onMeasure),布局(onLayout),绘制(onDdraw)三大流程。所以从这三个地方开始查看,本篇是27.1.1版本的源码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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82protected void onMeasure(int widthSpec, int heightSpec) {
//mLayout是LayoutManager如果为null,就走默认测量然后返回
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
//是否自动测量,比如常用的LinearLayoutManager和GridLayoutManager中默认直接返回true
if (mLayout.isAutoMeasureEnabled()) {
//获取长宽的测量规格
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
//内部还是调用了mRecyclerView.defaultOnMeasure走默认测量
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
//判断宽高的测量模式是不是精确测量
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
//如果测量模式是精确值比如match_partent,写死的值或者adapter是null,结束测量
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
如果测量步骤是开始
if (mState.mLayoutStep == State.STEP_START) {
//布局的第一步,更新适配器,决定运行哪个动画,保存有关当前视图的信息,如果有必要,运行预测布局并保存其信息。
dispatchLayoutStep1();
}
// 在第二步设置尺寸,预布局和旧尺寸要一致
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
dispatchLayoutStep2();
//现在可以从子元素中得到宽和高
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// 如果RecyclerView 没有精确的高度和宽度,并且只有一个孩子
// 我们需要重新测量
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);
}
} else {
//如果子view的大小不影响recycleview的大小
if (mHasFixedSize) {
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
return;
}
// 自定义测量
if (mAdapterUpdateDuringMeasure) {
startInterceptRequestLayout();
onEnterLayoutOrScroll();
processAdapterUpdatesAndSetAnimationFlags();
onExitLayoutOrScroll();
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;
stopInterceptRequestLayout(false);
} else if (mState.mRunPredictiveAnimations) {
setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
return;
}
if (mAdapter != null) {
mState.mItemCount = mAdapter.getItemCount();
} else {
mState.mItemCount = 0;
}
startInterceptRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
stopInterceptRequestLayout(false);
mState.mInPreLayout = false; // clear
}
}
从上面的代码来看,先判断LayoutManager是否为null,如果是结束测量,然后判断测量模式是不是精确模式,也就是布局文件中设置match_parent和写死固定值,如果是结束测量。如果是wrap_content继续执行下面的方法。
如果是刚开始测量的状态,执行 dispatchLayoutStep1()方法,如果判断不是精准模式,在执行dispatchLayoutStep2()方法。dispatchLayoutStep1()主要是做一些清空和初始化操作,dispatchLayoutStep2()是真正的测量子view的宽高来决定recycleview的宽高。
初始化操作的代码就不看了,从dispatchLayoutStep1()的注释来看主要做了以下步骤:1. adapter的更新 2.决定应该运行哪个动画 3. 保存视图当前的信息 4. 如果需要,运行预测布局并保存信息。
下面来看dispatchLayoutStep2()方法
1 | private void dispatchLayoutStep2() { |
从上面代码可以看到,开始布局那mLayout是LayoutManager对象,调用了LayoutManager中的onLayoutChildren方法,所以从这里我们可以知道,最终的布局是交给LayoutManager来完成的,系统提供了三个LayoutManager,线性的,网格的和瀑布流的,我们也可以自己继承LayoutManager来实现我们自己的LayoutManager。
所有item的布局都是在onLayoutChildren中实现,下面看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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// layout algorithm:
// 1) 通过检子view和其他变量找到一个锚点坐标和锚点位置
// 2) 从底部开始填充
// 3) 从顶部开始填充
// 4) 处理2和3两种方式的滚动
......
final View focused = getFocusedChild();
if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// 计算锚点的位置
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}else{......}
......
//一般情况下会选取最上(反向布局则是最下)的子View作为锚点参考
if (mAnchorInfo.mLayoutFromEnd) {
// 更新锚点坐标
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) {
// end could not consume all. add more items towards start
extraForStart = mLayoutState.mAvailable;
updateLayoutStateToFillStart(firstElement, startOffset);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
}else{
......
}
......
}
这段代码比较多,本篇省略缓存的部分,只看绘制,主要是通过子view和其他变量找到锚点信息,通过锚点信息判断出是从下往上填充还是从上往下填充,updateLayoutStateToFillStart和updateLayoutStateToFillEnd不断更新锚点的值,其实就是计算屏幕的上方或者下方是否还有剩余的空间,在调用fill方法填充的时候,如果空间不足就不会执行填充的方法。然后在fill(recycler, mLayoutState, state, false)方法中填充View。
mAnchorInfo是AnchorInfo类用来保存锚点的信息,它有三个主要变量
- int mPosition;//锚点参考view在整个布局中的位置,是第几个
- int mCoordinate; //锚点的起始坐标
- boolean mLayoutFromEnd; 是否从尾部开始布局默认是false
1 | /** |
fill()方法中回收移除不可见的View,在屏幕上堆叠出可见的Viw,堆叠的原理就是看看当前界面有没有剩余的空间,如果有就拿一个新的View填充上去,填充工作使用layoutChunk(recycler, state, layoutState, layoutChunkResult)方法。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
50
51
52
53
54
55
56
57
58
59
60
61
62
63void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
//找到将要布局的View,先从缓存中找找不到在创建
View view = layoutState.next(recycler);
......
LayoutParams params = (LayoutParams) view.getLayoutParams();
//如果ViewHolder的列表不为null
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
//添加view,最终调用ViewGroup的addView方法
addView(view);
} else {
//添加view, 最终调用ViewGroup的addView方法
addView(view, 0);
}
} else {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
//将要消失的view
addDisappearingView(view);
} else {
//将要消失的view
addDisappearingView(view, 0);
}
}
//测量子view
measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
//横排和竖排不同模式下 子view的四个边的边距
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;
}
}
//布局这个子view
layoutDecoratedWithMargins(view, left, top, right, bottom);
......
}
- layoutChunk方法就是找到一个子view,寻找子view是先去缓存中寻找找不到在通过调用createViewHolder()创建一个新的,缓存的逻辑此篇不往下看,只看绘制流程
- 找到view之后,通过addView方法,加入到ViewGroup中
- 通过measureChildWithMargins方法测量一个子view,会把我们通过recycleview.addItemDecoration方法设置的分割线的大小也计算进去,之后计算子view的四个边的边距
- 最后通过layoutDecoratedWithMargins方法布局一个子view。layoutDecoratedWithMargins中调用就是view的layout方法。
到此dispatchLayoutStep2()这个方法算是看完了,到此所有子view的测量(measure)和布局(layout),然后执行dispatchLayoutStep2()这个方法后面的方法 mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec) 根据子view的大小来设置自身(RecycleView)的大小。
RecycleView的onMeasure方法看完了,下面来看一下它的onLayout方法。1
2
3
4
5
6protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout();
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
里面调用了 dispatchLayout()方法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
28void 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;
//如果状态还是开始状态,那么从新走一遍dispatchLayoutStep1();和dispatchLayoutStep2();
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
// 数据更改后重新执行dispatchLayoutStep2();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
// 确保是精准模式
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
在onMeasure的源码中我们知道,如果RecycleView设置的是精准模式(比如match_partent,写死的值)就直接返回了,那么它的状态还是State.STEP_START,到了onLayout方法后还是会执行dispatchLayoutStep1()和dispatchLayoutStep2()方法。
也就是说如果RecycleView设置的wrap_content,那么就先去测量和布局子view,根据子view的宽高来确定自身的宽高,反之如果RecycleView设置的是精准模式,就在onLayou中去测量和布局子veiw。
这里有出来一个dispatchLayoutStep3(),第三步
1 | private void dispatchLayoutStep3() { |
可以看到,dispatchLayoutStep3()主要做了一些收尾的工作,这是布局的最后一步,保存视图和动画的信息,并做一些清理的工作。
onLayout方法就完了,最后看onDraw()方法1
2
3
4
5
6
7
8public 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);
}
}
onDraw方法很简单,就是绘制分割线,我们通过recycleview.addItemDecoration方法设置的分割线就在这里开始绘制,调用的是我们自定义分割线的时候里面写的onDraw方法。绘制的区域就是我们在自定义分割线的时候重写的getItemOffsets方法中的设置的偏移。这部分的测量工作在dispatchLayoutStep2()->onLayoutChildren->fill->layoutChunk->measureChildWithMargins这个方法中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
//获取装饰线条 就是我们添加的分割线
final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
widthUsed += insets.left + insets.right;
heightUsed += insets.top + insets.bottom;
//计算长和宽的测量模式 加上margin,padding 和 分隔线的长宽
final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
getPaddingLeft() + getPaddingRight()
+ lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
canScrollHorizontally());
final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
getPaddingTop() + getPaddingBottom()
+ lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
canScrollVertically());
if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
child.measure(widthSpec, heightSpec);
}
}
上面代码中就是获取线条的长宽,然后子veiw的可使用的宽高要减去这部分的值。
OK到这里RecycleView的绘制流程查看完成。
参考文章
https://www.jianshu.com/p/f91b41c8f487
https://www.jianshu.com/p/0c41bf63072a