Android猎奇宝宝_06_聊一聊Android里的动画

Android好奇宝宝_06_聊一聊Android里的动画

这一篇我们来聊一聊高大上的动画效果。


首先说一个常识,一个对理解动画最重要的概念,亦是动画的本质:

动画的原理是利人眼的视觉暂留的特性,即如果一帧帧图像切换的足够快的话,人眼就察觉不到停顿,看起来就像连续的动画了。


动画的原理很简单,就是让图像进行快速的切换。动画的难点是计算出每两帧之间的差异,比如一个位移动画,对于每一帧你都必须计算出它的位置,如果是直线匀速的。很容易计算,但如果是曲线的而且还是有加速度(即移动的速度是会变化的)的,那么计算就会变的复杂了。


总结一下,动画有两个要素,一个是若干的帧图像,一个是变化。


回到Android的动画体系,有一道很普遍的面试题:Android中动画的种类?

在我以前面试时,答案还只有两种,不过现在3.0版本以后现在变成3种了。


下面我们一种一种讲。


(1)Frame Animation(帧动画)

这个是最简单的,即我们提供第一帧到最后一帧的所有帧,然后系统帮我们快速的显示出来而已,没啥好说的。

这种动画在实际开发中也比较少用,因为需要大量的图片资源,浪费存储空间。


(2)传统View动画

我先说下这种动画的原理,然后我们再到源码中去验证。


前面说过,动画有两个要素:若干的帧和变化。


而传统View动画的原理就是:我们只提供一帧和变化,然后系统基于我们提供的这一帧和变化,去生成动画需要的所有帧,然后不停的刷新界面轮播帧直到动画结束。


来看一下一个最简单的动画实现:

		Button btn=new Button(MainActivity.this);
		ScaleAnimation anim=new ScaleAnimation(0, 1, 0, 1);
		btn.startAnimation(anim);

这里的Button的初始状态就是我们提供的一帧,构造ScaleAnimation的参数列表就是我们提供的变化。


startAnimation是通知系统开始执行动画的方法:

    public void startAnimation(Animation animation) {
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
        setAnimation(animation);
        invalidateParentCaches();
        invalidate(true);
    }

startAnimation会将要执行的动画保存(setAnimation),然后请求重绘。所以我们可以确定在重绘过程中,一定会对这个保存动画的变量进行是否为空和动画类型的判断。

这里呢不打算详细讲请求重绘的过程,我们只需要知道重绘的请求会一直向上向父View传递,然后到最顶层父View后再反向向下传递,我们这里只关注传递到Button的父View时发生的事。

我们知道所有容器View都是ViewGroup或其子类,在重绘时子View是由父View来绘制出来的,这里我们从下面的ViewGroup的dispatchDraw方法开始追踪:

    protected void dispatchDraw(Canvas canvas) {
    	
        //...省略非关键代码...
    	
        //看到回调方法onAnimationStart在这里被调用,说明动画是从这里之后就开始的
            if (mAnimationListener != null) {
                mAnimationListener.onAnimationStart(controller.getAnimation());
            }
            
        //...省略非关键代码...
            
        boolean more = false;
        final long drawingTime = getDrawingTime();
        //这个是判断child是否有特定的绘制顺序,跟我们的动画实现无关,我们只关注一种情形
        if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
            for (int i = 0; i < count; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        } else {
            for (int i = 0; i < count; i++) {
                final View child = children[getChildDrawingOrder(count, i)];
                //如果这个child可见或者有动画需要执行的话
                //由于我们之前在 startAnimation方法中调用了setAnimation(animation)方法
                //所以getAnimation()结果不为空
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    //more用来标示动画的状态,more==true时表示动画还没结束,为false则表示动画已经结束了
                	more |= drawChild(canvas, child, drawingTime);
                }
            }
        }
        //...省略非关键代码...
    }

跳到drawChild(canvas, child, drawingTime)方法:

    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

跳到child的draw(Canvas canvas, ViewGroup parent, long drawingTime)方法,这个方法很长,我只说明一些关键语句:

boolean more = false;
同样用来标示动画是否结束


