×

掌握RecyclerView动画不得不看的文章

96
milter
2016.07.27 20:36* 字数 4130

RecyclerView已经开始取代ListView成为广大Android开发者的新宠。很多第三方增强版RecyclerView也开始繁荣生长(如UltimateRecyclerView),它们的出现使得RecyclerView的功能和易用性达到一个新的高度。但是,要想真正掌握RecyclerView,仅仅会使用轮子是不够的,必须对它的实现原理有一定程度的掌握。本文翻译自YIGIT BOYAR的两篇讲解RecyclerView动画实现原理的文章。Who is YIGIT BOYAR?他是RecyclerView的主要作者,所以,本文的含金量已不需赘言。在翻译过程中,有些语句不是照翻的原文,其中加入了自己的理解,有些关键的地方,插入了个人理解部分,目的是为了降低大家的理解难度。如果文中有不能完全理解的地方,你可以查看原文

第一部分:RecyclerView中的动画是怎样运行的


ListView是Android Framework中最受欢迎的组件。它有许多的特性,但过于复杂的结构使得修改它非常困难。随着以用户体验为核心的设计范式的发展以及手机运行越来越快,它的局限性已经使它的良好特性蒙上了重重的阴影。

伴随着Android Lollipop的发布,Android团队决定发布一个新的组件,它采用插件化的架构,使得实现不同的集合视图(collection views)更加容易,仅需满足简单的约定,就可以改变许多不同的行为。

  • item的布局
  • 动画
  • item 的装饰
  • 循环利用策略
  • ...

与这些令人惊叹的灵活性相伴的,就是更大、更复杂的架构。使用者需要学习更多的东西才能掌握它。

本文中,我将深入RecyclerView的内部,剖析它的动画机制是怎样运作和实现的。

在Honeycomb上,Android Framework 引入了LayoutTransition,使得为ViewGroup内部的改变增加动画效果变得非常简单。它的原理是在layout变化之前和之后保存该ViewGroup的快照(snapshot),然后创建Animators以实现两个状态转换时的动画。这一处理过程与RecyclerView为其adapter中内容的改变创建动画非常相似。

LayoutTransition 示例图:


1.gif

很不幸,作为列表的RecyclerView有一个与普通ViewGroup不同的地方,这使得使用LayoutTransiton实现它的动画非常困难。具体讲就是,list中的item与ViewGroup中的child view是不一样的。这种不一样,对于理解item的动画机制非常重要。

在一个普通的ViewGroup中,如果一个view被加到它的层次结构中,该view就可以被看做是"newly added"view,因此可以对它施加相应的动画(比如 fade in)。但是对于list,就没这么简单了。比如,一个item的view变成visible时,你不能简单地把它看做是一个"newly added"view,它之所以变成visible,有可能是adapter中排在它前面的view被移除了。在这种情况,为它施加一个fade in 的动画很可能误导用户,因为这个item已经在list中了,该item的view之所以是新的,是因为该item恰好进入了视窗之内。RecyclerView知道该item是不是新的,但是如果该item不是新的,RecyclerView不知道该item的view是之前在哪个位置。同样的道理也适用于disappearing view,如果该item没有从Adapter中移除,RecyclerView不知道这个item的disappearing view去了哪里。

个人理解:在这里,初学者要注意区分item和它的view两个概念,区分它们的关键是要从LayoutManager的视角去看待二者。当你创建一个RecyclerView,给它设置好LayoutManager。然后,你创建一个Adapter,该Adapter包含数据data,多数情况下它都是一个List。然后将该Adapter设置给RecyclerView。好了,下面就是LayoutMananger代表RecyclerView与Adapter交互的时间了。首先,LayoutMananger会调用Adapter的getItemCount,Adapter会返回item的数量,LayoutMananger现在知道有多少item要展示,那么它就会从第一个(position = 0)开始,向RecyclerView索要这个item的view,经过一番周折,RecyclerView会请求Adapter生成第一个item的view,绑定数据后传给LayoutMananger。为什么不直接加到RecyclerView,因为这就是LayoutManager的责任,LayoutManager会把第一个view加到RecyclerView。然后按照同样的方式第二个item的view,第三个item的view逐渐被加载到RecyclerView中,直到LayoutManager发现RecyclerView中已经没有空间可以展示新的item了(假设加载到position=5的item)。然后,假设用户向上滚动(这里的向上滚动是指手指触摸屏幕后,不离开,向上滑动)了,LayoutManager会让第一个item的view离开RecyclerView(Detach from RecyclerView),同时,LayoutManager发现屏幕下方又有了可以展示item的空间,于是它再次向RecyclerView索要position=6的item的view,RecyclerView与Adapter相互配合,生成了该item的view并返回给LayoutManager,LayoutManager将该view添加到RecyclerView。大家可以看到,对于LayoutManager来说,它所关心的只是Adapter中有多少items要展示,并不关心每个item的view是怎么来的,对LayoutManager来说,屏幕上有空闲空间时,就向RecyclerView和Adapter索要下一个item的view,好像每一个item的view就放在RecyclerView中,随时等着它取用一样。由此我们可以进一步理解,Adapter中的notifyItemInserted(int pos)之类的方法,实际上是Adapter在告诉LayoutManager,哪个位置上插入了一个item,请根据你现在正在展示的items的情况,作出相应的调整。

