自定义 RecyclerView.LayoutManager

463 阅读16分钟

RecyclerView 整体架构

image.png

  • RecyclerView的本质是ViewGroup,因此它需要将大量数据对象映射成View对象集合,将View对象集合中的View作为自己的子View在自身区域内布局展示。

  • 由于待展示的数据集可能是无限的,所以可能存在一个无限大的View对象集合,但RecyclerView自身区域有限,无法同时在这个区域内展示出所有子View,这意味着它需要提供按需在自身区域内布局出应被展示的子View的能力,并通过手势操作替换正在展示的子View的能力,相对应的,在替换展示的View时需要及时回收已经不被展示的View对象。

  • 如果数据集合中的大多数据,都可以使用同一种View形态展示出来,则意味着对每一个数据对象都创建一个对应的View是浪费的,如果用type来区分数据对象期望的View展示形态,RecyclerView需要拥有根据type安排数据对象复用View对象的能力,即同一种type的不同数据对象可以在不同时机复用同一个View对象进行展示。

对上述能力,RecyclerView安排不同的类进行处理,依次分别对应:

  • Adapter
  • LayoutManager
  • Recycler

RecyclerView 成员

ViewHolder

如果用Item表示数据集中的一个数据对象,ItemView表示用来展示该Item的View对象。ViewHolder负责在RecyclerView中承载一个ItemView,除了维护View对象本身外,还维护着Item位置(position,此处的位置是指Item在数据集中的次序)、item类型、item Id等。大部分时候,RecyclerView内部或其辅助类并不会直接操作View,而是对ViewHolder进行操作。

在阅读RecyclerView源码时发现,一些操作需要用View做参数,也有一些操作需要用ViewHolder做参数,实际上在RecyclerView中,可以通过任意一样拿到另一样,不必太过纠结RecyclerView的各个内部类的变量中究竟维护的是哪种类型。

image.png

Adapter负责根据数据Item创建对应的ViewHolder;Recycler负责管理ViewHolder,根据实际情况创建新的ViewHolder或复用已有的ViewHolder;LayoutManager可以通过Recycler直接获取到View,负责将其添加到RecyclerView的布局中,并通过Recycler回收已经不被展示的View。

image.png

Adapter

Adapter负责将数据对象映射为View对象。待展示的数据集维护在Adapter内,Adapter除了负责将数据映射为View外,也会向外分发数据集中数据的变化。

将数据对象映射为可用来展示的View对象,在RecyclerView体系中被拆分为两步:

  • 步骤1:根据itemType创建符合预期的ViewHolder对象,此处更关注于View的结构样式。
  • 步骤2:根据position从Adapter维护的数据集中获取数据对象,将数据对象与ViewHolder中的View进行绑定,此处更关注View展示出的数据内容。

在代码实现中,RecyclerView.Adapter基类有两个待使用者实现的回调方法,这两个方法会分别被Adapter.createViewHolder和Adapter.bindViewHolder调用,对应着步骤1与步骤2。

public abstract VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType);

public abstract void onBindViewHolder(@NonNull VH holder, int position);

Recycler

Recyler负责管理ViewHolder,它可以回收起已经不被展示的ViewHolder,并在恰当的时候复用这些ViewHolder。它最重要的一个能力就是根据position提供一个ViewHolder/View,使用者无需关心这个ViewHolder是新创建的还是复用已有的,Recycler帮助RecyclerView处理ViewHolder的缓存。

View的detach vs remove

在了解Recycler对View的两种处理方式前,我们先看一下View被添加到父View后其状态的流转。View被add到parent后,除了可以被remove外,还有一个更轻量级的detach操作,detached表示一种临时状态,意味着这个View在之后会马上被重新attach或彻底remove。如果一个View处于detached状态,像被remove一样它也无法通过其parent的getChildAt方法获得。

image.png

Recycle的scrap vs recycle

相对应的,Recycler对ViewHolder也有两种处理方式:scrap和recycle。

