💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
相信大家都对`RecyclerView`的用法相当熟悉了,`RecyclerView`的出现给我们开发者提供了一个高扩展的控件,不管是列表、网格、瀑布流,一个控件就可以搞定,而且神奇的是`只需要修改一行代码,就可以轻松切换`。`RecyclerView`的好处太多太多,就不一一列举了,网上也有很多关于`RecyclerView`的教程。说到这里,我们就开始进入主题了,虽然网上有那么多的`RecyclerView`教程,但是没有一篇是详细介绍`RecyclerView`的动画的,大部分都是使用默认的`DefaultItemAnimator`或者使用第三方的动画库,这篇博客我们就来弥补这个空白,咱们来根据`DefaultItemAnimator`的代码来实现一个简单的`RecyclerView`动画。 > 我们仅仅去实现一下最常用的`add`和`remove`的动画,其他的动画,如果大家感兴趣,可以自己参考`DefaultItemAnimator`去扩展。 在开始之前,我们先来看看实现的效果吧: ![](https://box.kancloud.cn/2016-02-18_56c55b39c52c8.jpg "") 如何定制动画呢?首先要继承自`RecyclerView.ItemAnimator`这个类,既然要继承这个类,那我们有必要去了解一下这个类和这个类中的几个方法(仅关注我们今天需要的,其他的类同)。 > This class defines the animations that take place on items as changes are made to the adapter. Subclasses of ItemAnimator can be used to implement custom animations for actions on ViewHolder items. The RecyclerView will manage retaining these items while they are being animated, but implementors must call the appropriate “Starting” (dispatchRemoveStarting(ViewHolder), dispatchMoveStarting(ViewHolder), dispatchChangeStarting(ViewHolder, boolean), or dispatchAddStarting(ViewHolder)) and “Finished” (dispatchRemoveFinished(ViewHolder), dispatchMoveFinished(ViewHolder), dispatchChangeFinished(ViewHolder, boolean), or dispatchAddFinished(ViewHolder)) methods when each item animation is being started and ended. 只看重点,当开始动画时,我们需要调用`dispatchXXXStarting()`,当动画结束时,我们需要调用`dispatchXXXFinished()`。 接下来,来看看需要我们去动手实现的几个方法: 1. isRunning() > 返回当前是否有动画需要执行。 1. runPendingAnimations() > 当有动画要执行的时候调用。这里需要说明一点,当我们去add一个item时,动画可能不是立即去执行的,这种机制可以让ItemAnimator一个个的添加,然后一块去执行。 1. animateAdd() > add时的动画,当我们调用`Adapter.notifyItemInsert()`时会触发该方法,该方法有一个boolean类型的返回值,返回值表示:`runPendingAnimations`是否可以在下一个时机去执行。所以当我们定制动画时,这个方法要返回true。 1. 与`animateAdd`类似的还有`animateMove`、`animateRemove`、`animateChange`。 1. dispatchAddStarting() > 动画开始时调用。 1. dispatchAddFinished() > 动画结束时调用。 1. dispatchAnimationsFinished() > 所有动画结束时调用。 好了,介绍完了几个用到的方法,下面就来动手实现我们自己的动画吧。还是上面说的,我们只实现`add`和`remove`的动画。模仿着`DefaultItemAnimator`算了算,我们需要4个`ArrayList`. ~~~ private ArrayList<RecyclerView.ViewHolder> mPendingAddHolders = new ArrayList<>(); private ArrayList<RecyclerView.ViewHolder> mPendingRemoveHolders = new ArrayList<>(); private ArrayList<RecyclerView.ViewHolder> mAddAnimtions = new ArrayList<>(); private ArrayList<RecyclerView.ViewHolder> mRemoveAnimations = new ArrayList<>(); ~~~ 明明就两个动画,怎么需要4个`ArrayList`呢?而且还是一对一对的!羡慕不?这里要好好说道说道了。上面说了,动画可能不是立即执行的,而是在`runPendingAnimations`中一块去执行,所以我们在`animateAdd`中,仅仅是向`mPendingAddHolders`中添加了一个`ViewHolder`,而不是去写动画的代码。这时,考虑一种情况: > 当我们`animateAdd`了一次,这时`runPendingAnimations`里的动画还没执行完毕,所以我们还不能清空`mPendingAddHolders`这个集合,这时又执行了一次`animateAdd`会出现什么情况?前面的又重复执行了一次动画,在`DefaultItemAnimator`中巧妙的解决了这个问题,和上面4个变量有关,在下面的代码中,我们也会借鉴这种方式实现。 开始代码之前,我们先来看看`isRunning`这个方法该怎么写。 ~~~ @Override public boolean isRunning() { return !(mPendingAddHolders.isEmpty() && mPendingRemoveHolders.isEmpty() && mAddAnimtions.isEmpty() && mRemoveAnimations.isEmpty()); } ~~~ 不多说,只有一句话:`isRunning`不是表示有没有动画要执行嘛。 那继续代码,来看看`animateAdd`和`animateRemove`方法怎么写的。 ~~~ @Override public boolean animateAdd(RecyclerView.ViewHolder holder) { holder.itemView.setAlpha(0.f); mPendingAddHolders.add(holder); return true; } @Override public boolean animateRemove(RecyclerView.ViewHolder holder) { mPendingRemoveHolders.add(holder); return true; } ~~~ 上面说了,这里我们仅仅是向集合中添加一个holder,并且将返回值置为`true`,表示可以去执行`runPendingAnimations`。这里需要注意的就是`animateAdd`方法的第一行代码,我们将view设置不可见,这样做的目的是防止item闪动(出现后才去执行动画)。 那接下来就是重头戏了:`runPendingAnimations`: ~~~ @Override public void runPendingAnimations() { boolean isRemove = !mPendingRemoveHolders.isEmpty(); boolean isAdd = !mPendingAddHolders.isEmpty(); if(!isRemove && !isAdd) return; // first remove if(isRemove) { for(RecyclerView.ViewHolder holder : mPendingRemoveHolders) { animateRemoveImpl(holder); } mPendingRemoveHolders.clear(); } // last add if(isAdd) { ArrayList<RecyclerView.ViewHolder> holders = new ArrayList<>(); holders.addAll(mPendingAddHolders); mPendingAddHolders.clear(); for(RecyclerView.ViewHolder holder : holders) { animateAddImpl(holder); } holders.clear(); } } ~~~ 解释一下代码,首先两个变量,判断两个pending集合是否不为空,这里决定着我们的代码是否有必要往下执行。然后去判断`isRemove`,在这里面去执行`remove`的动画, 可以看到,我们遍历出保存的每一个ViewHolder,然后去执行`animateRemoveImpl`方法,最后将`mPendingRemoveHolders`清空。 `animateRemoveImpl`我们先不去管它,继续看看`add`,这里首先将`mPendingAddHolders`中的所有holder添加到了一个局部List中,然后清空, 这样做的目的是防止动画的重复执行,接着和remove的时候一样,去遍历所有的holder执行`animateAddImpl`方法,最后的最后,将局部的list清空。 那接下来,我们就要去看看`animateRemoveImpl`和`animateAddImpl`方法了,这两个方法才是真正执行动画的地方。 ~~~ // 执行添加动画 private void animateAddImpl(final RecyclerView.ViewHolder holder) { mAddAnimtions.add(holder); final View item = holder.itemView; ObjectAnimator animator = ObjectAnimator.ofFloat(item, "alpha", 0.f, 1.f); animator.setDuration(1000); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { dispatchAddStarting(holder); } @Override public void onAnimationCancel(Animator animation) { item.setAlpha(1.f); } @Override public void onAnimationEnd(Animator animation) { dispatchAddFinished(holder); mAddAnimtions.remove(holder); if (!isRunning()) dispatchAnimationsFinished(); } }); animator.start(); } // 执行移出动画 private void animateRemoveImpl(final RecyclerView.ViewHolder holder) { mRemoveAnimations.add(holder); final View item = holder.itemView; ObjectAnimator animator = ObjectAnimator.ofFloat(item, "alpha", 1.f, 0.f); animator.setDuration(1000); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { dispatchRemoveStarting(holder); } @Override public void onAnimationEnd(Animator animation) { mRemoveAnimations.remove(holder); item.setAlpha(1.f); dispatchRemoveFinished(holder); if (!isRunning()) dispatchAnimationsFinished(); } }); animator.start(); } ~~~ 可以看到这两个方法非常类似,所以我们只说其中的一个,恩,就说离我最近的这个`animateRemoveImpl`吧。首先将这个holder添加到`mRemoveAnimations`中,然后一段大家非常熟悉的属性动画,这里我们仅仅在动画中改变了itemView的`alpha`值,我们按照文档上说的在动画开始的时候调用`dispatchRemoveStarting`方法,在动画结束的石斛调用`dispatchRemoveFinished`方法,最后还去判断了一下有没有执行的动画,如果没有,调用一下`dispatchAddFinished`。这里所做的一切都是按照文档的规定来的。 好激动,终于实现了`RecyclerView`的动画,来使用一下我们的ItemAnimator ~~~ ... mRecyclerView.setAdapter(mAdapter); mRecyclerView.setItemAnimator(new MyItemAnimator()); ~~~ 看看效果: ![](https://box.kancloud.cn/2016-02-18_56c55b3a220bf.jpg "") 从效果中可以看到,我们的`add`动画是没有问题的,但尼玛`remove`动画绝壁不是我们想要的效果!!这也太…了吧。 恩,在仔细观察了1min效果后,我终于发现问题所在了,偷偷告诉你: > 在我们remove的时候,下面的item是不是往上移动了? 移动了, 那是不是要执行`animateMove`方法? 所以,我们还需要一套`move`的处理过程。 首先加一对集合。 ~~~ private ArrayList<MoveInfo> mPendingMoveHolders = new ArrayList<>(); private ArrayList<MoveInfo> mMoveAnimtions = new ArrayList<>(); ~~~ 又是一对好基友,哦,不对,是好情侣。哎?这个`MoveInfo`是啥? 仔细想想,一个移动的过程,是不是需要知道`来自哪里,将要去何方`。我们模仿`DefaultItemAnimator`来定义一个内部类`MoveInfo`。 ~~~ class MoveInfo { private RecyclerView.ViewHolder holder; private int fromX; private int fromY; private int toX; private int toY; public MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { this.holder = holder; this.fromX = fromX; this.fromY = fromY; this.toX = toX; this.toY = toY; } } ~~~ 恩,没啥好说的, 那就继续看`animateMove`方法吧,肯定是向`mPendingMoveHolders`中添加一个,不过这里添加的就是一个`MoveInfo`了。 ~~~ @Override public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { View view = holder.itemView; fromY += view.getTranslationY(); int delta = toY - fromY; view.setTranslationY(-delta); MoveInfo info = new MoveInfo(holder, fromX, fromY, toX, toY); mPendingMoveHolders.add(info); return true; } ~~~ 值得一提的是`int delta = toY - fromY`我们去计算了改view需要移动的距离,然后取反塞给`view.translationY`,这样做的目的是让上面一个在移出的过程中,下面的item不会立马移动上去。而是偏移一定的距离。这里为了好理解,我们打印一组值来帮我我们理解 > fromY: 20, toY: 0 delta: -20 translationY: 20 所以在这个场景下,首先将view向下移动了20个像素,效果就是它在原来的位置不动。往下走,构造了一个`MoveInfo`并添加到`mPendingMoveHolders`里。 继续修改`runPendingAnimations`方法,将我们的move操作加上,这部分代码和`add`的代码非常相似,完全可以copy过来修改修改。 ~~~ @Override public void runPendingAnimations() { boolean isRemove = !mPendingRemoveHolders.isEmpty(); boolean isMove = !mPendingMoveHolders.isEmpty(); boolean isAdd = !mPendingAddHolders.isEmpty(); if(!isRemove && !isMove && !isAdd) return; ... // then move if(isMove) { ArrayList<MoveInfo> infos = new ArrayList<>(); infos.addAll(mPendingMoveHolders); mPendingMoveHolders.clear(); for(MoveInfo info : infos) { animateMoveImpl(info); } infos.clear(); } ... // last add } ~~~ 可以看到我们move的处理完全就是add的翻版,如果不理解,可以网上翻翻博客,看看add部分的说明。继续来到`animateMoveImpl`方法。 ~~~ // 执行移动动画 private void animateMoveImpl(final MoveInfo info) { mMoveAnimtions.remove(info); final View view = info.holder.itemView; ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationY", view.getTranslationY(), 0); animator.setDuration(1000); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { dispatchMoveStarting(info.holder); } @Override public void onAnimationEnd(Animator animation) { dispatchMoveFinished(info.holder); mMoveAnimtions.remove(info.holder); if(!isRunning()) dispatchAnimationsFinished(); } }); animator.start(); } ~~~ 这里面我们构造了一个`translationY`的动画,效果是从该view当前的偏移量到0的一个不断偏移效果。说白了就是不断往上的效果。在动画开始和结束中的处理和add是一样的逻辑。 现在代码终于完成了,效果就是博客刚开始的那个效果。 说起来,到现在我们唯一一个没有实现的效果就是`change`的效果了,其实`change`的处理和`move`的处理也很相似,感兴趣的朋友可以去参考一下`DefaultItemAnimator`的源码。看起来,自己去实现一个`RecyclerView`的item动画也不是那么的复杂,但是代码量也不少,那是不是我们每次需要一种效果都要写这么长的代码? 当然不是!仔细观察代码,其实动画的实现都是在`animateXXXImpl`中实现的,我们完全可以把`aninateXXXImpl`抽象出来,需要什么动画,我们就继承这个类,仅仅去实现`aninateXXXImpl`中的代码就可以,当然,别人也给我们提供好了很多动画库,我们也完全可以不用自己去写,直接使用三方的。下面就推荐一个做的非常棒的`RecyclerView`动画库,直接copy到项目里就可以使用。 [github上的RecyclerView item动画库](https://github.com/wasabeef/recyclerview-animators) 最后是demo的下载地址: [demo下载,戳这里](http://download.csdn.net/detail/qibin0506/8954799) *注意:demo中的属性动画,如果需要向下兼容,可以换用`nineoldandroids`或者`ViewCompat`实现。*