ViewAnimator源码分析

我每周会写一篇源代码分析的文章,以后也可能会有其他主题.
如果你喜欢我写的文章的话,欢迎关注我的新浪微博@达达达达sky
地址: weibo.com/u/203068311…
每周我会第一时间在微博分享我写的文章,也会积极转发更多有用的知识给大家.谢谢关注^_^,说不定什么时候会有福利哈.

项目地址:ViewAnimator,本文分析版本: dfa45e0

1.简介

《ViewAnimator源码分析》

ViewAnimator.png

在项目开发中我们应该都接触过动画效果的开发.我们知道在Andorid中实现动画大致分为两类,一种是Tween/Frame动画,另一种是Property Animation也就是属性动画.关于这两种动画的使用方法我们这篇文章就不多做讨论了。可以从这篇文章了解更多。我们这篇文章只涉及属性动画相关知识。我们今天要介绍的ViewAnimator就是用来简化我们写属性动画的的代码量的,它可以通过非常简洁的代码通过建造者模式调用来组合各种动画.让我们的代码简洁易读。如果你的APP里需要各种动画组合,ViewAnimator一定是你的最佳选择。

<!– more –>

2.使用方法

想必大家都使用过属性动画了。我们来做一个最简单的位移动画:

        ObjectAnimator animator = ObjectAnimator.ofFloat(textView, "translationX",0, 500);
        animator.setDuration(2000);
        animator.setRepeatCount(1);
        animator.setInterpolator(new BounceInterpolator());
        animator.start();

上面的代码执行之后就可以使textView从当前位置水平移动500px,整个动画过程是2s,并且添加了一个弹性插值器,而且使动画再重复执行一遍。这样看起来整个代码还是很清晰的,使用起来也很方便,但是如果我们要多个View相互组合再加上各种动画,可想而知代码量会有多少了。下面我们就用属性动画来写一个下面这张图里的动画:

《ViewAnimator源码分析》

ViewAnimatorGif.gif

这张图里包含了:1.文字颜色的渐变以及背景的渐变,然后同时又textView放大动画,和两张图片的下落动画,第一组动画结束后,圆形的图片开始旋转,然后textView不断的显示进度.这就是所有动画,下面是我们实现的代码:

        ObjectAnimator mountainTransY = ObjectAnimator.ofFloat(mountain, "translationY", - dip2px(500), 0);
        ObjectAnimator mountainAlpha = ObjectAnimator.ofFloat(mountain, "alpha", 0, 1);
        ObjectAnimator imageTransY = ObjectAnimator.ofFloat(image, "translationY", - dip2px(500), 0);
        ObjectAnimator imageAlpha = ObjectAnimator.ofFloat(image, "alpha", 0, 1);
        ObjectAnimator percentScaleX = ObjectAnimator.ofFloat(percent, "scaleX", 0, 1);
        ObjectAnimator percentScaleY = ObjectAnimator.ofFloat(percent, "scaleY", 0, 1);
        ObjectAnimator textColorAnimator = ObjectAnimator.ofInt(text, "textColor", Color.BLACK, Color.WHITE, Color.RED);
        textColorAnimator.setEvaluator(new ArgbEvaluator());
        ObjectAnimator textBackgroundAnimator = ObjectAnimator.ofInt(text, "backgroundColor", Color.WHITE, Color.BLACK, Color.YELLOW);
        textBackgroundAnimator.setEvaluator(new ArgbEvaluator());

        ObjectAnimator imageRotation = ObjectAnimator.ofFloat(image, "rotation", 0, 360);
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                percent.setText(String.format(Locale.US, "%.02f%%", animation.getAnimatedValue()));
            }
        });

        AnimatorSet firstSet = new AnimatorSet();
        firstSet.playTogether(mountainTransY, mountainAlpha, imageTransY, imageAlpha, percentScaleX,
                percentScaleY, textColorAnimator, textBackgroundAnimator);
        firstSet.setInterpolator(new AccelerateDecelerateInterpolator());
        firstSet.setDuration(5000);

        final AnimatorSet secondSet = new AnimatorSet();
        secondSet.playTogether(imageRotation, valueAnimator);
        secondSet.setDuration(5000);

        firstSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                secondSet.start();
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        firstSet.start();

