RecycleView的绘制流程

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
82
protected 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void dispatchLayoutStep2() {
//开始中断布局请求
startInterceptRequestLayout();
onEnterLayoutOrScroll();
//设置状态为 布局和动画状态
mState.assertLayoutStep(State.STEP_LAYOUT | State.STEP_ANIMATIONS);
mAdapterHelper.consumeUpdatesInOnePass();
mState.mItemCount = mAdapter.getItemCount();
mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

// 开始布局
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);

mState.mStructureChanged = false;
mPendingSavedState = null;

// 是否禁用动画
mState.mRunSimpleAnimations = mState.mRunSimpleAnimations && mItemAnimator != null;
mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
}

从上面代码可以看到,开始布局那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
53
public 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类用来保存锚点的信息,它有三个主要变量

  1. int mPosition;//锚点参考view在整个布局中的位置,是第几个
  2. int mCoordinate; //锚点的起始坐标
  3. boolean mLayoutFromEnd; 是否从尾部开始布局默认是false
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
   /**
* 填充由layoutState给定的布局,它独立于LinearLayoutManager之外,稍微改一下可以用于我们的自定义的LayoutManager
* @param recycler 当前的回收对象
* @param layoutState 记录如何填充空间
* @param state 控制滚动的步骤
* @param stopOnFocusable 如果为true,则在第一个可聚焦的新子元素中停止填充
* @return 它添加的像素数。适用于滚动函数
*/
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// 我们应该设置最大偏移量是 mFastScroll + available
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
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.resetInternal();
if (VERBOSE_TRACING) {
TraceCompat.beginSection("LLM LayoutChunk");
}
//向屏幕上填充一个View
layoutChunk(recycler, state, layoutState, layoutChunkResult);
if (VERBOSE_TRACING) {
TraceCompat.endSection();
}
if (layoutChunkResult.mFinished) {
break;
}
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
/**
* Consume the available space if:
* * layoutChunk did not request to be ignored
* * OR we are laying out scrap children
* * OR we are not doing pre-layout
*/
if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
|| !state.isPreLayout()) {
//如果进行了填充,减去填充使用的空间
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// 保留一个单独的剩余空间,因为mAvailable对于回收非常重要
remainingSpace -= layoutChunkResult.mConsumed;
}

if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
if (DEBUG) {
validateChildOrder();
}
return start - layoutState.mAvailable;
}

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
63
void 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
6
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()方法

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
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;
//如果状态还是开始状态,那么从新走一遍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
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
private void dispatchLayoutStep3() {
mState.assertLayoutStep(State.STEP_ANIMATIONS);
startInterceptRequestLayout();//开始中断布局
onEnterLayoutOrScroll();
mState.mLayoutStep = State.STEP_START;
if (mState.mRunSimpleAnimations) {
// 找到当前的位置,并处理更改动画
for (int i = mChildHelper.getChildCount() - 1; i >= 0; i--) {
ViewHolder holder = getChildViewHolderInt(mChildHelper.getChildAt(i));
if (holder.shouldIgnore()) {
continue;
}
long key = getChangedHolderKey(holder);
final ItemHolderInfo animationInfo = mItemAnimator
.recordPostLayoutInformation(mState, holder);
ViewHolder oldChangeViewHolder = mViewInfoStore.getFromOldChangeHolders(key);
if (oldChangeViewHolder != null && !oldChangeViewHolder.shouldIgnore()) {
//运行一个变更动画
final boolean oldDisappearing = mViewInfoStore.isDisappearing(
oldChangeViewHolder);
final boolean newDisappearing = mViewInfoStore.isDisappearing(holder);
if (oldDisappearing && oldChangeViewHolder == holder) {
// run disappear animation instead of change
mViewInfoStore.addToPostLayout(holder, animationInfo);
} else {
final ItemHolderInfo preInfo = mViewInfoStore.popFromPreLayout(
oldChangeViewHolder);
// we add and remove so that any post info is merged.
mViewInfoStore.addToPostLayout(holder, animationInfo);
ItemHolderInfo postInfo = mViewInfoStore.popFromPostLayout(holder);
if (preInfo == null) {
handleMissingPreInfoForChangeError(key, holder, oldChangeViewHolder);
} else {
animateChange(oldChangeViewHolder, holder, preInfo, postInfo,
oldDisappearing, newDisappearing);
}
}
} else {
mViewInfoStore.addToPostLayout(holder, animationInfo);
}
}

// 触发动画
mViewInfoStore.process(mViewInfoProcessCallback);
}

mLayout.removeAndRecycleScrapInt(mRecycler);
mState.mPreviousLayoutItemCount = mState.mItemCount;
mDataSetHasChangedAfterLayout = false;
mDispatchItemsChangedEvent = false;
mState.mRunSimpleAnimations = false;

mState.mRunPredictiveAnimations = false;
mLayout.mRequestedSimpleAnimations = false;
if (mRecycler.mChangedScrap != null) {
mRecycler.mChangedScrap.clear();
}
if (mLayout.mPrefetchMaxObservedInInitialPrefetch) {
// Initial prefetch has expanded cache, so reset until next prefetch.
// This prevents initial prefetches from expanding the cache permanently.
mLayout.mPrefetchMaxCountObserved = 0;
mLayout.mPrefetchMaxObservedInInitialPrefetch = false;
mRecycler.updateViewCacheSize();
}
//布局完成
mLayout.onLayoutCompleted(mState);
onExitLayoutOrScroll();
stopInterceptRequestLayout(false);
mViewInfoStore.clear();
if (didChildRangeChange(mMinMaxLayoutPositions[0], mMinMaxLayoutPositions[1])) {
dispatchOnScrolled(0, 0);
}
recoverFocusFromState();
resetFocusInfo();
}

可以看到,dispatchLayoutStep3()主要做了一些收尾的工作,这是布局的最后一步,保存视图和动画的信息,并做一些清理的工作。

onLayout方法就完了,最后看onDraw()方法

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

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
19
public 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

https://www.jianshu.com/p/616ca453aa17

https://www.jianshu.com/p/8fa71076179d

# 进阶

コメント

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×