scrap通常和detach操作共同使用,如果使用Recycler对一个View进行scrap操作,表示期望该View已经处于detach状态(而不是removed状态),持有这个View的ViewHolder会被标记为scrap状态,然后临时存放到Recycler.mAttachedScrap列表中,等待进一步的处理(unScrap或recycle)。scrap是一种临时操作,通常表示该View之前在屏幕中展示,并且之后大概率也会继续展示,不希望被remove回收掉。mAttachedScrap是一个ArrayList,存放着没有被remove的子View的ViewHolder。

recycle通常和remove操作共同使用,如果使用Recycler对一个View进行recyle操作,表示期望该View已经从其parent中remove掉,并且持有该View的ViewHolder是unScrap状态。当ViewHolder及其View的状态都满足条件后,RecyclerView会将这个ViewHolder放入Recycler的缓存池中。recycle操作只针对已经被remove掉的View,它之前是被展示在屏幕中的,但由于滑动操作或数据集改变等因素,该View不再继续展示,此时它可以被回收起来等待复用。这也是本文认为RecyclerView是3级缓存的原因,只有被remove掉的View才有机会被回收缓存。

RecyclerView.Recycler的源码中,有一些方法或变量的命名也与scrap有关,但观察其使用,实际上都在做recycle的工作。

缓存回收

了解detach/remove和scrap/recycle的区别后,RecyclerView的缓存机制变得更易读一些,缓存实际上是Recycler中存放ViewHolder的集合的变量,Recycler中用来表示三级缓存的变量的优先级从高到低分别为:mCacheViews、mViewCacheExtension和mRecyclerPool。其中mViewCacheExtension是自定义缓存,本文不做展开,只看mCacheView和mRecyclerPool,首先需要明确的是,这两者缓存的内容都是已经不在屏幕内展示的ViewHolder。

mCacheViews是更高效的缓存,既不需要创建ViewHolder步骤,也不需要重新绑定ViewHolder步骤,这意味着只有在数据对象完全匹配的时候,即待展示的数据Item与缓存的ViewHolder中维护的数据Item完全匹配时(ItemType与Item都相同),才会复用mCacheViews中的ViewHolder。

mRecyclerPool中缓存的ViewHolder对象的使用条件,相较于mCacheViews要求更低,只需ItemType匹配,即可复用ViewHolder,但使用时需要重新绑定ViewHolder。

简单介绍mCacheViews和mRecyclerPool数据结构上的区别。mCacheViews是一个ArrayList,可以存放ViewHolder类型的对象,mRecyclerPool是RecycledViewPool对象,此处先简单理解成一种Map<Int, ArrayList>类型的数据结构(实际上RecyclerView中并不是用Map实现的),Int表示itemType,ArrayList用来存放该itemType的ViewHolder对象。

Recycler回收ViewHolder的规则为:

  • 如果mCacheViews.size未达到最大size,则将该ViewHolder对象add到mCacheViews中;如果size已经达到最大值,则移除mCacheViews中最先被add的ViewHolder,再将待回收的ViewHolder添加到mCacheViews中。
  • 如果mCacheViews.size已经达到最大size,将最先被add到mCacheViews的ViewHolder对象从mCacheViews移除后,尝试将其回收到mRecyclerPool中,无论该ViewHolder是否成功回收到mRecyclerPool中,都会将这个ViewHolder对象从mCacheViews中移除。
  • 如果mRecyclerPool中可以存放这个ViewHolder的itemType的List的size(默认为5)已经达到最大值,则直接抛掉该ViewHolder对象,否则add到这个List中。

image.png

Recycler获取View

Recycler可以根据一个给定的position获得一个可以直接用来展示的ViewHolder。Adapter将数据对象映射为View对象分成了两步进行,即创建View和绑定数据,Recycler从自己内部不同的地方获取ViewHolder,调用Adapter的步骤也略有区别。

