写点什么

图文 DEMO 并茂讲解 RecyclerView 滑动时回收和复用触发的时机

用户头像
Android架构
关注
发布于: 刚刚

![](


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


https://img-blog.csdnimg.cn/img_convert/48c2ebacaa3339d8e1d512d07bfb0cd4.png)


程序代码


class RecyclerViewActivity2 : AppCompatActivity() {


private lateinit var mRecyclerView: RecyclerView


override fun onCreate(savedInstanceState: Bundle?) {


super.onCreate(savedInstanceState)


setContentView(R.layout.activity_recycler_view2)


mRecyclerView = findViewById(R.id.recyclerview)


mRecyclerView.setHasFixedSize(true)


mRecyclerView.setItemViewCacheSize(0)


mRecyclerView.layoutManager =


LinearLayoutManager(this).apply {


orientation = LinearLayoutManager.VERTICAL


isItemPrefetchEnabled = false


}


val list: MutableList<String> =


ArrayList()


repeat(100) {


list.add("item $it")


}


mRecyclerView.adapter = MyAdapter(list)


}


inner class MyAdapter(val mStrings: MutableList<String>) :


RecyclerView.Adapter<RecyclerView.ViewHolder>() {


override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {


println("RecyclerView 场景二 onCreateViewHolder ")


val view = LayoutInflater.from(parent.context)


.inflate(R.layout.view_item, parent, false)


return object : RecyclerView.ViewHolder(view) {}


}


override fun getItemCount(): Int {


return mStrings.size


}


override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {


println("RecyclerView 场景二 onBindViewHolder $position ")


val textView = holder.itemView as TextView


textView.layoutParams.height =


if (position == 0) (resources.displayMetrics.density * 50).toInt() else (resources.displayMetrics.density * 100).toInt()


textView.text = mStrings[position]


}


override fun onViewRecycled(holder: RecyclerView.ViewHolder) {


println("RecyclerView 场景二 发生回收 " + (holder.itemView as TextView).text)


super.onViewRecycled(holder)


}


}


fun scroll120(view: View) {


mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 120).toInt())


}


fun scroll60(view: View) {


mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 60).toInt())


}


fun scroll40(view: View) {


mRecyclerView.scrollBy(0, (resources.displayMetrics.density * 40).toInt())


}


}


日志输出首先进入初始状态



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 0



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 1



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 2



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 3



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 4



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 5



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 6


点击上滑 40px。打印日志不变。证明 回收和复用都没有发生


点击上滑 60px。打印日志如下。证明 发生回收,没有发生复用



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 0



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 1



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 2



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 3



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 4



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 5



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 6



RecyclerView 场景二 发生回收 item 0 //只发生了回收


点击上滑动 120px。打印日志如下。证明 发生了回收和复用。先回收后复用



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 0



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 1



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 2



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 3



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 4



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 5



RecyclerView 场景二 onCreateViewHolder



RecyclerView 场景二 onBindViewHolder 6



RecyclerView 场景二 发生回收 item 0 //先回收



RecyclerView 场景二 onBindViewHolder 7 //再复用


3. 滑动原理分析





如图所示,介绍几个关于坐标的参数


  1. delta:手指滑动的距离 120px。

  2. mOffset:RV 最后一个子 View 的 Bottom 在屏幕坐标系的 Y 坐标 600px。RV 的下一个 View(Item7)从 mOffset 处布局。

  3. mScrollingOffset:RV 最后一个子 view 的 Bottom 距离 RV Bottom 的距离 50px。向上滑动不超过该距离。如超过需创建新的 View 填充。

  4. mVailable:delta-mScrollingOffset。可以填充 View 的空间。如果大于 0 表示有空间填充新的 View

  5. 如果 delta<mScrollingOffset,mScrollingOffset=delta,mVailable<0


滑动逻辑如下


  1. 从 RecyclerView 的第 0 个 View 开始遍历,直到 View 的 Bottom>mScrollingOffset,并记录该 View 的下标 index,回收[0,index)区间的 View,index 为开区间,如果 index>=1,则会将[0,index)区间的 View 移除屏幕,并按照回收算法放入回收池。具体回收算法先按下不表。

  2. 如果 mVailable>0,则从 mOffset 处,用新的 View 填充。mOffset+=新 View 的高度,mVailable-=新 View 的高度,mScrollingOffset+=新 View 的高度,如果 mVailable<0,mScrollingOffset+=mVailable。布局完成后用步骤 1 的算法按需回收上面的 View。

  3. 重复步骤 2

  4. 将 RV 整体,向上移动 delta 或者 consumed 距离(一般是 delta 距离,但是当 RecyclerView 下面没有 Item 时会是具体消耗掉的距离)


根据此滑动逻辑,我们分析场景一中的向上滑动 120px



mOffset = 600px


mScrollingOffset = 50px


mAvailable = 70px


item1 高度 100px


  1. 首先从第 0 个 View 遍历 Bottom>50px。找到 item1.bottom=100px,记录 index=0。因为 index<1。所以不发生回收

  2. mAvailable>0,从 Item6 的底部,增加 View Item7(此处发生复用逻辑)高度为 100px,mOffset=700px,mAvailable=-30,mScrollingOffset=mScrollingOffset+100-30=120px。然后检查回收。首先从第 0 个 View 遍历 Bottom>120px。找到 item2.bottom=200px,记录 index=1。回收[0,1)区间的 View。即回收 Item1

  3. mAvailable=-30<0,退出填充逻辑

  4. 整体向上移动 120px


我们看到先创建 Item7 然后回收 Item1。跟日志相符合


RecyclerView 场景一 onCreateViewHolder //先复用


RecyclerView 场景一 onBindViewHolder 6