上面就是实现这个效果的所有代码.借用岳云鹏的一句话就是:”我的天哪”(请脑补配音)。这么一大坨代码。。我整整写了十几分钟。。而且从这么多代码来看上去,如果以后需要调整动画的话,无论如何也得先整个看一遍才能找到怎么调节。这样维护成本就增加了。那么如何解决这个问题呢?这就要用到我们今天要介绍的主角ViewAnimator,下面是用ViewAnimator来实现相同动画的代码:

        ViewAnimator.animate(mountain, image)
                .dp().translationY(-500, 0)
                .alpha(0, 1)

                .andAnimate(percent)
                .scale(0, 1)

                .andAnimate(text)
                .textColor(Color.BLACK, Color.WHITE)
                .backgroundColor(Color.WHITE, Color.BLACK)
                .waitForHeight()
                .interpolator(new AccelerateDecelerateInterpolator())
                .duration(2000)

                .thenAnimate(percent)
                .custom(new AnimationListener.Update<TextView>() {
                    @Override
                    public void update(TextView view, float value) {
                        view.setText(String.format(Locale.US, "%.02f%%", value));
                    }
                }, 0, 1)
                .andAnimate(image)
                .rotation(0, 360)
                .duration(2000)
                .start();

真是又简洁又易读。简直”完美”(请再脑补配音)。可以看到从上到下,我们需要先通过animate(View... view)方法将我们要进行动画的View传入,然后通过建造者模式调用我们需要做的动画,方法名代表我们需要动画的属性,方法参数里直接传入数值即可。andAnimate(View... view)表示同时做该view的动画但是具体的动画可以不一样。然后通过thenAnimate(View... view)方法就可以表示前面的动画执行完毕后再执行的动画.具体每个方法代表的意思也很清楚就是我们需要操作的属性的意思。所以整体来看代码简洁易读又好维护。

此外ViewAnimator还封装了不少动画组合让我们拿来即用,例如:standUp(),wave(),shake()等等动画。此外还支持Path以及SVG Path动画.更多的使用方法可以参照ViewAnimatorREADME.md。下面我们就具体来看看如此好用的ViewAnimator是如何实现的。

3.类关系图

《ViewAnimator源码分析》

classes-relation.png

从类图上来看ViewAnimator的结构也很简单明了,ViewAnimatorAnimationBuilder双向关联.AnimationListener.StartAnimationListener.Stop两个接口是单独定义出来,分别用来在动画开始和结束时的回调。下面我们就来看看具体是如何实现的:

4.源码分析

ViewAnimator的实现并不复杂,我相信大家都应该能看懂,但是作者的实现思路非常值得我们学习,所以我们还是按照我们一直以来的方式来看,根据我们的使用方法,来分析ViewAnimator的调用流程来看具体的实现。

由于ViewAnimator类和AnimationBuilder是相互调用,所以我这里为了防止理解错误,在我们看到的执行的方法都写在了对应的类里,并省略了其他方法。

1.ViewAnimator.animate(mountain, image);的实现:

public class ViewAnimator {
    ...
    private List<AnimationBuilder> animationList = new ArrayList<>();

    public static AnimationBuilder animate(View... view) {
        //创建一个ViewAnimator对象.
        ViewAnimator viewAnimator = new ViewAnimator();
        //通过addAnimationBuilder方法返回一个AnimationBuilder对象
        return viewAnimator.addAnimationBuilder(view);
    }