image.png

  • Recycler先尝试从mAttachedScrap中获取可用的ViewHolder(可以认为该ViewHolder在复用前与复用后对应着同一个Item数据对象,且这个数据对象无变化),这里获取到的ViewHolder可以直接使用,既不需要执行Adapter.createViewHolder,也不需要执行Adapter.bindViewHolder。
  • 如果未从mAttachedScrap中取到可用的ViewHolder,Recycler会尝试去缓存中获取,本文省略自定义缓存一层的介绍,Recycler会先从mCacheViews中尝试获取到符合要求的ViewHolder对象,与从mAttachedScrap中获取到的ViewHolder相似,该ViewHolder可以直接使用。
  • 如果mCacheViews中依然没有满足条件的ViewHolder,则尝试从mRecyclerPool中获取到符合要求的ViewHolder,这里获得的ViewHolder itemType可以匹配,即View的结构样式满足需求,但需要重新进行数据绑定,即不需要执行Adapter.createViewHolder,但需要执行Adapter.bindViewHolder。
  • 如果Recycler没有从缓存中得到符合要求的ViewHolder,会完整的执行Adapter的两个步骤。

Recycler小结

此时我们已经大致了解怎样通过position(此处的position依然指代一个数据对象在Adapter维护的数据集中的次序,换句话说,我们可以用position表示一个特定的数据对象)来获得一个可用的ViewHolder,并且也清楚Recycler拥有两种操作View/ViewHolder的能力:scrap和recycle,来临时保存或缓存一些ViewHolder。那么是谁,在什么时候希望获取到可用来展示的ViewHolder?又是谁在什么时机会调用Recycler临时保存或回收ViewHolder?将目光放到RecyclerView必不可少成员的最后一员:LayoutManager。

LayoutManager

LayoutManager是RecyclerView中实际决定ItemView摆放规则与滑动规则的执行者,甚至可以决定ItemView的一些布局参数。LayoutManager中有几个待实现的抽象方法和空方法,给使用者充分的自由通过扩展LayoutManager实现自己想要的列表效果或滚动效果。

// 创建ItemView默认的LayoutParams
public abstract LayoutParams generateDefaultLayoutParams();
// 布局RecyclerView的子View
public void onLayoutChildren(Recycler recycler, State state) {
    Log.e(TAG, "You must override onLayoutChildren(Recycler recycler, State state) ");
}
// RecyclerView是否支持水平滑动
public boolean canScrollHorizontally() {
    return false;
}
// RecyclerView是否支持垂直滑动
public boolean canScrollVertically() {
    return false;
}
// 处理RecyclerView的水平滑动
public int scrollHorizontallyBy(int dx, Recycler recycler, State state) {
    return 0;
}
// 处理RecyclerView的垂直滑动
public int scrollVerticallyBy(int dy, Recycler recycler, State state) {
    return 0;
}

ItemDecoration

ItemDecoration可以让使用者向ItemView添加特殊的绘制和布局偏移。 此处先不对绘制进行展开,着重介绍布局偏移。ItemDecoration可以通过重写getItemOffsets方法自定义ItemView的间距,getItemOffsets方法中使用的Rect记录四个值,这四个值类似于ItemView的padding或margin的概念,分别对应left、right、top、bottom。该方法会在LayoutManager measure ItemView时调用,并将值对应的添加到ItemView measure后的宽高结果中。

image.png

需要注意的是:为RecyclerView设置ItemDecoration的方法是add而不是set,在RecyclerView中,维护的是ItemDecoration集合,layout过程中measure ItemView时,会累积计算ItemDecoration集合中offset的值。个人在使用ItemDecoration时,出现过重复对RecyclerView设置同一个ItemDecoration,导致间距表现不符预期的情况,多发生在页面刷新场景。

ItemAniamtor

它用来定义Adapter中维护的数据集发生变化时ItemView需要执行的动画效果,例如删除某个正在展示中的ItemView对应的Item数据时,该ItemView需要执行的消失动画,以及由于它的消失其它ItemView需要执行的位移动画等。

LayoutManager的工作

LayoutManager的工作实际上是帮助RecyclerView决定子View的位置,并且这项工作并不一定只在RecyclerView.onLayout方法中完成。

RecyclerView如何实现布局与绘制

为了了解LayoutManager是在什么时机开始布局ItemView,可以先回到RecyclerView中,RecyclerView作为一个ViewGroup,逃不掉measure、layout、draw三大流程。

