上一篇文章讲述了“从哪里获得回收的表项”,这一篇会结合实际回收场景分析下“回收哪些表项?”。
(ps: 下文中的 粗斜体字 表示引导源码阅读的内心戏)
回收场景
在众多回收场景中最显而易见的就是“滚动列表时移出屏幕的表项被回收”。滚动是由MotionEvent.ACTION_MOVE
事件触发的,就以RecyclerView.onTouchEvent()
为切入点寻觅“回收表项”的时机:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 { @Override public boolean onTouchEvent(MotionEvent e) { ... case MotionEvent.ACTION_MOVE: { ... if (scrollByInternal( canScrollHorizontally ? dx : 0, canScrollVertically ? dy : 0, vtev)) { getParent().requestDisallowInterceptTouchEvent(true); } ... } } break; ... } }
去掉了大量位移赋值逻辑后,一个处理滚动的函数出现在眼前:
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 { ... @VisibleForTesting LayoutManager mLayout; ... boolean scrollByInternal(int x, int y, MotionEvent ev) { ... if (mAdapter != null) { ... if (x != 0) { consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState); unconsumedX = x - consumedX; } if (y != 0) { consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState); unconsumedY = y - consumedY; } ... } ... }
RecyclerView
把滚动交给了LayoutManager
来处理,于是移步到最熟悉的LinearLayoutManager
:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider { ... @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 dy, RecyclerView.Recycler recycler, RecyclerView.State state) { if (getChildCount() == 0 || dy == 0) { return 0; } mLayoutState.mRecycle = true; ensureLayoutState(); final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; final int absDy = Math.abs(dy); //更新LayoutState(这个函数对于“回收哪些表项”来说很关键,待会会提到) updateLayoutState(layoutDirection, absDy, true, state); //滚动时向列表中填充新的表项 final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); if (consumed < 0) { if (DEBUG) { Log.d(TAG, "Don't have any more elements to scroll"); } return 0; } final int scrolled = absDy > consumed ? layoutDirection * consumed : dy; mOrientationHelper.offsetChildren(-scrolled); if (DEBUG) { Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled); } mLayoutState.mLastScrollDelta = scrolled; return scrolled; } ... }
沿着调用链往下找,发现了一个上一篇中介绍过的函数LinearLayoutManager.fill()
,原来列表滚动的同时也会不断的向其中填充表项(想想也是,不然怎么会不断有新的表项出现呢~)。上一遍只关注了其中填充的逻辑,但其实里面还有回收逻辑:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider { ... int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { ... int remainingSpace = layoutState.mAvailable + layoutState.mExtra; LayoutChunkResult layoutChunkResult = mLayoutChunkResult; //不断循环获取新的表项用于填充,直到没有填充空间 while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { layoutChunkResult.resetInternal(); if (VERBOSE_TRACING) { TraceCompat.beginSection("LLM LayoutChunk"); } //填充新的表项 layoutChunk(recycler, state, layoutState, layoutChunkResult); if (VERBOSE_TRACING) { TraceCompat.endSection(); } if (layoutChunkResult.mFinished) { break; } layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null || !state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed; // we keep a separate remaining space because mAvailable is important for recycling remainingSpace -= layoutChunkResult.mConsumed; } if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { //在当前滚动偏移量基础上追加因新表项插入增加的像素(这句话对于“回收哪些表项”来说很关键,待会会提到) layoutState.mScrollingOffset += layoutChunkResult.mConsumed; if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } //回收表项 recycleByLayoutState(recycler, layoutState); } if (stopOnFocusable && layoutChunkResult.mFocusable) { break; } ... } ... return start - layoutState.mAvailable; } }
在不断获取新表项用于填充的同时也在回收表项(想想也是,列表滚动的时候有表项插入的同时也有表项被移出),移步到回收表项的函数:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider { ... private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) { if (!layoutState.mRecycle || layoutState.mInfinite) { return; } if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { recycleViewsFromEnd(recycler, layoutState.mScrollingOffset); } else { recycleViewsFromStart(recycler, layoutState.mScrollingOffset); } } ... /** * Recycles views that went out of bounds after scrolling towards the end of the layout. * 当向列表尾部滚动时回收滚出屏幕的表项 * <p> * Checks both layout position and visible position to guarantee that the view is not visible. * * @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView} * @param dt This can be used to add additional padding to the visible area. This is used * to detect children that will go out of bounds after scrolling, without * actually moving them.(该参数被用于检测滚出屏幕的表项) */ private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) { if (dt < 0) { if (DEBUG) { Log.d(TAG, "Called recycle from start with a negative value. This might happen" + " during layout changes but may be sign of a bug"); } return; } // ignore padding, ViewGroup may not clip children. final int limit = dt; final int childCount = getChildCount(); if (mShouldReverseLayout) { for (int i = childCount - 1; i >= 0; i--) { View child = getChildAt(i); if (mOrientationHelper.getDecoratedEnd(child) > limit || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) { // stop here recycleChildren(recycler, childCount - 1, i); return; } } } else { //遍历LinearLayoutManager的孩子找出其中应该被回收的 for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (mOrientationHelper.getDecoratedEnd(child) > limit || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) { // stop here //回收索引为0到i-1的表项 recycleChildren(recycler, 0, i); return; } } } } ... }
原来RecyclerView
的回收分两个方向:1. 从列表头回收 2.从列表尾回收。就以“从列表头回收”为研究对象分析下RecyclerView
在滚动时到底是怎么判断“哪些表项应该被回收?”。
(“从列表头回收表项”所对应的场景是:手指上滑,列表向下滚动,新的表项逐个插入到列表尾部,列表头部的表项逐个被回收。)
回收哪些表项
要回答这个问题,刚才那段代码中套在recycleChildren(recycler, 0, i)
外面的判断逻辑是关键:mOrientationHelper.getDecoratedEnd(child) > limit
。
/** * Helper class for LayoutManagers to abstract measurements depending on the View's orientation. * 该类用于帮助LayoutManger抽象出基于视图方向的测量 * <p> * It is developed to easily support vertical and horizontal orientations in a LayoutManager but * can also be used to abstract calls around view bounds and child measurements with margins and * decorations. * * @see #createHorizontalHelper(RecyclerView.LayoutManager) * @see #createVerticalHelper(RecyclerView.LayoutManager) */public abstract class OrientationHelper { ... /** * Returns the end of the view including its decoration and margin. * <p> * For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right * decoration and 3px right margin, returned value will be 205. * * @param view The view element to check * @return The last pixel of the element * @see #getDecoratedStart(android.view.View) */ public abstract int getDecoratedEnd(View view); ... public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) { return new OrientationHelper(layoutManager) { ... @Override public int getDecoratedEnd(View view) { final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams(); return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin; } ... }
结合注释和该方法的实现,原来mOrientationHelper.getDecoratedEnd(child)
表示当前表项的尾部相对于列表头部的坐标,OrientationHelper
这层抽象屏蔽了列表的方向,所以这句话在纵向列表中可以翻译成“当前表项的底部相对于列表顶部的纵坐标”。
判断条件mOrientationHelper.getDecoratedEnd(child) > limit
中的limit
又是什么鬼?在纵向列表中,“表项底部纵坐标 > 某个值”意味着表项位于某条线的下方,回看一眼“回收表项”的逻辑:
//遍历LinearLayoutManager的孩子找出其中应该被回收的for (int i = 0; i < childCount; i++) { View child = getChildAt(i); //直到表项底部纵坐标大于某个值后,回收该表项以上的所有表项 if (mOrientationHelper.getDecoratedEnd(child) > limit || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) { // stop here //回收索引为0到索引为i-1的表项 recycleChildren(recycler, 0, i); return; } }
隐约觉得limit
应该等于0,这样不正好是回收所有从列表头移出的表项吗?不知道这样YY到底对不对,还是沿着调用链向上找一下limit
被赋值的地方吧~,调用链很长,就不全部罗列了,但其中有两个关键点,其实我在上面的代码中埋了伏笔,现在再罗列一下:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider { ... int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { if (getChildCount() == 0 || dy == 0) { return 0; } mLayoutState.mRecycle = true; ensureLayoutState(); final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; final int absDy = Math.abs(dy); //1. 更新LayoutState(这个函数对于“回收哪些表项”来说很关键,待会会提到) updateLayoutState(layoutDirection, absDy, true, state); //滚动时向列表中填充新的表项 final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); ... } ... int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { ... //不断循环获取新的表项用于填充,直到没有填充空间 while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { ... if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { //2. 在当前滚动偏移量基础上追加因新表项插入增加的像素(这句话对于“回收哪些表项”来说很关键,待会会提到) layoutState.mScrollingOffset += layoutChunkResult.mConsumed; if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } //回收表项 recycleByLayoutState(recycler, layoutState); } ... } ... return start - layoutState.mAvailable; } ... private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, RecyclerView.State state) { ... int scrollingOffset; if (layoutDirection == LayoutState.LAYOUT_END) { mLayoutState.mExtra += mOrientationHelper.getEndPadding(); //获得当前方向上里列表尾部最近的孩子(最后一个孩子) final View child = getChildClosestToEnd(); // the direction in which we are traversing children mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD : LayoutState.ITEM_DIRECTION_TAIL; mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child); // calculate how much we can scroll without adding new children (independent of layout) // 获得一个滚动偏移量,如果只滚动了这个数值那不需要添加新的孩子 scrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding(); } else { ... } ... //对mLayoutState.mScrollingOffset赋值 mLayoutState.mScrollingOffset = scrollingOffset; } }
一图胜千语:
作者:唐子玄
链接:https://www.jianshu.com/p/6eabcb7743b1
共同学习,写下你的评论
评论加载中...
作者其他优质文章