    public AnimationBuilder addAnimationBuilder(View... views) {
        //创建一个animationBuilder对象并添加到animationList中去
        AnimationBuilder animationBuilder = new AnimationBuilder(this, views);
        animationList.add(animationBuilder);
        return animationBuilder;
    }
    ...
}

public class AnimationBuilder {
    ...
    private final ViewAnimator viewAnimator;
    private final View[] views;

    public AnimationBuilder(ViewAnimator viewAnimator, View... views) {
        //分别赋值viewAnimator和views
        this.viewAnimator = viewAnimator;
        this.views = views;
    }
    ...
}

可以看到这一步初始化了一个ViewAnimator对象和一个AnimationBuilder,并将AnimationBuilder对象保存在了ViewAnimatoranimationList数组里,Views则保存在了AnimationBuilder对象里.

2.dp().translationY(-500, 0).alpha(0, 1);的实现:

由于返回了一个AnimationBuilder对象,所以dp()方法肯定在AnimationBuilder里实现:

public class AnimationBuilder {
    ...
    public AnimationBuilder dp() {
        //标记nextValueWillBeDp
        nextValueWillBeDp = true;
        return this;
    }

    public AnimationBuilder translationY(float... y) {
        return property("translationY", y);
    }

    public AnimationBuilder alpha(float... alpha) {
        return property("alpha", alpha);
    }

    public AnimationBuilder property(String propertyName, float... values) {
        //遍历views中的所有view,依次实例化ObjectAnimator对象
        //并添加到AnimationBuilder的animatorList对象中.
        for (View view : views) {
            this.animatorList.add(ObjectAnimator.ofFloat(view, propertyName, getValues(values)));
        }
        return this;
    }
    ...
}

先是标记了nextValueWillBeDp,然后translationY(float... y)alpha(float... alpha)方法都是调用了property(String propertyName, float... values)方法,然后在这个方法里去实例化对应的ObjectAnimator对象,这样就避免了我们重复写很多创建ObjectAnimator对象的代码了,所以我们类似的操作下面这些属性时都会调用这个方法:

  • translationY
  • translationX
  • alpha
  • scaleX
  • scaleY
  • rotationX
  • rotationY
  • rotation

3.andAnimate(text).textColor(Color.BLACK, Color.WHITE).backgroundColor(Color.WHITE, Color.BLACK)的实现:

public class AnimationBuilder {
    ...
    public AnimationBuilder andAnimate(View... views) {
        return viewAnimator.addAnimationBuilder(views);
    }
    ...
}

public class ViewAnimator {
    ...
    public AnimationBuilder addAnimationBuilder(View... views) {
        //创建一个animationBuilder对象并添加到animationList中去
        AnimationBuilder animationBuilder = new AnimationBuilder(this, views);
        animationList.add(animationBuilder);
        return animationBuilder;
    }
    ...
}

注意这里是先在AnimationBuilderviewAnimator初始化了一个新的AnimationBuilder对象并返回了,当然也同样添加进了animationList,所以下面的textColor(Color.BLACK, Color.WHITE).backgroundColor(Color.WHITE, Color.BLACK)就会实例化text对应的ObjectAnimator对象了,代码这里我们就不贴了,我们继续往下看。

4.waitForHeight().interpolator(new Interpolator()).duration(2000);方法的实现

public class AnimationBuilder {
    ...
    public AnimationBuilder waitForHeight() {
        //waitForHeight表示当View开始绘制的时候再开始动画.
        waitForHeight = true;
        return this;
    }

    public AnimationBuilder interpolator(Interpolator interpolator) {
        //赋值插值器,直接赋值给了viewAnimator中的interpolator对象
        viewAnimator.interpolator(interpolator);
        return this;
    }

    public AnimationBuilder duration(long duration) {
        //设定动画持续时间,也是直接赋值给了viewAnimator的duration对象
        viewAnimator.duration(duration);
        return this;
    }
    ...
}

这些都很简单,我们继续往下看thenAnimate(percent).custom(...);方法。

