关于布局项填充填的两种情况:
- 完全填充
- 因为滑动产生了空白区域,需要进行表项填充 二次布局相关的逻辑暂时「架空」了,暂时只关注于呈现最终效果的布局填充。
1. RecyclerView#onLayout函数
RecyclerView的onLayout函数整体上只干了一件事情,将布局委托给LayoutManager,在dispatchLayout()方法中,我们可以清楚地看到,没有Adapter和LayoutManager的情况下,布局是不会发生的。
void dispatchLayout() {
if (mAdapter == null) {
return;
}
if (mLayout == null) {
return;
}
mState.mIsMeasuring = false;
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();// 预布局
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();// 布局
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {
// 前两步是在onMeasure中完成的,但是此处我们不得不再次执行,因为我们发现尺寸发生了改变。
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();// 布局
} else {
// 确保同步(测量MODE为EXACTLY时)
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();// 动画执行相关
}
在dispatchLayoutStep1、dispatchLayoutStep2中都耦合了大量的动画相关的调用和语句,先看看dispatchLayoutStep1,我们 大致地将流程简化一下,其实只是为了突出一点,仅在设置了预测动画的情况下,dispatchLayoutStep1真正地进行一次布局 mLayout.onLayoutChildren(mRecycler, mState);。
private void dispatchLayoutStep1() {
// ……
if (mState.mRunSimpleAnimations) {
int count = mChildHelper.getChildCount();
for (int i = 0; i < count; ++i) {
// ……
}
}
if (mState.mRunPredictiveAnimations) {
mLayout.onLayoutChildren(mRecycler, mState);
}
// ……
mState.mLayoutStep = State.STEP_LAYOUT;
}
再看看dispatchLayoutStep2,没有任何的条件分支,任何一个走到dispatchLayoutStep2的方法,都会进行一次布局。
private void dispatchLayoutStep2() {
// ……
mLayout.onLayoutChildren(mRecycler, mState);
// ……
mState.mLayoutStep = State.STEP_ANIMATIONS;
// ……
}
这也说明了一点,如果你的LayoutManager启用了预测动画,那么在重新布局时,将会有两次布局的过程,其中,step1中的mState中的mInPreLayout将为true,step2中的mInPreLayout将为false。
2. 布局填充(以LinearLayoutManager为例)
2.1 概览
由RecyclerView#dispatchLayoutStepX方法调用,进入到LayoutManager的onLayoutChildren()方法,这个方法接收了一个recycle和一个state。
之后会调用到fill(recycler,layoutState,state,stopOnFocusable)方法,根据layoutState中的一些信息,来进行布局的填充。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
// ……
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
// ……
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// ……
}
// ……
}
fill()函数中,在一个循环中,对layoutChunk(recycler, state, layoutState, layoutChunkResult);进行了调用,layoutChunk中,就开始和recycler做交互了。
// layoutChunk >> layoutState.next(recycler);
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);// 从recycler中取出一个ViewHolder.itemView,可能是从缓存中取出的,也可能是从itemView中取出的。
mCurrentPosition += mItemDirection;
return view;
}
这就是整个布局大致的调用流程,从onLayoutChildren()开始,计算布局,并采用fill()函数填充,使用layoutChunk()函数和recycler交互,对单独的表项进行填充。
2.2 AnchorInfo
锚点位置和坐标定义了LinearLayoutManager在布局时的参考点。有如下的五个变量
OrientationHelper mOrientationHelper;// 和LinearLayoutManager布局的方向相关
int mPosition;// 位置
int mCoordinate; // 坐标
boolean mLayoutFromEnd; // 是否是reverse布局
boolean mValid; // 是否有效
LinearLayoutManager会根据mShouldReverseLayout来考虑,是否需要做反向的布局。在RecyclerView本身足够大时,如果不设置,那么第一个item默认会显示在顶部;如果设置了stackFromEnd = true或者ShouldReverseLayout为true的情况下,第一项就会在RecyclerView的底部开始布局,然后第二项在第一项的上面,一直向上布局到最后一项。
LinearLayoutManager在默认情况下,AnchorInfo中的mCoordinate的数值是0,即默认的锚点是RecyclerView的顶部;而reverse之后的锚点,通常是RecyclerView的高度,即锚点在RecyclerView的底部位置。如果产生了滑动,那么锚点通常是露出的第一个View的位置(如果是Reverse,那么锚点就是RecyclerView底部第一个露出的View的位置)。也就是说,如果锚点在头部,那么布局方式一定是从上往下;而锚点在尾部,布局方式一定是从下往上,不存在双向布局的情况(虽然调用了两次fill(),但只有一侧的fill()是真的在fill)。
也有一种特殊的情况,锚点会在中间的,这种情况主要发生在中间的某个ViewHolder获得了焦点,例如软键盘聚焦到某个ViewHolder的EditText之上时,mCoordinate的数值会变成该EditText相较于RecyclerView顶部的偏移量(这部分的逻辑在assignFromViewAndKeepVisibleRect()调用assignFromView当中)。这种情况下,会从mAnchor处的View开始,向两侧发起布局。
2.3 SavedState
mPendingSavedState,存储的主要是锚点的一些额外的存储信息,该信息主要是用于RecyclerView在onSaveInstanceState/onRestoreInstance恢复LinearLayoutManager视图数据时使用的。
int mAnchorPosition; // 锚点位置(index)
int mAnchorOffset;// 锚点的偏移
boolean mAnchorLayoutFromEnd;// 是否从End开始布局
3. 布局流程分析
3.1 onLayoutChildren
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
// mPendingSavedState != null时,说明是经过了onRestoreInstace,布局刚刚由系统恢复的,并且getItemCount = 0即没有可布局的项目,清除掉所有Views,并直接返回;
if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
return;
}
}
// 从系统恢复的,并且含有有效的Anchor。
if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
// 恢复之前的偏移量
mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
}
// 检查layoutState和判断是否需要做reverse
// ......
final View focused = getFocusedChild();
// 如果有聚焦的Child,那么将锚点设置在它身上;
if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null) {
mAnchorInfo.reset();
mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
// 从聚焦的View中获取锚点
updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
mAnchorInfo.mValid = true;
} else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
>= mOrientationHelper.getEndAfterPadding()
|| mOrientationHelper.getDecoratedEnd(focused)
<= mOrientationHelper.getStartAfterPadding())) {
// 一些情况可能会引起RecyclerView的缩小,例如软键盘弹起,原先的聚焦的View可能会被踢到屏幕外。
// 这样一来,我们需要确保focusedView被正确布局。
mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
}
……
// 锚点设置完成后的回调,这方法默认是一个空方法,但是在GridLayoutManager中被重写了。
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
// 废弃掉所有attached的视图,按需要将送入回收器处理。
detachAndScrapAttachedViews(recycler);
……
mLayoutState.mNoRecycleSpace = 0;
// 从锚点开始布局,该if分支下,是从End处往上布局的。
if (mAnchorInfo.mLayoutFromEnd) {
//向上填充
……
updateLayoutStateToFillStart(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
……
// 向下填充
……
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
……
// 如果剩余空间mAvailable还没被完全消费完,那么再做一次向上填充。
if (mLayoutState.mAvailable > 0) {
// end could not consume all. add more items towards start
……
updateLayoutStateToFillStart(firstElement, startOffset);
fill(recycler, mLayoutState, state, false);
……
}
} else {
// 和上面,同理,只不过这个分支中是优先向下布局的。
updateLayoutStateToFillEnd(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
……
updateLayoutStateToFillStart(mAnchorInfo);
fill(recycler, mLayoutState, state, false);
……
if (mLayoutState.mAvailable > 0) {
……
updateLayoutStateToFillEnd(lastElement, endOffset);
fill(recycler, mLayoutState, state, false);
……
}
}
// 对可能产生的UI缝隙(gap)进行修复
……
// 如果是第二次布局,那么可以宣告当前的布局阶段已经结束,回调onLayoutComplete();
if (!state.isPreLayout()) {
mOrientationHelper.onLayoutComplete();
} else {
mAnchorInfo.reset();
}
mLastStackFromEnd = mStackFromEnd;
if (DEBUG) {
validateChildOrder();
}
}
大致上的一个布局步骤是:
- 查看是否是由系统恢复的布局,是的话恢复之前的Anchor数据;
- 无的话,去查找当前
focused的视图,锚点会被设置在FocusedView之上;LLM将会确保该视图被正确地布局,不会被输入法弹出等事件改变布局。 - 将所有视图从屏幕上揭下(detachAndScrapAttachedViews),并送入回收器,无用ViewHolder的调用
recycleViewHolderInternal去做回收(mCachesView、RecyclerViewPool);有用的调用scrapView去做一次缓存(mChangedScrap、mAttachedScrap)。 - 然后将根据布局的方向选择,从上到下布局或者是从下到上布局。具体的起始点是锚点(Anchor),例如我们常用的从上到下布局方式中,会从锚点开始,优先调用
fill()向下布局,填充;再向上填充(仅当焦点View是Anchor时,Anchor可能会在中央,一般不会出现这种向上填充的情况)。 - 最后,会根据
mAvailable判断是否还有剩余的空间,如果有,会请求新的ViewHolder项目进入布局。
3.2 fill()方法
既然我们调用一次fill()方法,就能够完成一个方向上的布局,那么fill()方法中,必然少不了一个循环:
while(还有剩余空间){
布局;
bottom += 布局高度;
剩余空间 -= 布局高度;
……
}
fill()函数的执行,几乎每一个语句都和layoutState相关,之前提到过,LayoutState中存储的是和就是当前布局的一个状态,诸如mAvailable、mOffset等等参数
先额外地看看这几个LayoutState中的参数:
mOffset:RecyclerView最后一个子View的Bottom的数值,该数值是相较于屏幕顶部的;下一个Item会从此处开始布局。mScrollingOffset,限制的滚动量,即最后一个Item超出屏幕的距离,一旦滚动超出这个可见距离会导致RecyclerView加载一个新的item。无滑动情况发生,那么该数值将会是LayoutState.SCROLLING_OFFSET_NaN;如有滑动情况发生,该数值将会是临界导致一个新的项目被加入RecyclerView的那一次的滑动距离,如果滑得慢的话,通常是1、2、3这种个位数。如果快速滑动,可能是一百多、两百多。mAvailable:剩余可用空间,LLM会在mAvailable的范围内,不断地向其中添加项目,直到mAvailable的值为负数,该数值在布局的初期会被设置为RecyclerView的高度。
对于fill()函数,我们只关注它的这个while循环:
//mExtraFillSpace,和预布局相关的一个额外的空间。
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
// layoutChunkResult,这个对象就是一个临时的空间,当每一个布局项目被填充时,其layoutChunkResult中的变量mConsumed就会被设置为该布局项目所消耗的高度,进入下一轮循环的时候就会被reset。
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunkResult.resetInternal();
// 布局的核心👇🏻
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// layoutChunk中,会设置该值,如果设置为true就跳出,这也说明之后的语句都是为下一项布局做准备。
if (layoutChunkResult.mFinished) {
break;
}
// 最后一项的bottom距离顶部的高度增大了(增加的数目就是新加入一项的高度);mLayoutDirection是布局的方向,可能是reverse的布局,那就是反向的,这里体现的就是。
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
// 理所应当地,如果新的项目增加进来,可用空间变小;
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// 剩余空间也变小
remainingSpace -= layoutChunkResult.mConsumed;
}
// 如果是滑动情况下造成的布局(新的Item被加载进来的情况)
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// mScrollingOffset会被设置为「导致新项目被加载这一次滑动」的数值,X,X + mConsumed的数值就是新加入的一项的bottom的数值。
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
// 前几行的代码中,因为新的项目被填充进来了,mAvailable-=新加入的项目的高度,如果mAvailable<0的话,说明不仅没有剩余控件,还导致最后一项显示不完全(超出了屏幕),所以更新mScrollingOffset的值 = 新加入的项目 - 原有的可用空间。
// 如果新的项目补充进来,可用可见变小,但是仍然 > 0,说明还能补,不是最后一项,所以这里不作处理。
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
if (stopOnFocusable && layoutChunkResult.mFocusable) {
break;
}
}
补充一下刚才的伪代码:
// 从上到下布局为例,不考虑预布局
// remainingSpace由mAvailable和预布局相关的数值共同构成,为了便于理解,这里直接采用mAvailable。
while(mAvailable > 0){
// 新增一项
layoutChunk(recycler, state, layoutState, layoutChunkResult);
// 最后一项的偏移
layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
// 剩余空间 -= 最新一项消耗掉的空间
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// 如果产生了滑动,滑动导致的新的项目被加入。这里有个前提,一般的滑动并不会每一次都引起reLayout(),引起reLayout()的滑动必然是已经靠近临界了。因为滑动的原因走到这儿,一定会有一个新的项目被填充进来所以mScrollingOffset的数值会拼接上layoutChunkResult.mConsumed.
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
// 如果已经没有可用可见了,说明最后一项已经显示不全了,mScrollingOffset - 多出来的可用空间
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);// 对应方向的视图回收
}
}
3.2.1 举两个🌰:
1. 是布局引起的fill()
当前的布局示意图如下图左,我们在一个高度为600的RecyclerView中,做初始化布局,此时已经完成了4个高度为100的item的布局,此时的mOffset = 400,mAvailable = 200,注意,初始化布局和滑动无关,因此mScrollingOffset的数值为NaN,我们不必去理会它。此时,我们需要填充一个150的item,如上伪代码执行的步骤是:
- mAvailable = 200 > 0 : 说明还有空间,可以填充。
- layoutChunk做填充,其中会在
layoutChunkResult中设置诸如mConsumed这类的结果。 - 填充完毕之后,我们看图二,非常非常明显地,mOffset的数值变大了 = 550,mAvailable的数值变小了 = 50;差额正是
layoutChunkResult.mConsumed。 - 因为和滑动无关,所以这一个项目的布局就算是已经完成了;
- 同理,此时的mAvailable = 50 > 0:说明还有空间,可以填充,但可能不能完全显示,但是也得填充,不能空着。
- 然后就如下图右,填充了一个黄色的item。
2.滑动引起的fill()
这种情况相对来说比情况一来说,多了一个mScrollingOffset,即手指滑动的一个偏移量,并且,这一次的fill()调用,是直接由scroll系列的方法过来的,而不是直接触发的onLayoutChildren(),在updateLayoutState(layoutDirection, absDelta, true, state);中,为mAvailable赋予了初值,如下图二,由于滑动,视图整体上升了50,导致mAvailable的初始值为50,进入fill()方法。
-
此时的mOffset的数值是600,mAvailable是50,mScrollingOffset = 0进入while循环;
-
使用
layoutChunk(recycler, state, layoutState, layoutChunkResult);进行布局; -
mOffset + 100 = 700,mAvailable - 100 = -50。 -
mScrollingOffset + 100 = 100; -
因为剩余空间(mAvailable) < 0,mScrollingOffset += mAvailable = 50
-
当前项目布局完成;while条件不满足,跳出该
fill()函数。 -
此时,mScrollingOffset = 50,mAvailable = -50,而mOffset = 600。
-
如果此时一次向下滑动50的时候,进入
fill()函数时,参数状态是:mScrollingOffset = 50、mOffset = 650和mAvailable = 0,没有可用空间,自然就不需要布局了,于是返回fill()函数。(注意,这里的mOffset = 650是在第二次的scrollBy#updateLayoutState(layoutDirection, absDelta, true, state);`中使用OrientationHelper计算出来的,和之前的700没关系;) -
如果进行的不是滑动50,而是向下滑动100,结果就不同了,此时进入
fill()函数时,参数状态是:mScrollingOffset = 50、mOffset = 650和mAvailable = 50,此时会有50的可用空间,这将会导致新的一项被布局进来,紧接着就开始走步骤2的流程。显然,这种情况下由于滑动产生了可用空间,布局的填充和``LayoutManager`对滑动的处理密切相关。
3.2.2 fill方法的总结
总结一下fill()方法,本质上是由布局操作触发的一个使用ViewHolder.itemView对RecyclerView进行填充的方法。从锚点开始,有两个填充方向,分别是正向布局、逆向布局。对于通常的下,二者只会发生一次;如果锚点在某个对焦的视图之上,那么就会从锚点开始,两个布局都会执行。
引起layout的原因主要有两个,首先是初始的布局,这种情况下,我们需要使用ViewHolder.itemView填满整个RecyclerView的第一屏。
其次是因为滑动引起的relayout,这种情况下,手指滑动会形成很多个滑动采样数据,如果滑动得快的话,其中一个采样数据可能是100、200个像素,如果滑动得很慢的话,其中的一个采样数据可能会是2、3这种个位数的数据。而其中,一定会有一个临界数据导致一个原先不可见的视图在屏幕上显示出来,这一次的临界滑动采样,将会导致fill()方法的调用。
总体上来说,填充的算法和我们预想的填充算法大体上相同。
3.3 layoutChunk
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
// 向Recycler要一个ViewHolder,无论是创建的还是复用的,然后取出itemView
View view = layoutState.next(recycler);
// addView(view);的几种情况,最终调用的是ChildHelper的addView方法;
// ……
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
// ……
}
该方法只要是通过访问Recycler,取出一个ViewHolder的itemView,并将它add到RecyclerView当中,在该方法中的 result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);获得了当前view消费的视图高度,主要的逻辑在RecyclerView是如何管理视图的(一) 回收与复用中做了相关的阐述。