ViewAnimator源码分析

作者: SkyKai | 来源:发表于2016-03-26 13:45 被阅读5472次

    我每周会写一篇源代码分析的文章,以后也可能会有其他主题.
    如果你喜欢我写的文章的话,欢迎关注我的新浪微博@达达达达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
    每周我会第一时间在微博分享我写的文章,也会积极转发更多有用的知识给大家.谢谢关注_,说不定什么时候会有福利哈.

    相关文章

      网友评论

      • 一心729:喜欢前辈的文章,每期都会看!支持!!
        SkyKai:@我所向往的sky 谢谢支持!有你们支持,更新起来更有动力了 :smile: :smile:
      • Luke_单车:薄荷还招人吗
        SkyKai: @Lukey丶宽 暂时不招了
      • fe5b2366f21c:支持,原来ViewAnimator这么好用~
      • 56a2e8b8a207:薄荷真牛X,全是高手
      • 606fd5f5448c:我也是跟楼上一样的原因找到你的简书的,很厉害,向你学习,加油!
      • 6b464ed79fb4:从stormzhang知道了作者、并且第一时间关注努力+天分
        SkyKai:@Edsion 谢谢支持,以后会有更多好文章的 :blush:
      • miaoyongjun:支持你,看到掘金的文章过来的
        SkyKai:@miaoyongjun 谢谢支持~
      • yabin小站:下周做哪方面的源码研究?
        SkyKai: @ayyb1988 下周分享一个二维码扫描库barcodescanner,如果有其他想看的项目,欢迎推荐给我哈
      • 木月新痕:前辈写的真不错
        SkyKai:@木月新痕 谢谢 :blush: 会努力写出更多好文章的
      • 41ba36d008d1:好文章,您的项目叫什么名字,我想参考一下的话效果
        SkyKai:@ycdm 食物百科,下个版本会改名成食物派,这月29号上线,动画功能主要在首页的扫码里,上线后可以关注一下哈

      本文标题:ViewAnimator源码分析

      本文链接:https://www.haomeiwen.com/subject/bcvrlttx.html