抬头仰望星空,是否能发现自己的渺小。

伪斜杠青年

人们总是混淆了欲望和理想

多布局RecycleView复用问题以及原生ItemAnimator踩坑手札

引语:

一个小小的需求暴露了自身能力缺陷:我是真的不喜欢看别人的长篇大论文章

前置条件

需求:常见多类型item的RecycleView 普通浏览模式与编辑模式状态切换 以及动画转场

多类型、排序拖动、diff简单实现方案:http://www.recyclerview.org/

动画实现方案:https://developer.android.com/reference/androidx/recyclerview/widget/DefaultItemAnimator

简单描述

复用问题

对于多类型的Item显示,只需要定义几个ItemType即可实现。但是Recycleview自带的Item复用在这上面弊端明显,在onBind过程中,每个元素的状态,需要处理清楚,每个类型除非不同布局,否则在onBind过程中一定要一一对应,否则就状态错乱。什么是一一对应?伪代码:

以前说到RecycleView,都谈回收,但实际上这个词是为了服务复用,就是一个已有的Item,不到“迫不得已”不用创建新的,直接用旧的换上新数据即可,这也是谷歌官方DiffUtil的主要作用,用于差量通知Item的改变、添加、移除。

元素转场动画

对于这个动画,一开始是准备直接放在onBind过程中进行处理的,但是由于需求只是想对当前的元素进行过渡,如果放在onBind中,那么滑动的时候也会进行bind操作,这样处理动画逻辑就不合适了。那么有没有只在差量更新的时候进行刷新,也就是只对当前屏幕条目进行处理,notifyDataSetChange这种全量更新就不会执行的呢?

于是,我直接找RecycleView ItemAnimation,果然官方提供了一个

RecyclerView.ItemAnimator

但是这个玩意儿一般是不会拿来直接用的,因为缺失太多轮子,我们花费那么时间去做一些轮子可能还不如谷歌做得好,所以,它的子类还有这样几个:

public abstract class SimpleItemAnimator extends RecyclerView.ItemAnimator
public class DefaultItemAnimator extends SimpleItemAnimator

先看看SimpleItemAnimator,对于SimpleItemAnimator按字面意思理解就好了,但是你去实现一下就会发现,WTF,还有这么多要实现的方法,想想就麻烦。但确实也足够简单,有用于实现删除效果的animateMove(移除一个,其他的就得move嘛),Item change时候的animateChange方法(一个旧holder,一个新holder,有时候是相同的对象,有时候是不同的对象,这里需要注意)

其他的add回调,remove回调就不说了,因为这种资料随处可见。官方文档:SimpleItemAnimator ,但是官方主要推荐的是DefaultItemAnimator ,同时难道不会觉得其实官方的一些动画是可取的嘛,而且这个帮我们实现了大多数功能,而且也是官方推荐使用的。

拿来即用,但是怎么用?没人说,哪些坑?没人说。上代码(PS:无法直接用)

    class AnricItemAnimator : DefaultItemAnimator() {

        override fun animateRemove(holder: RecyclerView.ViewHolder?): Boolean {
            Log.d(TAG, "animateRemove:  [holder:$holder] ")
            holder?.also {
                //do some animation
                dispatchAnimationFinished(holder)
                return false
            }
            return super.animateRemove(holder)
        }

        override fun animateAdd(holder: RecyclerView.ViewHolder?): Boolean {
            Log.d(TAG, "animateAdd:  [holder:$holder] ")
            holder?.also {
                //do some animation
                dispatchAnimationFinished(holder)
                return false
            }
            return super.animateAdd(holder)
        }

        override fun animateMove(holder: RecyclerView.ViewHolder?, fromX: Int, fromY: Int, toX: Int, toY: Int): Boolean {
            Log.d(TAG, "animateMove: [holder:$holder]  [fromX:$fromX]  [fromY:$fromY]  [toX:$toX]  [toY:$toY] ")
            holder?.also {
                //do some animation
                dispatchAnimationFinished(holder)
                return false
            }
            return super.animateMove(holder, fromX, fromY, toX, toY)
        }

        override fun animateChange(oldHolder: RecyclerView.ViewHolder, newHolder: RecyclerView.ViewHolder, preInfo: ItemHolderInfo, postInfo: ItemHolderInfo): Boolean {
            Log.d(TAG, "animateChange:  [holder:${oldHolder == newHolder}] ")
            if (oldHolder != newHolder) {
                //用于处理前后holder不是复用的情况
                oldHolder.itemView.alpha = 0f
                newHolder.itemView.alpha = 1f
            }
            when(newHolder.itemViewType){
                //只有某个类型的holder才需要这个动画
                TYPE_A->{
                    //do some animation
                    dispatchAnimationFinished(newHolder)
                    return false   
                }
            }
            return super.animateChange(oldHolder, newHolder, preInfo, postInfo)
        }
    }