------------------------------我是分割线------------------------------------------------------

正文继续:

LayoutTransition对于list失效示例:


1.gif

个人理解:这里就是RecyclerView的Item Animation与众不同的地方,以上图为例,当用户将item “C”移除时,RecyclerView会让Item Animator去执行item动画,这里有三种情况需要处理:第一种是item “C” 的移除动画(请注意,这里指的移除动画,与滑动时边缘item的消失动画是不一样的,消失动画意味着该item还在,只是离开了视窗,移除动画意味着该item从Adapter中删除了,再也不会存在的。)第二种是item “D”“E”“F”向前移动的动画,第三种就是item “G”的出现动画(上图中的示例显然是误导人的,因为使用fade in 会让用户产生这是一个newly added item的错觉,而实际上该item一直存在,只是现在进入到视窗之内而已)。本文下面所讲的LayoutManager的preLayout,postLayout技术正是为了帮助RecyclerView搞清楚每一个item究竟需要什么样的动画。

------------------------------我是分割线---------------------------------------------------
正文继续:

要解决这个问题,RecyclerView可以向LayoutManager查询,获得新出现的view前面的位置,以此判断新出现的view究竟是不是一个"newly added"view,这尽管能够行得通,但是却要求LayoutManager承担了一部分簿记(bookkeeping)的功能,对于结构复杂的LayoutManager来说,也许是个不小的负担。

实际上,RecyclerView对于items的出现和消失的动画处理(这是指,对于那些还在list中的item的view的出现和消失的动画处理)是依靠LayoutManager执行了预布局计算(predictive layout logic)。一方面,RecyclerView想要知道,如果LayoutManager能够将不可见的items也lay out的话,在这次变化之前(上例中的变化即是remove item “C”),每一个item将会怎样被laid out;另一方面,RecyclerView想要知道,如果LayoutManager能够将不可见的items也lay out的话,每一个item在变化之后将会怎样被laid out。

为了使得LayoutManager能够更容易提供这些信息,当Adapter中发生了诸如 remove item “C”这样的需要进行动画处理的变化后,RecyclerView会要求LayoutManager执行两次layout(即preLayout和postLayout)。处理过程如下:

  • 在第一次layout(preLayout)过程中,LayoutManager 会layout(此处layout为动词)之前的状态,但是RecyclerView会给LayoutManager提供一些额外的信息。就上面的例子而言,RecyclerView会对LayoutManager说:“哥们,将所有的items重新layout一遍吧,btw,item “C”已经被移除了哈。”然后,LayoutManager会正常执行它的layout过程,但是它知道“C”将要被移除,所以它会layout新的items以填补“C”移除后留出的空间。
    这个过程中最酷的地方在于,RecyclerView会将item “C”当作它还在Adapter中一样。就上面的例子来讲,当preLayout完成后,如果LayoutManager调用getViewForPosition(2),那么RecyclerView会返回View("C")给LayoutManager,如果LayoutManager调用getViewForPosition(4),RecyclerView会返回View("E")(虽然在Adapter中,应该是"F")。每一个返回的View都有一个isItemRemoved方法,该方法可以让LayoutManager来判断该View是否是一个正在消失的View。
    个人理解:第一次layout,其实就做了两件事,一个是给"C"标记了removed标签,一个是多layout出了“G”。
    此时,在Adapter中的item数据是这样的
    A B D E F
    但是在RecyclerView中的View数据是这样的
    A B C D E F G
    也就是说,此时“G”已经被added到了RecyclerView中,只是没有在视窗内而已。

  • 在第二次layout时(postLayout),RecyclerView要求LayoutManager再次layout它的items。这一次,“C”已经不在Adapter中了。getViewForPosition(2)将会返回“D”,getViewForPosition(4)将会返回“F”。
    请时刻牢记,View(“C”)所对应的Adapter中的item C已经被移除了,但是由于RecyclerView还保留着View(“C”),所以表现出来就像Adapter中还有“C”一样。换句话讲,RecyclerView实际上是替LayoutManager做了簿记(bookkeeping)工作。
    每次LayoutManager的onLayoutChildren方法被调用,它都会暂时地将所有layout出来的views detach掉,然后从0开始进行layout。那些没有变化的Views将会从scrap cache中返回,所以它们可以直接使用,因为它们的measurements仍然是有效的,这让relayout操作非常的简单快捷。
    说明: scrap cache是RecyclerView内部的一个缓存,该缓存主要存储那些detached的Views,以便在LayoutManager需要的时候迅速返回给它,达到复用的目的。

