Loader's Blog

人不会死在绝境,却往往栽在十字路口

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

相信大家都对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实现。

阅读更多
版权声明:本文来自Loader's Blog,未经博主允许不得转载。 https://blog.csdn.net/qibin0506/article/details/47250299
个人分类: android
所属专栏: Android新技术
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

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

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