RecyclerView 场景一 发生回收 item 0 //后回收


同样的逻辑我们也可以分析场景二中的向上滑动 120px 的情况。场景二会先发生回收,再发生复用。读者可以自己去求证。


4.源码分析




RV 的滑动,最终会调用 LayoutManager 的 scrollBy 方法。我们使用的是 LinearLayoutManager。


//LinearLayoutManager.java


int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {


if (getChildCount() == 0 || delta == 0) {


return 0;


}


ensureLayoutState();


mLayoutState.mRecycle = true;


final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;


final int absDelta = Math.abs(delta);


##代码 1 updateLayoutState 方法,主要是计算 mOffset 等参数。


updateLayoutState(layoutDirection, absDelta, true, state);


##代码 2 fill 方法,根据剩余空间,填充 View


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 = absDelta > consumed ? layoutDirection * consumed : delta;


    ##代码 3 offsetChildren,整体移动 RV 的子 View


    mOrientationHelper.offsetChildren(-scrolled);


    if (DEBUG) {


    Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);


    }


    mLayoutState.mLastScrollDelta = scrolled;


    return scrolled;


    }


    ##代码 1 updateLayoutState 方法,主要是计算 mOffset 等参数。


    ##代码 2 fill 方法,根据剩余空间,填充 View


    ##代码 3 offsetChildren,整体移动 RV 的子 View


    //主要是计算


    private void updateLayoutState(int layoutDirection, int requiredSpace,


    boolean canUseExistingSpace, RecyclerView.State state) {


    // If parent provides a hint, don't measure unlimited.


    mLayoutState.mInfinite = resolveIsInfinite();


    mLayoutState.mLayoutDirection = layoutDirection;


    mReusableIntPair[0] = 0;


    mReusableIntPair[1] = 0;


    calculateExtraLayoutSpace(state, mReusableIntPair);


    int extraForStart = Math.max(0, mReusableIntPair[0]);


    int extraForEnd = Math.max(0, mReusableIntPair[1]);


    boolean layoutToEnd = layoutDirection == LayoutState.LAYOUT_END;


    mLayoutState.mExtraFillSpace = layoutToEnd ? extraForEnd : extraForStart;


    mLayoutState.mNoRecycleSpace = layoutToEnd ? extraForStart : extraForEnd;


    int scrollingOffset;


    if (layoutToEnd) {


    mLayoutState.mExtraFillSpace += mOrientationHelper.getEndPadding();


    // get the first child in the direction we are going


    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 {


    final View child = getChildClosestToStart();


    mLayoutState.mExtraFillSpace += mOrientationHelper.getStartAfterPadding();


    mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL


    : LayoutState.ITEM_DIRECTION_HEAD;


    mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;


    mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child);


    scrollingOffset = -mOrientationHelper.getDecoratedStart(child)


    • mOrientationHelper.getStartAfterPadding();


    }


    mLayoutState.mAvailable = requiredSpace;


    if (canUseExistingSpace) {


    mLayoutState.mAvailable -= scrollingOffset;


    }


    mLayoutState.mScrollingOffset = scrollingOffset;


    }


    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,


    RecyclerView.State state, boolean stopOnFocusable) {


    // max offset we should set is mFastScroll + available


    final int start = layoutState.mAvailable;


    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {


    // TODO ugly bug fix. should not happen


    if (layoutState.mAvailable < 0) {


    layoutState.mScrollingOffset += layoutState.mAvailable;


    }


    ##代码 1 首先判断是否需要回收 View


    recycleByLayoutState(recycler, layoutState);


    }


    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;


    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;


    ##代码 2 根据剩余空间,判断是否需要填充 View


    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {


    layoutChunkResult.resetInternal();


    if (RecyclerView.VERBOSE_TRACING) {


    TraceCompat.beginSection("LLM LayoutChunk");


    }


    ##代码 3 是具体的 layout 方法


    layoutChunk(recycler, state, layoutState, layoutChunkResult);


    if (RecyclerView.VERBOSE_TRACING) {


    TraceCompat.endSection();


    }


    if (layoutChunkResult.mFinished) {


    break;


    }


    layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;


    /**


    • Consume the available space if:

    • layoutChunk did not request to be ignored

    • OR we are laying out scrap children

    • OR we are not doing pre-layout


    */


    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;


    }


    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {


    layoutState.mScrollingOffset += layoutChunkResult.mConsumed;


    if (layoutState.mAvailable < 0) {


    layoutState.mScrollingOffset += layoutState.mAvailable;


    }


    ##代码 4 是 layout 完成后判断是否需要回收 View


    recycleByLayoutState(recycler, layoutState);


    }


    if (stopOnFocusable && layoutChunkResult.mFocusable) {


    break;


    }


    }


    if (DEBUG) {


    validateChildOrder();


    }


    return start - layoutState.mAvailable;


    }


    ##代码 1,首先判断是否需要回收 View


    ##代码 2,根据剩余空间,判断是否需要填充 View


    ##代码 3 是具体的 layout 方法


    ##代码 4 是 layout 完成后判断是否需要回收 View


    本文主要讲解了滑动时的回收和复用的逻辑。具体如何如何回收,如何复用。RecyclerView 的三级缓存是如何实现的。且听下回分解。


    5. 提问互动




    最后为了巩固大家对知识的理解,提出一个问题,请在评论区写出你的答案吧。



    问题一 场景一的 case3,向上滑动 120px,120px 大于第一个 Item 的高度 100px,为何不先让 Item1 先回收掉呢?

    用户头像

    Android架构

    关注

    还未添加个人签名 2021.10.31 加入

    还未添加个人简介

    评论

    发布
    暂无评论
    图文DEMO并茂讲解RecyclerView滑动时回收和复用触发的时机