引语:
一个小小的需求暴露了自身能力缺陷:我是真的不喜欢看别人的长篇大论文章
前置条件
需求:常见多类型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会变,语言会变,岗位会变,慢慢去学会分析问题,才是主要的。
相关文章:
RecyclerView之ItemDecoration由浅入深
本站由以下主机服务商提供服务支持:
0条评论