5.thenAnimate(percent).custom(…);方法的实现

public class AnimationBuilder {
    ...
    public AnimationBuilder thenAnimate(View... views) {
        //直接调用viewAnimator的thenAnimate()方法
        return viewAnimator.thenAnimate(views);
    }
    ...
}

public class ViewAnimator {
    ...
    public AnimationBuilder thenAnimate(View... views) {
        //再创建一个nextViewAnimator对象
        ViewAnimator nextViewAnimator = new ViewAnimator();
        //将nextViewAnimator赋值给当前ViewAnimator对象的next.
        this.next = nextViewAnimator;
        nextViewAnimator.prev = this;
        //return一个nextViewAnimator创建的AnimationBuilder对象
        return nextViewAnimator.addAnimationBuilder(views);
    }
    ...
}

可以看到这里是又创建了一个新的ViewAnimator对象和一个新的AnimationBuilder对象,注意这里的nextprev赋值,其实就是数据结构中双向链表的思想,这里我们在动画开始的时候就可以根据prev来找到最早的ViewAnimator对象,然后再用next就可以将动画顺序执行了。

5.custom(…);方法的实现

在使用方法里我们是这样调用的:

    .custom(new AnimationListener.Update<TextView>() {
        @Override
        public void update(TextView view, float value) {
            view.setText(String.format(Locale.US, "%.02f%%", value));
        }
    }, 0, 1)

这样就实现了textView从0-1的进度显示.原理如下:

public class AnimationListener {
    ...
    public interface Update<V extends View>{
        void update(V view, float value);
    }
    ...
}

public class AnimationBuilder {
    ...
    public AnimationBuilder custom(final AnimationListener.Update update, float... values) {
        //遍历所有view,实例化valueAnimator,并在onAnimationUpdate()回调接口里,回调
        //update接口,最后把valueAnimator添加到animatorList中去
        for (final View view : views) {
            ValueAnimator valueAnimator = ValueAnimator.ofFloat(getValues(values));
            if (update != null)
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        //noinspection unchecked
                        update.update(view, (Float) animation.getAnimatedValue());
                    }
                });
            add(valueAnimator);
        }
        return this;
    }
    ...
}

我们可以看到update接口里可以传入任何View的子类,但是其实在AnimationBuilder里会遍历所有当前动画View的并全部添加了这个valueAnimator,这样做有一个问题就是,虽然作者想传入泛型是想不在回调方法里强制转换从而直接做操作,但是这样做是不安全的,如果现在我把thenAnimate(percent).custom(...);方法写成thenAnimate(percent, image).custom(...);运行时立马会报ClassCastException:

    java.lang.ClassCastException: android.support.v7.widget.AppCompatImageView cannot be cast to android.widget.TextView

因为由于每个做动画的view都添加了这个回调,再回调处理的时候又会直接当成TextView来处理所以会崩溃。所以我们在使用的时候一定要注意这个。当然在下文的个人评价中我也会给出解决办法,这里我们知道就好了。

下面的调用很相似我们就不看了,我们直接来看start()方法.

5.start();方法的实现

首先是在AnimationBuilder中直接调用了ViewAnimatorstart()方法:

    public void start() {
        viewAnimator.start();
    }

再来看ViewAnimatorstart()方法:

