RecyclerView详解二,LayoutManager分析

本文承接上一篇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 的方法,它又调用了 RecyclergetViewForPosition() 方法,通过当前的位置获取 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线理解

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 的添加和回收机制,最后我来总结一下:

  1. RecyclerView 在滚动发生之前,会根据预计滚动位移大小来决定需要向列表中填充多少新的表项。

  2. RecyclerView 填充表项是通过while循环一个一个实现的,当列表没有剩余空间时,填充表项也就结束了。

  3. RecyclerView 滑动发生之前,会计算出一条 limit 控制线,它是决定哪些表项该被回收的重要依据。它可以理解为:隐形线当前所在位置,在滚动完成后会和列表顶部重叠

  4. limit 控制线的初始值 = 列表当前可见表项的底部到列表底部的距离,即列表在不填充新表项时,可以滑动的最大距离。每一个新填充表项消耗的像素值都会被追加到 limit 值之上,即 limit 控制线会随着新表项的填充而不断地下移。

  5. 触发回收逻辑时,会遍历当前所有表项,若某表项的底部位于 limit 控制线下方,则该表项上方的所有表项都会被回收。

以上是我对源码的分析与看别人博客所总结,如果有不正确之处,还请大家批评指正~

猜你喜欢

转载自blog.csdn.net/m0_51276753/article/details/126689572
今日推荐