ViewAnimator源码分析

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


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

1.简介

ViewAnimator.png

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

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相互组合再加上各种动画,可想而知代码量会有多少了。下面我们就用属性动画来写一个下面这张图里的动画:

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.类关系图

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
地址: http://weibo.com/u/2030683111
每周我会第一时间在微博分享我写的文章,也会积极转发更多有用的知识给大家.谢谢关注_,说不定什么时候会有福利哈.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容