measure

RecyclerView为LayoutManager提供了自定义onMeasure方法的机会,如果LayoutManager期望RecyclerView使用自定义的onMeasure方法,可以通过重写isAutoMeasureEnabled方法返回false禁用RecyclerView的autoMeasure策略,实际上,该方法默认返回false,但大多情况下,常用的LayoutManager此处都返回true。需要特别注意的是,当isAutoMeasureEnabled返回true时,不应重写LayoutManager的onMeasure方法。

image.png

layout

RecyclerView的layout过程分为3个步骤,且三个步骤对应的方法命名也非常简单粗暴:

  • dispatchLayoutStep1
  • dispatchLayoutStep2
  • dispatchLayoutStep3

与之相对应的是RecyclerVIew中State类(State类中记录各种可能会使用到的信息)中的mLayoutStep变量可能的三个取值(mLayoutStep变量实际上是int类型,为了便于阅读这里列出其可能取值的常量名):

  • STEP_START
  • STEP_LAYOUT
  • STEP_ANIMATIONS

image.png

RecyclerView中一次完整的layout过程需要至少调用一次dispatchLayoutStep1、dispatchLayoutStep2、dispatchLayoutStep3,其中dispatchLayoutStep2可能被多次调用。onMeasure中可能会提前进行layout的部分过程,是指dispatchLayoutStep1和dispatchLayoutStep2,如果onMeasure中已经完成layout的前两步工作,大多数情况下onLayout中仅需执行dispatchLayoutStep3即可,如果onMeasure中未提前进行layout的前两步,则需要在onLayout中完整的执行一次layout过程。

image.png

虽然经常说RecyclerView将layout的能力外包给LayoutManager处理,但实际上RecyclerView只是将布局子View的能力交由LayoutManager处理,RecyclerView在layout过程中还会进行预布局pre-layout等其它操作。

在layout过程中,第二步即dispatchLayoutStep2中会调用LayoutManager的onLayoutChildren方法,这一步通常也被认为是实际的布局过程post-layout,在这一步将需要在屏幕上展示的ItemView添加到RecyclerView中,并进行ItemView的measure和layout;layout过程中的第一步与第三步则主要是为了服务于RecyclerView的动画(ItemAnimator),在第一步先进行一次pre-layout,再在第三步比较pre-layout和post-layout的区别,进而触发ItemAnimator的动画执行。

draw

RecyclerView的绘制过程中特殊处理相对较少,本文只对ItemDecoration相关的流程简单介绍。在绘制ItemView前,RecyclerView会先遍历其维护的ItemDecoration列表,执行ItemDecoration的onDraw方法,绘制出的内容在ItemView的下层;ItemView完成绘制后,执行ItemDecoration的onDrawOver方法,绘制出的内容在ItemView的上层。

image.png

滚动处理

我们知道,用户的手指在列表上移或下移,会导致列表滚动,因此我们到RecyclerView.onTouchEvent的ACTION_MOVE分支中,观察它有没有与scroll相关的处理,发现RecyclerView在接收到ACTION_MOVE的消息后,经过了一系列的计算与判断,可以得到手势滑动导致的列表水平方向和垂直方向的位移dx与dy,然后调用RecyclerView内部的scrollByInternal方法处理滚动的位移值dx与dy,最终进入scrollStep方法,根据dx与dy分别调用LayoutManager的scrollHorizontallyBy/scrollVerticallyBy方法,把滚动导致的子View的移动和布局工作外包给了LayoutManager处理,同时LayoutManager在处理滚动时也需要及时的使用Recycler处理不在屏幕中继续展示的View。

image.png

关于RecyclerView滚动需要注意的是,以谷歌提供的LinearLayoutManager为例,它在处理滚动时,是调用View提供的offsetTopAndBottom方法平移已经展示在屏幕中的ItemView,并使用fill方法向滚动产生的空白区域添加View和处理已经不在屏幕展示的View,在这个过程中,与LayoutManager.onLayoutChildren方法并无关联。一次正常的滚动过程不会导致RecyclerView的重复布局,因此一次正常的列表滚动不会触发ItemAnimator的任何动画。