有没有发现一些奇怪的事情,比如为什么return false,为什么每句return背后都得加个dispatchAnimationFinished,当然是因为坑啊。

为什么return false?

先看看return值,右键这些方法查找引用,找自己现在用的RecycleView源码

什么鬼直接放在if语句块中,那就继续点击去看,即便看方法名也知道是干什么了。

然后看这个方法的实现,就会找到我们的主角DefaultItemAnimator,其实猜一猜也知道了,官方解释:就是用来调用那些准备被执行的动画。

而我们的动画肯定是UI说好的,不能和官方的这些玩意儿重叠或者搞出什么新的飞机,那么,就return false,这样DefaultItemAnimator里的东西就不会和我们的动画重叠了,这样UI的效果就可以随意实现了。

为什么要调用dispatchAnimationFinished?

因为这也是谷歌的糖果,看看它做了啥:

这是RecycleView的内部方法,主要是帮我们做回收。

这个在animateAdd、animateRemove、animateMove上其实没啥太多用,而且官方其实有说其他行为有其他的调用方法,比如dispatchMoveFinished

看到了吧,谷歌都是这样推荐,那我为什么不调用呢?因为啊,你看源码

空的,最后还是调用dispatchAnimationFinished,禁止套娃,禁止套娃,禁止套娃!!!

animateChange时 新旧holder动画重叠 不消失?

那么不知道有没有看到我的那行

if (oldHolder != newHolder) {
   //用于处理前后holder不是复用的情况
   oldHolder.itemView.alpha = 0f
   newHolder.itemView.alpha = 1f
}

对于change时,有时候holder不是复用的情况时,直接将之前的alpha改成0,这样newholder做动画就不会有什么影响了,注意尽量不要使用visibility属性,olderholder并不一定会被销毁,很可能是被拿去重新用了,所以在做新的渲染的时候,也就是在onbind的时候,要把holder的itemview alpha值和visibility改成正常值(标重点)

这里有个顺序得提下,所有的上面的动画流程animateMove、animateAdd都是在onbind过程后执行的,需要保证状态的一致性,什么是一致性(就是动画结束后的状态,一定要和单独跑完onbind的状态一致,否则滑动就会有奇迹发生,那是一片新大陆),但onBind在animation前,所以不要指望动画随意做,bind的时候再去修正,不存在的!!!

Diff结果中出现莫名其妙的move remove 或者add操作,明明只是change

这个最明显的效果就是,明明item没有改变却有了跳一跳的效果。出现这个的原因,主要是你对DiffUtil.ItemCallback<T>()的理解。

DiffUtil.ItemCallback有两个方法需要实现,分别是

override fun areItemsTheSame(oldItem: MultiItemEntity, newItem: MultiItemEntity): Boolean
override fun areContentsTheSame(oldItem: MultiItemEntity, newItem: MultiItemEntity): Boolean

在源码中,它们的顺序是这样的:

所以使用debug,拿捏好bean中判断条目类型相同与内容相同的条件。

一般在areItemsTheSame中去判断像ITEM_TYPE,ITEM_ID这种带有区分和唯一性质的数据,同id的条目内容不同,那么就只是需要重新填充数据即可。

在areContentsTheSame中就得进行全字段的匹配了(非必须的除外)使用equals去判断对象或者==都是不错的选择

周末愉快!

以上,我只是个文档搬运工,终于我也成了一个让人讨厌了的人,我讨厌长篇大论。抱歉此文没有demo,重“道”而非“法”,api会变,语言会变,岗位会变,慢慢去学会分析问题,才是主要的。


0条评论

发表评论