这一篇我们来聊一聊高大上的动画效果。
首先说一个常识,一个对理解动画最重要的概念,亦是动画的本质:
动画的原理是利人眼的视觉暂留的特性,即如果一帧帧图像切换的足够快的话,人眼就察觉不到停顿,看起来就像连续的动画了。
动画的原理很简单,就是让图像进行快速的切换。动画的难点是计算出每两帧之间的差异,比如一个位移动画,对于每一帧你都必须计算出它的位置,如果是直线匀速的。很容易计算,但如果是曲线的而且还是有加速度(即移动的速度是会变化的)的,那么计算就会变的复杂了。
总结一下,动画有两个要素,一个是若干的帧图像,一个是变化。
回到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则相反,开始快然后越来越慢。