数据更新处理

RecyclerView在设置Adapter时,会创建RecyclerViewDataObserver对象注册监听Adapter中的Observable。RecyclerViewDataObserver做的事情其实就是在Adapter的数据集发送改变或其中的某个数据发生改变时,在合适的情况下requestLayout,重新完成一次RecyclerView的layout过程,这个时候才是触发ItemAnimator相应动画的时机。

关于观察者模式此处不做赘述,只需明确注册监听后,RecyclerView可以接收到Adapter.notifyXXX的消息即可,然后将注意力放到RecyclerViewDataObserver中,关注其对notify消息的具体处理。

首先明确数据更新的几种类型:

  • 数据集全量更新(DataSetChanged)

  • 数据集局部更新

    • 局部Item改变(ItemChanged/ItemRangeChanged)
    • 新的Item插入(ItemInserted)
    • 已有Item删除(ItemRemoved)
    • 已有Item移动(ItemMoved)

观察RecyclerViewDataObserver中用来处理数据更新的方法,发现这些方法中都使用到了同一个帮助类:AdapterHelper。在AdapterHelper中,将数据更新行为抽象成UpdateOp类,每个UpdateOp对象表示一次数据更新操作,AdapterHelper中维护着一个待处理的更新操作列表mPendingUpdates(ArrayList)。

如果Adapter触发了一次全量更新,那么RecyclerViewDataObserver中的处理方法会在mPendingUpdates列表为空时requestLayout,进而触发RecyclerView的重新布局;如果Adapter触发了局部更新(包括ItemChange/ItemInsert/ItemRemove/ItemMove等),那么RecyclerViewDataObserver中的处理方法会在mPendingUpdates列表的size为1时requestLayout触发RecyclerView的重新布局。

image.png

小结

至此,已经了解RecyclerView的几个重要成员和它们的基本职责,以及它们与LayoutManager之间的关联:

  1. Adapter根据数据对象的type提供View,并提供View和数据间的绑定关系,LayoutManager不需要与Adapter打交道。
  2. Recyler可以根据position提供一个可以直接用来展示的View,它还负责管理已经不被展示的View。LayoutManager需要直接与Recycler打交道,它在onLayoutChildren时向Recycler索要可以用来展示的View,并在处理滑动时将不再展示的View交由Recycler处理。
  3. ItemDecoration可以处理ItemView的布局偏移,LayoutManager在measure ItemView时会将其计算在内。
  4. ItemAnimator用来定义数据集发生改变时ItemView需要执行的动画,LayoutManager与其并无直接的联系。ItemAnimator定义的动画的执行时机是由RecyclerView的layout过程触发的,正常的列表滑动不会触发RecyclerView的重复布局,因此列表滑动时也不会触发ItemAnimator的执行。

另外,从上述描述中可以知道LayoutManager需要完成的两个重要工作:

  1. 在onLayoutChildren方法中处理ItemView的布局。
  2. 在scrollHorizontallyBy和scrollVerticallyBy方法中处理列表滚动时ItemView的平移以及ItemView的补充和回收。

此时我们了解到,LayoutManager可以处理子View的measure和layout过程,它可以按自己的需要measure child,并把子View放在它期望的位置上(甚至可以把所有子View都叠放在同一个位置);LayoutManager还可以接管处理滚动的过程(如果愿意的话我们甚至可以在scroll方法中重新布局子View而不触发RecyclerView的layout过程)。

再次回看第一节中的demo,需求是在布局或滑动时,保证第一个完全可见的ItemView是大卡片态,其它ItemView是小卡片态,而布局和滑动恰好可以在LayoutManager进行处理,于是需求变成了让LayoutManager measure第一个完全可见的ItemView时处理为大卡片态,其它ItemView measure为小卡片态,并让LayoutManager将这些ItemView布局到正确的位置上。在后续文章中,我们将带领大家从0到1分析如何实现一个满足我们滚动动画需求的LayoutManager。