本文承接上一篇RecyclerView详解一
四、LayoutManager
LayoutManager是布局管理器,主要的作用是布局子视图,处理滚动过程中子视图的添加与回收。接下来我会讲一下它是怎么处理表项的加载和布局的。说实话,刚开始讲这个我都不知道从哪入手,在源码中遨游,跳来跳去的,往往看一个方法,就要跳到更多方法去理解这个方法,然后那些方法中还有更多的方法需要理解…
1. LayoutManager管理表项的添加
首先,我们既然要理解滚动过程中表项的状态,肯定还是要从 onTouchEvent() 入手,这个是RV的触摸事件。
public class RecyclerView {
...
@Override
public boolean onTouchEvent(MotionEvent e) {
switch (action) {
// RecyclerView 对滑动事件的处理
case MotionEvent.ACTION_MOVE: {
// 前面是判断启动条件的一些代码,就不展示了
...
// 滚动的内部
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e)) {
...}
}
}
}
}
可以看到,触摸事件调用了 scrollByInternal() 方法,并把滑动的距离传了进去。
boolean scrollByInternal(int x, int y, MotionEvent ev) {
if (mAdapter != null) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
// 在这里调用了 scrollStep(),滑动步骤
scrollStep(x, y, mReusableIntPair);
consumedX = mReusableIntPair[0];
consumedY = mReusableIntPair[1];
unconsumedX = x - consumedX;
unconsumedY = y - consumedY;
}
...
}
scrollStep() 是滚动之前进行的操作,dispatchNestedScroll() 是真正滚动的方法,dispatch是分发调度的意思。
void scrollStep(int dx, int dy, @Nullable int[] consumed) {
int consumedX = 0;
int consumedY = 0;
if (dx != 0) {
consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
}
if (dy != 0) {
consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
}
}
在 scrollStep() 方法里,我们终于看到了 mLayout,这是一个 LayoutManager 对象,说明从这开始,由布局管理器来接管。我们以 LinearLayoutManager 为例,将垂直位移作为参数传给 scrollBy()。
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
// 更新LayoutState
updateLayoutState(layoutDirection, absDelta, true, state);
// 滚动时向列表中填充新的表项
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
}
scrollBy() 中终于找到了我们想看到的 fill(),是填充表项的意思,这个意思是在滚动之前将未显示在界面的表项进行预布局吗?我们继续往下走。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
...
// 如果剩余空间 > 0,就循环添加item,layoutChunk()就是这个作用
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
// 如果 layoutChunk 中将 mFinished 设置为 true ,就退出循环
if (layoutChunkResult.mFinished) {
break;
}
...
}
}
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
// 如果view为空,即 layoutState 没有下一个item了,就结束布局
if (view == null) {
if (DEBUG && layoutState.mScrapList == null) {
throw new RuntimeException("received null view when unexpected");
}
// if we are laying out views in scrap, this may return null which means there is
// no more items to layout.
result.mFinished = true;
return;
}
...
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
// 将获取到的下一个 view 添加进来
addView(view);
} else {
addView(view, 0);
}
}
fill() 方法会根据剩余空间来循环地调用 layoutChunk() 向列表中填充表项,滚动列表的场景中,剩余空间的值由滚动距离决定。而填充用到的 view 其实就是从缓存里取出的,这就回到了之前讲过的缓存复用的步骤中去了。大家看一下源码就懂了~。addView() 就是向列表里添加视图的具体方法,在这里面将要添加的子视图与RecyclerView绑定起来,成为它的子视图。
View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}
这个就是循环填充表项中,获取 layoutState 下一个 view 的方法,它又调用了 Recycler 的getViewForPosition() 方法,通过当前的位置获取 view。
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
大家看到 tryGetViewHolderForPositionByDeadline() 可能就恍然大悟了,这不就是复用吗,从一到四级缓存中获取到 ViewHolder。经过以上分析,我做一下这部分的总结。
我们从 RecyclerView 的 onTouchEvent() 入手,通过调用链一步一步往下分析,终于在 layoutChunk() 方法里找到了LayoutManager 管理表项添加的内容。它是通过 next() 方法从缓存中获取到下一个表项,然后将其添加到列表中,这样,在用户来回滑动的过程中,在屏幕之外的 item 就可以正常的显示出来。而子视图要显示在什么位置是需要进行测量的,知道了显示的位置,才可以将它布局到界面上。那我们就来看看是怎么布局的。
// 还是这个方法里
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
...
// 测量获取到的子视图所占的空间和位置
measureChildWithMargins(view, 0, 0);
// 得到填充该视图所需要消耗的空间
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
...
// 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);
}
经过这个方法后布局子视图后,回到 fill() 中,剩余空间 remainingSpace 将布局子视图消耗的空间减掉以作为循环退出条件,所以最多会填充 remainingSpace 容量大小的表项,这个大小是通过滚动位移绝对值计算的,源码我就不放了。
if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
|| !state.isPreLayout()) {
layoutState.mAvailable -= layoutChunkResult.mConsumed;
// we keep a separate remaining space because mAvailable is important for recycling
remainingSpace -= layoutChunkResult.mConsumed;
}
好啦,至此我们就应该知道 LayoutManager 是如何添加和布局表项的了。以下两条就是阅读源码总结出的精华。
RecyclerView 在滚动发生之前,会有一个填充新表项的动作,填充的是当前还未显示的表项。
RecyclerView 填充表项是通过
while
循环实现的,当列表没有剩余空间时,填充表项也就结束了。
2. LayoutManager管理表项的回收
LayoutManager 的另一个作用便是回收了,有新表项入就有旧表项出。我在讲表项的添加的时候,有一个 **fill() ** 函数,它完成列表的填充,但填充之后的代码我没有放,现在来给大家看一下。
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
...
// 添加的操作
layoutChunk(recycler, state, layoutState, layoutChunkResult);
...
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
...
// 回收的操作
recycleByLayoutState(recycler, layoutState);
}
}
fill() 每次添加一个表项之后,都会执行一次回收操作,我们继续沿着调用链走。
private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
...
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
recycleViewsFromEnd(recycler, scrollingOffset, noRecycleSpace);
} else {
recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
}
}
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,
int noRecycleSpace) {
...
// 一条控制线
final int limit = scrollingOffset - noRecycleSpace;
// 列表中子视图的数量
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// getDecoratedEnd(item)就是获取该item底部纵坐标的方法
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
// stop here
// 回收子视图的方法,会将旧的表项从列表中删除
recycleChildren(recycler, 0, i);
return;
}
}
}
**注意!**重点来了,recycleViewsFromStart() 中出现了一个 limit,这个 limit 是什么?大家之前可能有个疑问,说是将旧的表项回收掉,但是从哪里开始回收呢,又该回收多少呢?而这个 limit 就是解答疑问的关键,它是一条隐形的控制线,控制的就是回收的具体位置。 mOrientationHelper.getDecoratedEnd(child) > limit
当表项底部的纵坐标大于 limit 的值时,就回收该表项。也就是说位于 limit 控制线上的表项就会被回收。用一张图理解一下~
limit 的值为 scrollingOffset - noRecycleSpace ,其中 noRecycleSpace = 0(源码中被赋值为零),那么 limit 就只与 scrollingOffset 有关了,那这个 scrollingOffset 又是怎么计算的呢?我们在源码中继续寻找。
private void updateLayoutState(int layoutDirection, int requiredSpace,
boolean canUseExistingSpace, RecyclerView.State state) {
...
// 获取到列表最后一个item
final View child = getChildClosestToEnd();
// 计算 scrollingOffset
// getDecoratedEnd()获取列表最后一个item底部的位置,getEndAfterPadding()获取列表底部的位置
scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
- mOrientationHelper.getEndAfterPadding();
// 为 mScrollingOffset 赋值
mLayoutState.mScrollingOffset = scrollingOffset;
}
它是在这里被引用的,大家一定不陌生。
int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
...
// 滑动位移绝对值
final int absDelta = Math.abs(delta);
// 更新 LayoutState
updateLayoutState(layoutDirection, absDelta, true, state);
// 计算表项消耗的像素
final int consumed = mLayoutState.mScrollingOffset
+ fill(recycler, mLayoutState, state, false);
...
}
在 updateLayoutState() 方法里,scrollingOffset 的大小为列表最后一个 item 的底部到列表底部的距离,这样 limit 的值就算出来了。比较抽象的一个概念,我也用一张图来带大家理解一下。
用户上拉下滑时,limit 的值如此计算,可能有人会问到,limit 线不是在上面的吗,怎么在下面计算?没错!因为下面的这段距离也正是 limit 线离顶部的距离。因此,我们可以认为,limit 的值就是这一次滚动的总距离,limit当前所在位置,在滚动完成后会和列表顶部重合 。这里我也画了一张图来助大家理解。
相信大家看了图就会更理解了一些,结合前面所讲,limit 线上面就是需要回收的(即item1),而列表底部下面的是将要添加进列表的(即item8)。但是 item8 之后的 item 呢?答案是,随着我们的滚动,用于计算 limit 值的 scrollingOffset 在不断变大,因为每添加一个新表项,它就会更新值,将新表项消耗的像素值加进来。这样 limit 的值也就在不断地变化,是动态更新的。
在循环填充新表项时,新表项占用的像素值每次都会追加到
layoutState.mScrollingOffset
,即它的值在不断增大(limit 隐形线
在不断下移)。在一次while
循环的最后,会调用recycleByLayoutState()
根据当前limit 隐形线
的位置回收表项。
我们通过源码看一下
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);
...
// 每次添加一个新表项,都会重新计算 mScrollingOffset 的值
layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
// 计算后,回收 limit 线上面的表项
recycleByLayoutState(recycler, layoutState);
}
}
看到这,我相信你们就更加的理解了 LayoutManager 的回收机制
了,也能够将它和添加机制
连接起来。
3. 总结
讲完了 LayoutManager 的添加和回收机制,最后我来总结一下:
RecyclerView 在滚动发生之前,会根据预计滚动位移大小来决定需要向列表中填充多少新的表项。
RecyclerView 填充表项是通过
while
循环一个一个实现的,当列表没有剩余空间时,填充表项也就结束了。RecyclerView 滑动发生之前,会计算出一条 limit 控制线,它是决定哪些表项该被回收的重要依据。它可以理解为:隐形线当前所在位置,在滚动完成后会和列表顶部重叠
limit 控制线的初始值 = 列表当前可见表项的底部到列表底部的距离,即列表在不填充新表项时,可以滑动的最大距离。每一个新填充表项消耗的像素值都会被追加到 limit 值之上,即 limit 控制线会随着新表项的填充而不断地下移。
触发回收逻辑时,会遍历当前所有表项,若某表项的底部位于 limit 控制线下方,则该表项上方的所有表项都会被回收。
以上是我对源码的分析与看别人博客所总结,如果有不正确之处,还请大家批评指正~