public class ViewAnimator {
    ...
    public ViewAnimator start() {
        if (prev != null) {
            //如果有上一个ViewAnimator则先调用上一个的start()方法
            prev.start();
        } else {
            //创建AnimatorSet对象
            animatorSet = createAnimatorSet();
            //如果需要等待view绘制则监听onPreDraw()方法,
            if (waitForThisViewHeight != null) {
                waitForThisViewHeight.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                    @Override
                    public boolean onPreDraw() {
                        animatorSet.start();
                        waitForThisViewHeight.getViewTreeObserver().removeOnPreDrawListener(this);
                        return false;
                    }
                });
            } else {
                //直接开始
                animatorSet.start();
            }
        }
        return this;
    }

    protected AnimatorSet createAnimatorSet() {
        //新建一个animators列表
        List<Animator> animators = new ArrayList<>();
        //将所有的animationBuilder对象中的Animator对象添加
        for (AnimationBuilder animationBuilder : animationList) {
            animators.addAll(animationBuilder.createAnimators());
        }

        //如果标记了waitForHeight,
        //则返回animationBuilder里View数组的第一个view
        for (AnimationBuilder animationBuilder : animationList) {
            if (animationBuilder.isWaitForHeight()) {
                waitForThisViewHeight = animationBuilder.getView();
                break;
            }
        }
        //如果有ValueAnimator 则单独设置重复模式和重复次数
        for (Animator animator : animators) {
            if (animator instanceof ValueAnimator) {
                ValueAnimator valueAnimator = (ValueAnimator) animator;
                valueAnimator.setRepeatCount(repeatCount);
                valueAnimator.setRepeatMode(repeatMode);
            }
        }

        //设置AnimatorSet的参数
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.playTogether(animators);

        animatorSet.setDuration(duration);
        animatorSet.setStartDelay(startDelay);
        if (interpolator != null)
            animatorSet.setInterpolator(interpolator);

        animatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                //回调Start接口
                if (startListener != null) startListener.onStart();
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                //回调Stop接口
                if (stopListener != null) stopListener.onStop();
                //如果有下一个ViewAnimator则继续执行
                if (next != null) {
                    next.prev = null;
                    next.start();
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

        return animatorSet;
    }
    ...
}

从上到下应该很清晰的看出,其实就是在内部创建了AnimatorSet对象,然后设置一些参数,最后执行,然后再在onAnimationEnd(Animator animation)的接口里检查是否还有动画,从而一直链式的执行。

上面就是整个的ViewAnimator主要的实现了,虽然看上去并不难,但是也不是很容易就能写出来的。值得我们好好学习。

5.个人评价

最近在我负责的项目里,我们设计了大量的组合动画与交互,如果使用原生的方法会使代码量特别大,而且相当难维护,因此我使用了ViewAnimator简化了大量的动画代码,而且使代码更易读,可维护性就更好了。如果你的项目里也有比较多的动画,强烈推荐ViewAnimator而且这个库并没有几个类,占用的体积非常小,推荐成为项目标配。

最后还有两点要说的问题就是:

1.AnimationListener.Update的问题

这个问题我们在上面已经说过了,使用不当会造成崩溃,而且我们把泛型作为参数传入之后,我觉得意义并不大,完全可以在回调接口里直接根据value直接操作我们的View,但是由于ViewAnimator里还有其他地方依赖Update接口,所以我把AnimationListener.Update修改成了下面这样。经测试使用完全没有问题。

public class AnimationListener {

    public interface Update {
        void update(View view, float value);
    }
}

2.为单独的AnimationBuilder添加Interpolator的问题.

在使用中发现如果我同时组合了好几个动画之后,只能为这些同时动画的属性添加同一个Interpolator这样不满足我想同时动画多个View但又要不同的Interpolator需求.所以我就在ViewAnimator的基础上添加了单个Interpolator的功能,而且给ViewAnimator的作者发了pull request.详细原理我就不讲了,比较简单,大家可以在我fork的分支上查看具体的实现方法:
Commit地址在这. 好了今天就写到这吧。

我每周会写一篇源代码分析的文章,以后也可能会有其他主题.
如果你喜欢我写的文章的话,欢迎关注我的新浪微博@达达达达sky
地址: weibo.com/u/203068311…
每周我会第一时间在微博分享我写的文章,也会积极转发更多有用的知识给大家.谢谢关注^_^,说不定什么时候会有福利哈.

    原文作者:Android源码分析
    原文地址: https://juejin.im/entry/59e5c51ef265da43346efd11
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