LinearLayoutManager pre layout的结果:(粉色的矩形标记的是对用户可见的区域)


2.jpeg

LinearLayoutManager post layout 的结果:


3.jpeg

经过两轮layout,RecyclerView现在知道这些Views(变化之后layout出的views)是从哪里来的了,所以它现在可以正确地对它们施加动画了:


4.gif

你也许会问:View “C”并没有被LayoutManager laid out,为什么它仍然是可见的?

这里要澄清的是,“C”在preLayout中是被LayoutManager laid out 的,因为LayoutManager感觉“C”还在Adapter中(RecyclerView 拦截了remove “C”这一事件,它只是告诉了LayoutManager,“C”将要被removed,好让LayoutManager多layout一个item “G”)。但是在postLayout中,LayoutManager并没有layout “C”,因为它已经不在Adapter中了。此时,“C”也不再是LayoutManager的child了。但是对于RecyclerView并不是这样的,如果ItemAnimator想对“C”施加动画,那么RecyclerView就仍然将它作为自己的child以便动画能够正常运行,待动画结束后才将“C”移除。此时,view “C”被存放于RecyclerView中的一个hiddern list,这个list中的views属于RecyclerView的children,LayoutManager对此毫不知情。

个人理解:在item动画正在进行的过程中,在上例中就是“C”正在fade out 时,LayoutManager与RecyclerView二者的children实际上是不一致的。那么此时,当LayoutManager 调用getViewForPosition(2)时,RecyclerView会收到LayoutManager的请求,虽然RecyclerView此时实际在position 2上的view 是正在fading out的“C”,但是RecyclerView知道,LayoutManager并不知道自己还在保存着“C”,从LayoutManager的视角来看,此时的RecyclerView 中的views应当是这样的:
A B D E F
,聪明的RecyclerView会跳过“C”,将“D”返回给LayoutManager,等“C”动画结束,RecyclerView将“C”移除后,二者的children又将变得一致了。

消失的 items

经过上面两轮的layout,RecyclerView已经可以对新的Views正确地施加动画了。但是现在,还有另外一个问题没有解决。即当一个新的item被加到list中时,会将其他的items推到视窗之外。如果使用LayoutTransitions,效果是这样的:


5.gif

很显然,“F”的动画效果不是我们想要的。

当“X”被添加到“A”的后面,从而将“F”推出到视窗之外时,因为LayoutManager将不再layout “F”,LayoutTransition认为它已经从UI中移除了,所以对它施加了一个fade out 动画。然而,实际上“F”仍然在Adapter中,只是被推出了边界(bounds)。

为了解决这个问题,RecyclerView为LayoutManager提供了一个额外的API来获取这个信息。在完成postLayout后,LayoutManager可以调用getScrapList方法来获取像“F”这样的View的列表(即没有被LayoutManager laid out但是仍存在Adapter中),然后,LayoutManager将会lay out这些Views,就像RecyclerView有足够大的空间来显示它们一样。

LinearLayoutManager post layout的结果:(粉色的矩形框代表对用户可见的区域)


6.png

一个重要的细节是,因为这些Views在它们的动画结束后,就没有存在的必要了,所以LayoutManager调用addDisappearingView,而不是调用addView。这样的调用会告诉RecyclerView,当这个View 的动画完成后,请将它移除。同时,RecyclerView也会将这个view(本例中即“F”)添加到它的hidden views 列表中(就像在前面的例子中,将“C”对LayoutManager隐藏一样),这样当postLayout返回后,对于LayoutManager来说,“F”已经是不存在的了。

最初,对于LinearLayoutManager来说,你可能会认为LayoutManager能够计算出views来自哪里以及去往哪里(如果是消失的话),因此不需要进行两次layout计算。

很不幸,在一次layout中,Adapter有许多不同类型的改变时(同时包含若干数量的remove,add,modify,insert等改变),将会有许多边界情况(edge case)出现。另外,对于一个复杂的LayoutManager,计算一个item将会被放在什么地方并不是一件轻松的事(如StaggeredGridLayout)。我们的办法(即使用两次layout)将会解除LayoutManager的计算负担,而且还可以支持正确地对item施加动画,这几乎不用费什么劲。

至此,我已经讲解了RecyclerView中的predictive animations的主要思想。实际上,要达到这种简单性,还有许多工作要做,具体的细节,请看第二部分

Android开发
Web note ad 1