Transformation transformToApply = null;
之前说过动画的所有帧都有系统生成,而Transformation是用来描述每一帧的状态信息,Transformation中有3个成员变量:

	//一个矩阵,通过改变它可以改变画布
	//会影响画布的大小、位置和旋转角度
	//而画布的改变就会影响到绘制在其上面的View
	//传统View动画就是通过改变画布(Canvas)的方式去产生所有的帧
    protected Matrix mMatrix;
    //影响透明度
    protected float mAlpha;
    //动画需要改变的类型
    //大小,位移、旋转需要改变mMatrix
    //透明度转变动画需要改变mAlpha
    protected int mTransformationType;

继续:
    final Animation a = getAnimation();
    if (a != null) {
        more = drawAnimation(parent, drawingTime, a, scalingRequired);
        //...省略非关键代码...
        
        //这一句后面会说明
        transformToApply = parent.getChildTransformation();
    }

先看下drawAnimation(ViewGroup parent, long drawingTime,Animation a, boolean scalingRequired)方法:

    private boolean drawAnimation(ViewGroup parent, long drawingTime,
            Animation a, boolean scalingRequired) {
        Transformation invalidationTransform;
        final int flags = parent.mGroupFlags;
        //这里判断动画是否进行过初始化,若否则进行初始化
        final boolean initialized = a.isInitialized();
        if (!initialized) {
            a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
            a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
            if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
            onAnimationStart();
        }
        //这里注意帧信息是保存在parent的
        final Transformation t = parent.getChildTransformation();
        //这一句是关键,是计算变化的地方
        //getTransformation方法的作用就是要根据动画已经进行的时间和一些其它信息
        //来计算出当前时刻要显示的这一帧的状态信息,并保存到t中
        //接下来后面就会从t取出信息来改变画布
        boolean more = a.getTransformation(drawingTime, t, 1f);
        //...省略非关键代码...

        //如果动画还没到结束时间
        if (more) {
            //会在这里调用 parent.invalidate()方法请求重绘,然后呢又会去计算下一帧的信息,改变画布,绘制帧,一直循环直到动画结束
        }
        return more;
    }

回到draw(Canvas canvas, ViewGroup parent, long drawingTime)方法,前面没说明的:

transformToApply = parent.getChildTransformation();

现在我们知道了drawAnimation方法计算出来的绘制当前帧的信息是保存在parent里的,这里就是把它取出来。

接下来还是在draw(Canvas canvas, ViewGroup parent, long drawingTime)方法里,如果是动画类型是需要改变Matrix的话,会调用:

canvas.concat(transformToApply.getMatrix());


要改变透明度的话会调用:

canvas.saveLayerAlpha();

最后调用:

draw(canvas);

把改变后的画布传给draw(canvas)方法,开始绘制帧。


小结:传统View动画通过改变Canvas来生成动画所需要的帧,每一帧Canvas的变化信息由Transformation类来保存,而该如何变化由Animation实现类来决定,具体是每个Animation的子类都重写了方法:

protected void applyTransformation(float interpolatedTime, Transformation t)

参数interpolatedTime是动画已经进行的时间(注:这是在没有设置Interpolator的情况下,关于Interpolator我会在后面再说明),t用来保存计算出来的结果。


补充:传统View动画有一个经常会出现的问题就是,一个按钮在进行位移动画之后,如果设置了setFillAfter(true),那么会停留在最后一帧,但是点击的触发位置还是在原位置。如果你仔细阅读了上面得源码分析,你就明白原因了:传统View动画只是改变画布,对于进行动画的物体(比如我们例子中的Button)并不会进行改变,而触摸、点击等事件的位置判断并不受画布的影响。


一个不完美的解决方法:不要设置setFillAfter(true),设置动画监听,在动画结束时调用layout()方法进行重新布局。

例子:

对一个按钮(btn)进行了一个向右移100、向下移200的动画:

		anim = new TranslateAnimation(0, 100, 0, 200);
		btn.startAnimation(anim);
设置监听:

		anim.setAnimationListener(new AnimationListener() {
			public void onAnimationStart(Animation animation) {
			}
			public void onAnimationRepeat(Animation animation) {
			}
			@Override
			public void onAnimationEnd(Animation animation) {
				// TODO Auto-generated method stub
				btn.layout(btn.getLeft() + 100, btn.getTop() + 200,
						btn.getRight() + 100, btn.getBottom() + 200);
			}
		});

这种方法的缺陷:

(1)layout方法会导致Button闪一下。

(2)这里只是一个简单的位移动画,如果动画复杂的话,想计算出layout方法的4个参数也会变得很复杂。


