RecyclerView动画

RecyclerView动画知识点记录


场景

列表中有两个表项(1、2),删除 2,此时 3 会从屏幕底部平滑地移入并占据原来 2 的位置。

item1
item2
删除item2,item3自动补位
item1
item3

RecyclerView的策略是:
为动画前的表项先执行一次pre-layout,将不可见的表项 3 也加载到布局中,形成(1、2、3)
再为动画后的表项执行一次post-layout,同样形成(1、3)

pre-layout预布局

  • RecyclerView为了实现表项动画,进行了 2 次布局(预布局 + 后布局),在源码上表现为LayoutManager.onLayoutChildren()被调用 2 次。
  • 预布局的过程始于RecyclerView.dispatchLayoutStep1(),终于RecyclerView.dispatchLayoutStep2()。
  • 在预布局阶段,循环填充表项时,若遇到被移除的表项,则会忽略它占用的空间,多余空间被用来加载额外的表项,这些表项在屏幕之外,本来不会被加载。(导致while循环多执行一次,这样表项 3 就被填充进列表。)
先清空表项再填充
  1. detach表项
    detachViewAt(index)
    ……
    RecyclerView.this.detachViewFromParent(offset);
    ViewGroup.removeFromArray()是容器控件移除子控件的最后一步(ViewGroup.removeView()也会调用这个方法)

  2. scrap表项
    recycler.scrapView(view)
    // 表项不需要更新,或被移除,或者表项索引无效时,将被会收到 mAttachedScrap
    mAttachedScrap.add(holder);
    // 只有当表项没有被移除且有效且需要更新时才会被回收到 mChangedScrap
    mChangedScrap.add(holder);

与 scrap 缓存的关系

1
2
3
4
5
// 从 scrap 结构中获取指定 position 的 ViewHolder 实例 
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
......
}

从mAttachedScrap列表中获取的ViewHolder实例后,得进行校验。校验的内容很多,其中最重要的的是:ViewHolder索引值和当前填充表项的位置值是否相等,即:
scrap 结构缓存的 ViewHolder 实例,只能复用于和它回收时相同位置的表项。

“何必这样折腾?即先 detach 并 缓存表项到 scrap 结构中,然后紧接着又在填充表项时从中取出?”

因为 RecyclerView 要做表项动画,

为了确定动画的种类和起终点,需要比对动画前和动画后的两张“表项快照”,

为了获得两张快照,就得布局两次,分别是预布局和后布局(布局即是往列表中填充表项),

为了让两次布局互不影响,就不得不在每次布局前先清除上一次布局的内容(就好比先清除画布,重新作画),

但是两次布局中所需的某些表项大概率是一摸一样的,若在清除画布时,把表项的所有信息都一并清除,那重新作画时就会花费更多时间(重新创建 ViewHolder 并绑定数据),

RecyclerView 采取了用空间换时间的做法:在清除画布时把表项缓存在 scrap 结构中,以便在填充表项可以命中缓存,以缩短填充表项耗时。

阻止回收

并不是所有的表项动画都会阻止表项被回收,只有通过ViewPropertyAnimator做动画才会。
只要在onFailedToRecycleView()回调中取消动画,并且返回 true 表示强制回收即可。

1
2
3
4
5
6
adapter.onFailedToRecycleView = { holder ->
// 在回收表项之前取消动画
(holder as? TextViewHolder)?.tv?.animate()?.cancel()
// 返回 true 表示强制回收表项
true
}