前言
本文将主要分析 RecyclerView 的布局过程(以第一次布局为例),由于 RecyclerView 的 measure 也与 layout 关系比较密切,所以接下来主要看 onMeasure 和 onLayout(亲测 onMeasure 和 onLayout 是在设置了 LayoutManager 和 Adapter 后才执行)。先看 onMeasure:
onMeasure
1 |
|
RecyclerView 重写了 onMeasure 方法。该方法中,先判断是否有设置 LayoutManager,没有设置就执行 defaultOnMeasure。
设置了 LayoutManager 的话,就要判断 LayoutManager 是否开启了自动测量,开启的话就会使用默认的测量机制,否则就需要通过 LayoutManager 的 onMeasure 方法来完成测量工作。系统提供的几个 LayoutManager 都开启了自动测量。
自动测量时,涉及到一个重要的类:RecyclerView.State,这个类封装了当前 RecyclerView 的状态信息。其 mLayoutStep 变量表示当前 RecyclerView 的布局状态,状态有三种:
- STEP_START
- STEP_LAYOUT
- STEP_ANIMATIONS
一开始的状态为 STEP_START,调用完 dispatchLayoutStep1 方法后,状态变为 STEP_LAYOUT,表示接下来要进行布局,调用完 dispatchLayoutStep2 方法后,状态变为 State.STEP_ANIMATIONS,等待之后在 layout 时执行 dispatchLayoutStep3
这三个 step 负责不同的工作,step1 负责更新和记录状态,step2 真正进行布局,step 执行动画并进行清理工作。
可以看到,在开启自动测量时,RecyclerView 如果是 WRAP_CONTENT 状态,就要根据子 View 所占空间大小动态调整自己的大小,这时它就将子 View 的 measure 和 layout 提前到 onMeasure 中,因为它需要确定子 View 的大小和位置后,再来设置自己的大小。所以就会在 onMeasure 中执行 step1 和 step2。
接下来看一下 RecyclerView 的 layout 过程:
onLayout
1 |
|
调用 dispatchLayout:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 void dispatchLayout() {
// ...
mState.mIsMeasuring = false;
// 如果已经在 onMeasure 执行了 step1 和 step2,就不再执行 step1
// 至于 step2,如果发现尺寸发生了改变,将会再执行一次
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();
}
可以看到,如果已经在 onMeasure 执行了 step1 和 step2,就不再执行 step1,至于 step2,如果发现尺寸发生了改变,将会再执行一次,否则也不会执行。最后执行 step3。
下面分别看下这 3 个 step,首先看 step1
RecyclerView#dispatchLayoutStep1
1 | private void dispatchLayoutStep1() { |
先看注释(2),这里会根据 mRunSimpleAnimations 和 mRunPredictiveAnimations 的值来决定是否运行简单动画和预动画。这两个值是在哪里设置的呢?答案是在注释(1)的 processAdapterUpdatesAndSetAnimationFlags 方法处:
RecyclerView#processAdapterUpdatesAndSetAnimationFlags
1 | private void processAdapterUpdatesAndSetAnimationFlags() { |
里面的一些属性在注释中已经有说明。这里以第一次 layout 为例,此时由于第一次 layout 过程还未完成,mFirstLayoutComplete 为 false,mRunSimpleAnimations 也就为 false,进而 mRunPredictiveAnimations 也为 false。
所以在第一次 layout 中,并不会进行简单动画和预动画。这里就先不分析了,详细过程在分析动画的时候再说。
下面重点看一下 step2:
RecyclerView#dispatchLayoutStep2
1 | private void dispatchLayoutStep2() { |
step2 进行真正的布局,布局任务交由 LayoutManager 负责,调用其 onLayoutChildren 方法为所有子 View 布局。该方法交由具体的 LayoutManager 实现,这里以 LinearLayoutManager 为例,看一下它的 onLayoutChildren 实现:
LinearLayoutManager#onLayoutChildren
1 |
|
注释(1)处调用了 detachAndScrapAttachedViews 方法,该方法会将子 View 移除并根据情况添加到相应缓存中。所以如果不是第一次 layout,RecyclerView 已经存在子 View 的话,在重新填充布局前,会将旧的子 View 添加到缓存中,这样之后填充布局时就可以直接从缓存中拿,不用再次创建子 View。
下面看下布局过程,主要分两步:
1. 找到锚点(auchor 点)
该过程通过 updateAnchorInfoForLayout 方法实现:
updateAnchorInfoForLayout
1 | private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, |
继续看下 updateAnchorFromChildren 方法,该方法从子 View 中获取锚点
updateAnchorFromChildren
1 | private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, |
可以看到,优先选择被 focus 的子 View 作为锚点,没有的话就根据布局方向决定锚点,默认从上往下布局,所以锚点选取头部。
如果想要从下往上布局,可以这样设置:1
linearLayoutManager.setStackFromEnd(true);
这样的话,锚点会在尾部,数据加载完后首先显示的是底部的数据。
2. 填充布局
根据布局方向,先后填充满锚点上方和下方的所有区域
填充的过程调用 fill 方法:
fill
1 | int fill(RecyclerView.Recycler recycler, LayoutState layoutState, |
看注释(1)处,在 while 循环里有一个 layoutChunk 方法,只要还有剩余空间,就不会不断执行该方法:
layoutChunk
1 | void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, |
先看注释(1)处,这里返回下一个要填充的 View,来看下具体过程:1
2
3
4
5
6
7View next(RecyclerView.Recycler recycler) {
// ...
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
可以看到,获取 View 的工作也是交给了 Recycler,通过 Recycler 的 getViewForPosition 来获取一个指定位置的子 View,该方法在 Recycler 已经分析过了。
继续看注释(2)处的 addView 方法: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
43private void addViewInt(View child, int index, boolean disappearing) {
final ViewHolder holder = getChildViewHolderInt(child);
// ...
// 该 ViewHolder 从 ChangedScrap、AttachedScrap、HiddenViews 中得到
// 或者该 ViewHolder 曾经通过 scrapView 方法缓存到 Scrap 缓存中
if (holder.wasReturnedFromScrap() || holder.isScrap()) {
// 做些清理工作:删除 Scrap 缓存、清除标记等
if (holder.isScrap()) {
holder.unScrap();
} else {
holder.clearReturnedFromScrapFlag();
}
// 子 View 重新 attach 到 RecyclerView 中
mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
// DISPATCH_TEMP_DETACH:该值默认为 false,且没看到有地方将其设置为 true
if (DISPATCH_TEMP_DETACH) {
ViewCompat.dispatchFinishTemporaryDetach(child);
}
}
// 该子 View 一直是有效的,只是可能要移动下位置(对应滑动时没有滑出屏幕的子 View)
else if (child.getParent() == mRecyclerView) {
int currentIndex = mChildHelper.indexOfChild(child);
if (index == -1) {
index = mChildHelper.getChildCount();
}
// 将该子 View 移动到正确位置
if (currentIndex != index) {
mRecyclerView.mLayout.moveView(currentIndex, index);
}
}
// 其他情况,例如从 CahcedView 或 RecycledViewPool 得到的缓存 View,或者是新创建的 View
else {
mChildHelper.addView(child, index, false);
lp.mInsetsDirty = true;
if (mSmoothScroller != null && mSmoothScroller.isRunning()) {
mSmoothScroller.onChildAttachedToWindow(child);
}
}
// ...
}
该方法通过判断 View 的来源,利用不同的方式将子 View 添加到 RecyclerView 中,填充完布局。
最后看一下 step3:
dispatchLayoutStep3
1 | private void dispatchLayoutStep3() { |
step3 主要是执行动画和进行一系列的清理工作,例如重置 layout 状态,清理 Scrap 缓存等等。由于在第一次布局时,mState.mRunSimpleAnimations 为 false,不会执行动画,动画部分就先不分析了。
小结
前面说了这么多,这里小结一下 onLayout 的过程:
- layout 过程分为 3 个 step,step1 负责更新和记录状态,step2 真正进行布局,step 执行动画并进行清理工作。如果 RecyclerView 的宽高为 WRAP_CONTENT 模式,那么需要在 measure 过程提前进行 step1 和 step2,先获得子 View 的大小,才能确定自己的大小。而 step3 肯定是在 layout 过程执行。
- step2 真正进行布局,布局任务由 LayoutManager 负责,通过它的 onLayoutChildren 方法对子 View 进行布局。布局过程分两步:
- 找到锚点,优先选择被 focus 的子 View 作为锚点,没有的话就根据布局方向决定锚点,默认头部为锚点。
- 根据布局方向,先后填充满锚点上方和下方的区域,填充所需的 View 交由 Recycler 提供。
写在最后
本文主要以第一次布局,分析了 RecyclerView 的 measure 和 layout 过程。当然,主要分析的还是 layout 过程。至于第二次 layout 或者是更新列表时的 layout,会在动画和缓存上有所不同,但主要流程还是一样的,并且缓存相关的在第一篇有更详细的说明,而动画的话可能会在后面另开一篇来讲。