RecyclerView 知识梳理(1) - 综述
二、为什么要使用RecyclerView
RecyclerView
诞生的目的就是为了替代ListView
,我们先总结一下在使用ListView
过程当中所遇到的问题:
- 复用
Item
需要编写很多的代码
在使用ListView
的时候,有经验的程序员一定会告诉你在getView
中要这么写,如果忘了,那么会产生很严重的性能问题。if (convertView == null) { //通过LayoutInflator生成convertView,并产生一个ViewHolder,通过setTag关联起来. } else { //通过getTag获取ViewHolder,进行更新操作. }
- 焦点冲突问题
当Item
有焦点时,Item
的子控件就无法获取到焦点;而如果子控件抢夺了焦点,那么Item
的点击事件又不能响应,这个相信大家都遇到过。 - 重复的
API
ListView
中提供了很多的API
,但是这些API
又和View
的一些API
重复了,例如我们可以给ListView
设置setOnItemClickListener
,也可以在getView
中给某个View
设置setOnClickListener
,这就让人很疑惑,到底应当选用哪个。 - 动画
当我们需要在ListView
中进行添加、删除、移动等操作的时候,如果希望加上动画,那么是很困难的,根本原因是我们是通过Adapter
通知ListView
进行更新,然而ListView
根本就没法确定到底是哪些View
发生了变化。 - 更加复杂的布局需求
ListView
在布局是规整的列表的时候能满足大多数人的使用,然而如果想要实现像瀑布流这种复杂的布局,并且保证View
能够复用,那么需要编写很多的代码。
如果之前有了解过RecyclerView
的基本用法,那么你会发现,对于上述这些问题,它都给出了自己的解决方案:
- 强制使用开发者使用
ViewHolder
,提供了onCreateViewHolder
和onBindViewHolder
这两个方法,把创建View
和绑定View
的操作分离开。 - 把焦点交给系统处理。
- 去掉了
onItemClickListener
,以及一些重复的API
。 - 在
Adapter
中增加了notifyItemChanged()
等方法,让我们可以指定变化的类型和范围,并且提供了setItemAnimator()
方法,让开发者能够方便地定义添加、删除、移动的动画。 - 把布局的工作抽象出来,放到了
LayoutManager
当中,并预制了瀑布流布局。
了解了这些,我们就能知道RecyclerView
能帮我们解决什么问题,也就能更好地理解它为什么要这么设计,下面就开始进入真正的RecyclerView
的学习。
三、RecyclerView
架构
整个RecyclerView
体系包含三大组件:
-
LayoutManager
:position the view
-
ItemAnimator
:animate the view
-
Adapter
:provide the view
这三大组件各司其职,而RecyclerView
负责管理,就组成了整个RecyclerView
的架构。
3.1 LayoutManager
LayoutManager
需要负责以下几部分的工作:
-
Position
它负责View
的摆放,可以是线性、宫格、瀑布流式或者任意类型,而RecyclerView
不知道也不关心这些,这是LayoutManager
的职责。 -
Scroll
对于滚动事件的处理,RecyclerView
负责接收事件,但是最终还是由LayoutManager
进行处理滚动后的逻辑,因为只有它在知道View
具体摆放的位置。 -
Focus traversal
当焦点转移导致需要一个新的Item
出现在可视区域中时,也是由LayoutManager
处理的。
3.2 Adapter
Adapter
需要负责以下几部分的工作:
- 创建
View
和ViewHolder
,后者作为整个复用机制的跟踪单元。 - 把具体位置的
Item
和ViewHolder
进行绑定,并存储相关的信息。 - 通知
RecyclerView
数据变化,支持局部的更新,在提高效率的同时也有效地支持了动画。 -
Item
点击事件的处理。 - 多类型布局的支持。
四、ViewHolder
的生命周期
4.1 LayoutManager
请求RecyclerView
提供指定position
的View
ViewHolder
是和View
相绑定的,同时它也是整个复用框架的跟踪单元。在RecyclerView
体系中,对ViewHolder
采用了二级缓存,分为Cache
和Recycled Pool
,当LayoutManager
向RecyclerView
请求位于某个Position
的View
时,Recycled View
会先去Cache
中寻找,如果找到,那么直接返回;如果找不到,那么再去Recycled Pool
中寻找,下面就是整个寻找过程的几种情况:
- 命中
Cache
这种情况下,不会调用Adapter
的onCreateViewHolder
或者onBindViewHolder
方法:
-
Cache
不存在,Recycled Pool
也不存在
这种情况下,会调用Adapter
的onCreateViewHolder
方法,让它提供一个对应viewType
的ViewHolder
,我们在其中建立ViewHolder
和View
之间的关联。
-
Cache
不存在,Recycled Pool
存在
这种情况下,会回调Adapter
的onBindViewHolder
方法,我们在其中使用当前的数据集合来更新ViewHolder
所绑定的itemView
的状态。
4.2 LayoutManager
找到对应位置的View
LayoutManager
通过addView
方法把之前找到的View
添加进RecyclerView
,RecyclerView
通过onViewAttachToWindow(VH viewHolder)
方法,通知Adapter
这个viewHolder
所关联的itemView
已经被添加到了布局当中,
4.3 LayoutManager
请求RecyclerView
移除某一个位置的View
4.3.1 普通情况
当LayoutManager
发现不再需要某一个position
的View
时,它会通知RecyclerView
,RecyclerView
通过onViewDetachFromWindow(VH viewHolder)
通知Adapter
和它绑定的itemView
被移出了。同时,RecyclerView
判断它是否能够被缓存,假设能够被缓存,那么它会先被放到Cache
当中,在Cache
中又会判断它内部是否有需要转移到Recycled Pool
中的ViewHolder
,在放入之后回收池后,通过onViewRecycled(VH viewHolder)
方法通知Adapter
它被回收了。
4.3.2 特殊情况
在上面的普通的情况中,onViewDetachFromWindow(VH viewHolder)
是立即被回调的。然而在实际当中,由于我们需要对View
的添加、删除做一些过度动画,这时候,我们需要等待ItemAnimator
进行完动画操作之后,才做detach
和recycle
的逻辑,这一过程对于LayoutManager
是不可见的。
4.4 ViewHolder
的销毁
在一般情况下,我们不会去销毁ViewHolder
,而是把它放入到缓存当中,除非出现以下两种情况。
4.4.1 ViewHolder
所绑定的itemView
当前状态异常
在放入Recycled Pool
时,会去检查itemView
的状态是否正常。这一操作的目的主要是为了避免出现诸如此类的情况:当前itemView
正在执行动画,此时它可能呈现半透明的状态,如果此时把它放入到回收池中,那么当另一个位置的position
需要复用它时就可能会出现问题。
当出现上面的情况后,Recycled Pool
会先通过Adapter
的onFailedToRecycled(VH viewHolder)
告诉它我们现在出现了异常的情况,由Adapter
的实现者通过返回值来决定是否仍然要把它放入到Recycled Pool
,默认是返回false
,也就是不放入,那么这个ViewHolder
就会被销毁了。
4.4.2 Recycled Pool
中已经没有足够的空间
Recycled Pool
的空间并不是无限大的,因此,如果没有足够的空间存放要被回收的ViewHolder
,那么它也会被销毁。
造成这种情况的一般是动画引起的,例如,我们调用了notifyItemRangeChanged(0, getItemCount())
方法,这时候为了进行渐出渐进的动画,那么我们就需要创建两倍的ViewHolder
,出现这种情况时一般有两种解决方法:
- 只通知具体发生变化的
Item
- 通过
pool.setMaxRecycledViews(type, count)
改变回收池的大小。
五、ItemAnimator
对于Item
的动画,主要有以下几种情况:
- 添加:
Fade In
- 删除:
Fade Out
- 移动:
Translate
- 更新:
Cross Fade
RecyclerView
对于动画的处理采用了Predictive
的方式,除了当前已经在RecyclerView
布局中的View
(实线框部分),它还需要知道在屏幕意外的信息(虚线框部分),这样在H
被删除的时候,它才能够对J-K
进行上移动画,并把原来不在屏幕内的L
上移到可视范围之内。
六、ChildHelper
和AdapterHelper
6.1 ChildHelper
对于ChildHelper
的作用是:Provide a virtual children list to layoutmanager
,下面我们就首先看一下为什么需要它。
6.1.1 解决什么问题
我们看下面这种情况,假如LayoutManager
想要移除一个View
,而ItemAnimator
又希望给这一移除的操作增加一个动画,那么这时候就会产生冲突,到底应该怎么办,为此,RecyclerView
通过ChildHelper
来把它们隔离开。
6.1.2 解决问题的方法
当RecyclerView
收到LayoutManager
要求改变布局的请求时,它并不是直接去更改ViewGroup
,而是让ChildHelper
和ItemAnimator
去协调,并由它来操作ViewGroup
。
最明显的例子是,假如我们当前列表中状态为0,1,2,3
,此时我们移除了position=0
的Item
,这时候假如删除的动画还没有完成,那么LayoutManager
和RecyclerView
的getChildAt(0)
返回值将会不同,因为在LayoutManager
并不清楚ChildHelper
的存在,在它看来,position=0
的Item
已经被移除了。
layoutManager.getChildAt(0); //return 1;
recyclerView.getChildAt(0); //return 0;
6.2 AdapterHelper
而AdapterHelper
所解决的问题和ChildHelper
类似,ChildHelper
是处理View
的,而AdapterHelper
用来跟踪ViewHolder
的,其作用为:
Tracks ViewHolder positions
Virtual Adapter for LayoutManager
说起来可能比较抽象,我们用下面这种图理解一下,当我们移动某个Item
并且它的onLayout
方法还没有完成,那么Adapter
和Layout
的postion
是不相同的:
七、ItemDecoration
ItemDecoration
用来在RecyclerView
的Canvas
上进行额外的绘制操作,我们不仅可以在单个Item
(例如给每个Item
添加分割线)的Canvas
上进行绘制,也可以在整个RecyclerView
的Canvas
上进行绘制,此外,我们还可以指定Item
之间的间隔:
Custom Drawing on RecyclerViews Canvas
Add offset to View bounds
Have multiple ItemDecoration
需要注意的点:
Do not try to access to adapter
Keep necessary information in viewHolder
General onDraw rules apply
recyclerView.getChildViewHolder(View view)
参考文章:Android RecyclerView 使用完全解析 体验艺术般的控件。
八、RecycledViewPool
RecyclerViewPool
用来缓存那些回收的View
,这些缓存不仅可以提供给单个RecyclerView
使用,还可以提供和别的自定义控件共享。
Sanctuary for reserve ViewHolders
Can be shared between RecyclerViews or Custom ViewGroups
PerActivity Context
九、ItemTouchHelper
之前使用ListView
的时候,如果需要支持侧滑删除、拖动排序这种操作,那么我们一般用引入一些开源库,现在RecyclerView
已经帮我们提供了实现的接口,通过重写ItemTouchHelper
的方法,就可以实现上面提到的那些操作。
Drag & Drop
Swipe to dismiss
参考文章:RecyclerView 进阶:使用 ItemTouchHelper 实现拖拽和侧滑删除
十、Tips
onBind Position != final
,use holder.getAdapterPostion()
如果我们像下面这样,在onBindViewHolder
中绑定了监听:
由于public void onBindViewHolder(final ViewHolder, final int position) { holder.itemView.setOnClickListener(new View.onClickListener) { @Override public void onClick(View view) { removeAtPostion(position); } } }
Item
会被添加、删除、移动,因此,我们在onBindViewHolder
中获得位置,并不一定是当前的位置,例如像下面这样:
那么就会得到错误的位置,这时候应当使用onBindViewHolder(holder, 5); notifyItemMoved(5, 15); holder.itemView.callOnClick();
holder.getAdapterPostion()
来保证能够得到预期的结果。-
Payloads
通过onBindViewHolder
中的List payloads
,我们可以指定在bind
的时候只更新某一部分的信息,而不是全部更新。 -
onCreate means create
在onCreateViewHolder
中,始终应当返回一个新的ViewHolder
,而不是返回一个缓存的ViewHolder
。 -
Adapter position and Layout position
就像我们前面在AdapterHelper
中讨论的那样,在某些时刻,Adapter Position
和Layout Position
并不相等,我们应当根据情况选择需要使用哪个,Adapter Position
数据所处的位置,而Layout Position
则对应当前View
的所处的位置。