前言
本文将主要分析 RecyclerView 的布局过程(以第一次布局为例),由于 RecyclerView 的 measure 也与 layout 关系比较密切,所以接下来主要看 onMeasure 和 onLayout,先看 onMeasure:
onMeasure
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
// mLayout 是 LayoutManager,通过 setLayoutManager 方法设置,没有设置则为空
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
// 是否开启自动测量(RV 提供的几种 LM 都开启了自动测量)
if (mLayout.isAutoMeasureEnabled()) {
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
// 实际上就是调用 RV 的 defaultOnMeasure 方法
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
// 如果测量模式是 EXACTLY,退出
if (measureSpecModeIsExactly || mAdapter == null) {
return;
}
// 布局状态为 STEP_START 时,进行 step1
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
mLayout.setMeasureSpecs(widthSpec, heightSpec);
mState.mIsMeasuring = true;
// 进行 step2
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
// 判断是否要测量两次
if (mLayout.shouldMeasureTwice()) {
mLayout.setMeasureSpecs(
MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
mState.mIsMeasuring = true;
dispatchLayoutStep2();
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
} else {
// ...
}
}
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 真正进行布局,step3 执行动画并进行清理工作。
可以看到,在开启自动测量时,RecyclerView 如果是 WRAP_CONTENT 状态,就要根据子 View 所占空间大小动态调整自己的大小,这时它就将子 View 的 measure 和 layout 提前到 onMeasure 中,因为它需要确定子 View 的大小和位置后,再来设置自己的大小。所以就会在 onMeasure 中执行 step1 和 step2。
接下来看一下 RecyclerView 的 layout 过程:
onLayout
@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;
}
调用 dispatchLayout:
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
private void dispatchLayoutStep1() {
// ...
// (1)
processAdapterUpdatesAndSetAnimationFlags();
// (2)
if (mState.mRunSimpleAnimations) {
// ...
}
if (mState.mRunPredictiveAnimations) {
// ...
}
// ...
mState.mLayoutStep = State.STEP_LAYOUT;
}
先看注释(2),这里会根据 mRunSimpleAnimations 和 mRunPredictiveAnimations 的值来决定是否运行简单动画和预动画。这两个值是在哪里设置的呢?答案是在注释(1)的 processAdapterUpdatesAndSetAnimationFlags 方法处:
RecyclerView#processAdapterUpdatesAndSetAnimationFlags
private void processAdapterUpdatesAndSetAnimationFlags() {
// ...
// mItemsAddedOrRemoved:当有 item 添加或删除的时候设置为 ture
// mItemsChanged:当有 item 的数据更新时设置为 true
boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
// 1. mFirstLayoutComplete:第一次 layout 完成后,设置为 true
// 2. mItemAnimator:默认为 DefaultItemAnimator,可通过 RecyclerView 的 setItemAnimator 方法设置
// 3. mDataSetHasChangedAfterLayout:调用 setAdapter、swapAdapter 或 notifyDateSetChanged
// 后设置为 true,在 layout 过程的 step3 中设置为 false
// 4. mLayout.mRequestedSimpleAnimations:默认为 false,
// 可以通过调用 LayoutManager 的 requestSimpleAnimationsInNextLayout 方法将该值设置为 true
// 5. mAdapter.hasStableIds:默认为 false,可通过 Adapter 的 setHasStableIds 方法设置
mState.mRunSimpleAnimations = mFirstLayoutComplete
&& mItemAnimator != null
&& (mDataSetHasChangedAfterLayout
|| animationTypeSupported
|| mLayout.mRequestedSimpleAnimations)
&& (!mDataSetHasChangedAfterLayout
|| mAdapter.hasStableIds());
// predictiveItemAnimationsEnabled:LinearLayoutManager 默认支持预动画,返回 true
mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
&& animationTypeSupported
&& !mDataSetHasChangedAfterLayout
&& predictiveItemAnimationsEnabled();
}
里面的一些属性在注释中已经有说明。这里以第一次 layout 为例,此时由于第一次 layout 过程还未完成,mFirstLayoutComplete 为 false,mRunSimpleAnimations 也就为 false,进而 mRunPredictiveAnimations 也为 false。
所以在第一次 layout 中,并不会进行简单动画和预动画。这里就先不分析了,详细过程在分析动画的时候再说。
下面重点看一下 step2:
RecyclerView#dispatchLayoutStep2
private void dispatchLayoutStep2() {
// ...
mLayout.onLayoutChildren(mRecycler, mState);
// ...
}
step2 进行真正的布局,布局任务交由 LayoutManager 负责,调用其 onLayoutChildren 方法为所有子 View 布局。该方法交由具体的 LayoutManager 实现,这里以 LinearLayoutManager 为例,看一下它的 onLayoutChildren 实现:
LinearLayoutManager#onLayoutChildren
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// ...
// AnchorInfo 的 mValid 属性默认为 false
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
// mShouldReverseLayout 和 mStackFromEnd 默认都为 false
// 异或操作后结果仍为 false
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// 找到锚点的位置,保存到 AnchorInfo 的 mPosition 中
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
}
// ...
// (1)
detachAndScrapAttachedViews(recycler);
if (mAnchorInfo.mLayoutFromEnd) {
// ...
} else {
// fill towards end
updateLayoutStateToFillEnd(mAnchorInfo);
mLayoutState.mExtra = extraForEnd;
fill(recycler, mLayoutState, state, false);
// ...
// fill towards start
updateLayoutStateToFillStart(mAnchorInfo);
mLayoutState.mExtra = extraForStart;
fill(recycler, mLayoutState, state, false);
// ...
}
// ...
}
注释(1)处调用了 detachAndScrapAttachedViews 方法,该方法会将子 View 移除并根据情况添加到相应缓存中。所以如果不是第一次 layout,RecyclerView 已经存在子 View 的话,在重新填充布局前,会将旧的子 View 添加到缓存中,这样之后填充布局时就可以直接从缓存中拿,不用再次创建子 View。
下面看下布局过程,主要分两步:
1. 找到锚点(auchor 点)
该过程通过 updateAnchorInfoForLayout 方法实现:
updateAnchorInfoForLayout
private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state,
AnchorInfo anchorInfo) {
// 一般这里都是返回 false
if (updateAnchorFromPendingData(state, anchorInfo)) {
return;
}
// 首先从子 View 中获取锚点
if (updateAnchorFromChildren(recycler, state, anchorInfo)) {
return;
}
// 没有从子 View 得到锚点,就将头或尾设置为锚点(默认将头设置为锚点)
anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
}
继续看下 updateAnchorFromChildren 方法,该方法从子 View 中获取锚点
updateAnchorFromChildren
private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler,
RecyclerView.State state, AnchorInfo anchorInfo) {
if (getChildCount() == 0) {
return false;
}
// 将被 focus 的子 View 作为锚点
final View focused = getFocusedChild();
if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) {
anchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
return true;
}
if (mLastStackFromEnd != mStackFromEnd) {
return false;
}
// 根据 layout 的方向决定锚点,默认从上往下,所以锚点在头部
View referenceChild = anchorInfo.mLayoutFromEnd
? findReferenceChildClosestToEnd(recycler, state)
: findReferenceChildClosestToStart(recycler, state);
if (referenceChild != null) {
anchorInfo.assignFromView(referenceChild, getPosition(referenceChild));
// ...
return true;
}
return false;
}
可以看到,优先选择被 focus 的子 View 作为锚点,没有的话就根据布局方向决定锚点,默认从上往下布局,所以锚点选取头部。
如果想要从下往上布局,可以这样设置:
linearLayoutManager.setStackFromEnd(true);
这样的话,锚点会在尾部,数据加载完后首先显示的是底部的数据。
2. 填充布局
根据布局方向,先后填充满锚点上方和下方的所有区域
填充的过程调用 fill 方法:
fill
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// ...
// 进行 layout 时,layoutState.mScrollingOffset 的值等于
// LayoutState.SCROLLING_OFFSET_NaN,不会进入此 if 块,这里先不分析
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// ...
// 进行回收工作
recycleByLayoutState(recycler, layoutState);
}
int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
// ...
// (1)
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// ...
}
// ...
}
看注释(1)处,在 while 循环里有一个 layoutChunk 方法,只要还有剩余空间,就不会不断执行该方法:
layoutChunk
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// (1)
View view = layoutState.next(recycler);
// ...
// 默认情况下,layoutState.mScrapList 等于 null
if (layoutState.mScrapList == null) {
// mShouldReverseLayout 默认为 false,可通过 LLM 的 setReverseLayout 方法设置
// 从上往下填充布局时,layoutState.mLayoutDirection 为 LayoutState.LAYOUT_END
// 默认情况下,从上往下布局时进入 if 块
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
// (2)
addView(view);
} else {
addView(view, 0);
}
} else {
// ...
}
// ...
// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
layoutDecoratedWithMargins(view, left, top, right, bottom);
}
先看注释(1)处,这里返回下一个要填充的 View,来看下具体过程:
View next(RecyclerView.Recycler recycler) {
// ...
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
可以看到,获取 View 的工作也是交给了 Recycler,通过 Recycler 的 getViewForPosition 来获取一个指定位置的子 View,该方法在 Recycler 已经分析过了。
继续看注释(2)处的 addView 方法:
private 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
private void dispatchLayoutStep3() {
// ...
// 将 layout 状态重置回 State.STEP_START
mState.mLayoutStep = State.STEP_START;
// 执行动画
if (mState.mRunSimpleAnimations) {
// ...
}
// 清空 attachedScrap
mLayout.removeAndRecycleScrapInt(mRecycler);
// 重置一系列的变量
mState.mPreviousLayoutItemCount = mState.mItemCount;
mDataSetHasChangedAfterLayout = false;
mDispatchItemsChangedEvent = false;
mState.mRunSimpleAnimations = false;
mState.mRunPredictiveAnimations = false;
mLayout.mRequestedSimpleAnimations = false;
// 清空 changedScrao
if (mRecycler.mChangedScrap != null) {
mRecycler.mChangedScrap.clear();
}
// 其它清理工作
}
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,会在动画和缓存上有所不同,但主要流程还是一样的,并且缓存相关的在第一篇有更详细的说明,而动画的话可能会在后面另开一篇来讲。
网友评论