关闭

RecyclerView的高级用法——定制动画

标签: RecyclerAnimator动画定制
19748人阅读 评论(16) 收藏 举报
分类:

相信大家都对RecyclerView的用法相当熟悉了,RecyclerView的出现给我们开发者提供了一个高扩展的控件,不管是列表、网格、瀑布流,一个控件就可以搞定,而且神奇的是只需要修改一行代码,就可以轻松切换RecyclerView的好处太多太多,就不一一列举了,网上也有很多关于RecyclerView的教程。说到这里,我们就开始进入主题了,虽然网上有那么多的RecyclerView教程,但是没有一篇是详细介绍RecyclerView的动画的,大部分都是使用默认的DefaultItemAnimator或者使用第三方的动画库,这篇博客我们就来弥补这个空白,咱们来根据DefaultItemAnimator的代码来实现一个简单的RecyclerView动画。

我们仅仅去实现一下最常用的addremove的动画,其他的动画,如果大家感兴趣,可以自己参考DefaultItemAnimator去扩展。

在开始之前,我们先来看看实现的效果吧:

如何定制动画呢?首先要继承自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()

    返回当前是否有动画需要执行。

  2. runPendingAnimations()

    当有动画要执行的时候调用。这里需要说明一点,当我们去add一个item时,动画可能不是立即去执行的,这种机制可以让ItemAnimator一个个的添加,然后一块去执行。

  3. animateAdd()

    add时的动画,当我们调用Adapter.notifyItemInsert()时会触发该方法,该方法有一个boolean类型的返回值,返回值表示:runPendingAnimations是否可以在下一个时机去执行。所以当我们定制动画时,这个方法要返回true。

  4. animateAdd类似的还有animateMoveanimateRemoveanimateChange

  5. dispatchAddStarting()

    动画开始时调用。

  6. dispatchAddFinished()

    动画结束时调用。

  7. dispatchAnimationsFinished()

    所有动画结束时调用。

好了,介绍完了几个用到的方法,下面就来动手实现我们自己的动画吧。还是上面说的,我们只实现addremove的动画。模仿着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不是表示有没有动画要执行嘛。

那继续代码,来看看animateAddanimateRemove方法怎么写的。

@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清空。
那接下来,我们就要去看看animateRemoveImplanimateAddImpl方法了,这两个方法才是真正执行动画的地方。

// 执行添加动画
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());

看看效果:

从效果中可以看到,我们的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动画库

最后是demo的下载地址:
demo下载,戳这里

注意:demo中的属性动画,如果需要向下兼容,可以换用nineoldandroids或者ViewCompat实现。

13
2
查看评论
发表评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场

Android中RecyclerView的item实现动画的效果

最近,项目中运用RecyclerView去实现item的删除和添加时的动画效果。 之前的想法是用RecyclerView本身的添加和删除的动画如下例子: RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list); r...
  • wangyongyao1989
  • wangyongyao1989
  • 2017-04-03 15:29
  • 2143

Android RecyclerView 使用完全解析 体验艺术般的控件

Android RecyclerView 使用完全解析 概述 RecyclerView出现已经有一段时间了,相信大家肯定不陌生了,大家可以通过导入support-v7对其进行使用。 据官方的介绍,该控件用于在有限的窗口中展示大量数据集,其实这样功能的控件我们并不陌生,例如:ListView、G...
  • lmj623565791
  • lmj623565791
  • 2015-04-16 09:07
  • 842383

深入理解 RecyclerView 系列之——ItemAnimator

原文  http://blog.piasy.com/2016/04/04/Insight-Android-RecyclerView-ItemAnimator/ 主题 RecyclerView 本文继上篇ItemDecoration 之后,是深入理解 ...
  • chenjiang2936
  • chenjiang2936
  • 2016-09-09 10:56
  • 1375

Android之RecyclerView的动画详解

RecyclerView是谷歌V7包下新增的控件,用来替代ListView的使用,它有着如下的特点: 你想要控制其显示的方式,请通过布局管理器LayoutManager你想要控制Item间的间隔(可绘制),请通过ItemDecoration你想要控制Item增删的动画,请通过ItemAnimat...
  • a497393102
  • a497393102
  • 2016-03-23 10:42
  • 17393

Android移动view动画问题(让移动更平滑)

eoiioe Android移动view动画问题 Android写动画效果不是一般的麻烦,网上找了好久,终于解决了动画的问题,总结记录以共勉。 仅以水平方向移动效果做说明,垂直方向类似。   完整动画函数代码:   1 public...
  • roserose0002
  • roserose0002
  • 2013-07-30 17:10
  • 16329

对RecyclerView Item做动画

对RecyclerView Item做动画对RecyclerView Item做动画,刚刚开始研究的时候一些坑,在这里把一些设计思路分享出去添加动态位移,静态位移,缩放等动画,保证了动画状态的平滑衔接效果图: 我的Github,Demo下载RecyclerView,ListView这些具有It...
  • Ru_Zhan
  • Ru_Zhan
  • 2016-07-01 01:40
  • 1713

RecyclerView.ItemAnimator终极解读(一)--RecyclerView源码解析

文章开始前,我觉得有必要说一下为什么要使用动画。 2008年那会儿,Android用户就如一个22岁的玉女般 只要用一束鲜花就能让她感动一个星期。虽然那时候的Android系统就如22岁的小伙子房子 车子都没有 只有一股子真情。但是少女不会有任何的抱怨和奢求。但是现在已经是2016年,Android...
  • zxm317122667
  • zxm317122667
  • 2016-05-25 11:59
  • 6720

RecyclerView实现Item滑动加载进入动画效果

如果是使用ListView的话我们知道,可以很简单的实现加载的动画,但是使用android.support.v7.widget.RecyclerView 的话,暂时还不是很多,所以自己就实现了一个效果,先看图吧(第一次上传动态图片,不喜勿喷哦) 图片有点模糊,但是能看到效果,那就是下面...
  • u012341052
  • u012341052
  • 2015-05-14 15:15
  • 11363

recyclerview item的进入动画

自动Material Design出现以来,我对一些视频中演示的网格铺开动画感到惊讶。这是一种斜对角线动画,让activity从上到下从左到右铺开。非常漂亮。我一直试图尝试所有能得到那种效果的方法。一种办法是,使用RecyclerView::notifyItemInserted()方法,这是很多人都...
  • tiankongcheng6
  • tiankongcheng6
  • 2016-12-27 09:44
  • 774

RecyclerView 分割线和 Item默认增删动画

虽然RecyclerView出现已经有一段时间了,但是还是想要自己总结一下,总的来说其基本使用方法: 你想要控制其显示的方式,请通过布局管理器LayoutManager 你想要控制Item间的间隔(可绘制),请通过ItemDecoration 你想要控制Item增删的动画,请通过ItemAnimat...
  • Wshiduo
  • Wshiduo
  • 2017-01-16 19:03
  • 1433
    个人资料
    • 访问:648154次
    • 积分:6909
    • 等级:
    • 排名:第3919名
    • 原创:80篇
    • 转载:0篇
    • 译文:2篇
    • 评论:625条
    文章分类
    博客专栏
    友情链接

    鸿洋_

    Aggie的博客

    梁肖技术中心

    极客导航

    最新评论