(3)Property Animation(属性动画)

属性动画跟传统View动画是类似的,其实所有动画都是类似的,不同的是我们是通过改变什么来达到动画的视觉效果的。

传统View动画通过改变View所在的画布,让View随着画布的变化而变化,但直接改变要进行动画的物体本身可能更简单。

属性动画就是通过改变物体的属性来达到动画效果的,这里说物体而不是View是因为Android的属性动画并没有规定进行动画的必须是View(当然大多数、几乎全部、差不多都是View),它只是依据我们提供的改变去改变一个对象的属性值,至于改变了这个属性值之后会发生什么事,它是不管的。


说下属性动画最重要的两个类:


(1)ValueAnimator

这个类就像它的名字一样,值的动画师,它只关注值的变化,依据我们给出的变化来提供某个时刻的值应该为多少。

例子:

我们想让一个值在1s的时间内从0匀速地变为1,那么ValueAnimator会在0ms时返回0,500ms时返回0.5,以此类推。


(2)ObjectAnimator

ObjectAnimator是ValueAnimator的子类,它与ValueAnimator的区别是它不仅仅提供值,它还会在得出值后去改变属性值。


ObjectAnimator的实现原理:

		ObjectAnimator objectAnimator=ObjectAnimator.ofFloat(btn, "x", new float[]{0,100});
		objectAnimator.setDuration(1000);
		objectAnimator.start();


第一个参数就是我们想改变其属性的对象。


第二个参数是属性的名称,要注意的是这并不意味着这个对象就必须要拥有这个属性变量。

因为ObjectAnimator是通过反射属性的getter和setter方法去改变和获取属性值,所以你只要有对应的getter和setter方法(要符合驼峰命名规则)。


第三个参数就是我们提供的变化,一个数组参数,数组的大小必须为2或者1。

2的情况:数组第一个元素做为初始值,第二个作为结束值,这种情况下对应的getter方法不是必须的。

1的情况:ObjectAnimator通过反射调用getter方法将获得的结果作为初始值,将数组中唯一元素作为结束值,这种情况下getter方法是必须的。


我来用文字来描述一下上面几条语句表达的意思:

请在1s内匀速的将btn的"x"属性值从0增长到100,谢谢!


注:像我上面说的,改变属性值不是真的类似btn.x=value这种方式,比如上面这个例子,ObjectAnimator每隔一段时间,当ValueAnimator计算出新的值时,它就会通过反射去执行语句btn.setX(value);,直到动画时间结束。


当然,对于View来说,在调用setX方法时肯定会去请求重绘,而在重绘过程中,不管setX做了什么,最终肯定会影响到View绘制出来的水平位置。当这些都不关ObjectAnimator事,ObjectAnimator只是改变属性值,不关心改变后会发生什么。


关于属性动画的源码我就不分析了,有兴趣的推荐一篇博客,讲得很好:传送门


附:Interpolator

前面的例子都是匀速地变化,而Interpolator就是可以改变改变速度的东东(是两个改变,我没打错),可以实现类似物理中的加速度,但其实可以实现更多。


不管是传统View动画还是属性动画,都得先计算出下一帧的信息再去请求刷新,而Interpolator就是提供一个对计算出来的值一次修改的机会。还是上面的例子:

在没有设置Interpolator的情况下,"x"的值在500ms时应该为50;

如果设置了一个越来越快的Interpolator,那么"x"的值在500ms时应该小于50,btn开始会移动地比较慢,然后越来越快。Interpolator就是在得出"x"为50的基础上再进行一次修改,这次修改可以是任意的,但一般不会偏差太大。可以是40、60、100,这是比较正常的,也可以是200、99999999,这些也是可以的,不过我们不会这么做。

回到前面的:

protected void applyTransformation(float interpolatedTime, Transformation t)
参数名称是interpolatedTime,表明Interpolator是通过改变动画已经进行的时间来改变最终值的。


我们可以自定义Interpolator,只要实现Interpolator接口重写float getInterpolation(float input);方法,这里的input并不是50,而是动画已进行时间的百分比。系统已经实现了几个常用的Interpolator,一般情况下是够用了。如AccelerateInterpolator就是上面说的开始慢,然后越来越快,与之对应的DecelerateInterpolator则相反,开始快然后越来越慢。