Android动画深入分析

Android的动画可以分为三种:View动画、帧动画和属性动画。帧动画通过顺序播放一系列图像从而产生动画效果,如果图片过大就会导致OOM;View动画通过对场景里的对象不断做图像变换(平移、缩放、旋转、透明度)从而产生动画效果,它是一种渐进式动画,并且View动画支持自定义;属性动画通过动态地改变对象的属性从而达到动画效果,属性动画为API11的新特性。

一、 View动画

View动画的作用对象是View,它支持4种动画效果,分别为平移、缩放、旋转、透明度动画。

1.1 View动画的种类

View动画的四种变换效果对应着Animation的四个类: TranslateAnimation、ScaleAnimation、RotateAnimation和AlphaAnimation,如下表,这四种动画既可以通过XML来定义,也可以通过代码来动态创建,对于View动画,建议采用XML来定义动画,这样可读性更好。

名 称 标 签 子 类 效 果
平移动画 <translate> TranslateAnimation 移动View
缩放动画 <scale> ScaleAnimation 放大或缩小View
旋转动画 <rotate> RotateAnimation 旋转View
透明度动画 <alpha> AlphaAnimation 改变View的透明度

要使用View动画,首先要创建动画的XML文件,这个文件的路径为:res/anim/filename.xml。View动画的描述文件是有固定的语法的,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@[package:]anim/interpolator_resource"
android:shareInterpolator=["true" | "false"] >
<alpha
android:fromAlpha="float"
android:toAlpha="float"/>
<scale
android:fromXScale="float"
android:toXScale="float"
android:fromYScale="float"
android:toYScale="float"
android:pivotX="float"
android:pivotY="float"/>
<translate
android:fromXDelta="float"
android:toXDelta="float"
android:fromYDelta="float"
android:toYDelta="float"/>
<rotate
android:fromDegrees="float"
android:toDegrees="float"
android:privotX="float"
android:privotY="float"/>
<set>
...
</set>
</set>

从上面语法可以看出,View动画既可以是单个动画,也可以由一系列动画组成。

<set>标签表示动画集合,对应AnimationSet类,它可以包含若干个动画,并且它的内部也是
可以嵌套其它动画集合的,它的两个属性的含义如下:

android:interpolator

表示动画集合所采用的插值器,插值器影响动画的速度,比如非匀速动画就需要
通过插值器来控制动画的播放过程。这个属性可以不指定,默认为@android:anim/
accelerate_decelerate_interpolator,即加速减速插值器。

android:shareInterpolator

表示集合中的动画是否和集合共享同一个插值器。如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或者使用默认值。

<translate> 标签表示平移动画,对应TranslateAnimation类,它可以使一个View在水平和竖直方向完成平移的动画效果,它的一系列属性含义如下:

  • android:fromXDelta——表示 x 的起始值,比如 0;
  • android:toXDelta——表示 x 的结束值,比如 100;
  • android:fromYDelta——表示 y 的起始值;
  • android:toYDelta——表示 y 的结束值。

<scale> 标签表示缩放动画,对应ScaleAnimation,它可以使View具有放大或者缩小的动画效果,它的一系列属性的含义如下:

  • android:fromXScale——水平方向缩放的起始值,比如 0.5;
  • android:toXScale——水平方向缩放的结束值,比如 1.2;
  • android:fromYScale——竖直方向缩放的起始值;
  • android:toYScale——竖直方向缩放的结束值;
  • android:pivotX——缩放的轴点的 x 坐标,它会影响缩放的效果;
  • android:pivotY——缩放的轴点的 y 坐标,它会影响缩放的效果。

    在<scale>标签中提到的轴点的概念,默认情况下轴点是View的中心点,如果水平方向进行缩放的话会导致View向左右两个方向同时进行缩放,但是如果把轴点设为View的右边界,那么View就只会向左边进行缩放,反之则向右边进行缩放。

<rotate> 标签标示旋转动画,对应RotateAnimation类,它的属性含义如下:

  • android:fromDegrees——旋转开始的角度,比如 0;
  • android:toDegrees——旋转结束的角度,比如 180;
  • android:pivotX——旋转的轴点的 x 坐标;
  • android:pivotY——旋转的轴点的 y 坐标。

    在旋转动画中也有轴点的概念,它也会影响到旋转的具体效果。它扮演着旋转轴的角色,即View是围绕着轴点进行旋转的,默认情况下轴点位View的中心点。

<alpha> 标签表示透明度动画,对应AlphaAnimation,它可以改变View的透明度,它的属性含义如下:

  • android:fromAlpha——表示透明度的起始值,比如 0.1;
  • android:toAlpha——表示透明度的结束值,比如 1。

上面介绍了View动画的XML格式,除了上面介绍的属性外,View动画还有一些常用的属性,如下:

  • android:duration——动画的持续时间;
  • android:fillAfter——动画结束以后View是否停留在结束位置,true表示View停留在结束位置,false则不停留。

下面是一个实际例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:fillAfter="true" >

<translate
android:duration="100"
android:fromXDelta="0"
android:toXDelta="100"
android:fromYDelta="0"
android:toYDelta="100"
android:interpolator="@android:anim/linear_interpolator"/>

<rotate
android:duration="400"
android:fromDegrees="0"
android:toDegrees="90" />

使用上面的动画:

1
2
3
Button mButton = (Button)findViewById(R.id.button);
Animation animation = AnimationUtils.loadAnimation(this,R.anim.animation_test);
mButton.startAnimation(animation);

除了在XML中定义动画外,还可以通过代码来应用动画,如下:

1
2
3
AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
alphaAnimation.setDuration(300);
mButton.startAnimation(alphaAnimation);

另外,通过Animation的setAnimationListener方法可以给View动画添加过程监听,接口如下所示:

1
2
3
4
5
public static interface AnimationListener{
void onAnimationStart(Animation animation);
void onAnimationEnd(Animation animation);
void onAnimationRepeat(Animation animation);
}

1.2 自定义View动画

除了系统提供的四种View动画外,我们还可以自定义View动画。只需要继承Animation这个抽象类,然后重写它的initialize和applyTransformation方法,在initialize方法中做一些初始化工作,在applyTransformation中进行相应的矩阵变换即可,很多时候需要采用Camera来简化矩阵变换的过程。下面例子来自Android的ApiDemos中的一个自定义View动画Rotate3dAnimation,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class Rotate3dAnimation extends Animation{
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private final float mDepthZ;
private final boolean mReverse;
private Camera mCamera;

public Rotate3dAnimation(float fromDegrees,float toDegrees,
float centerX,float centerY,float depthZ,boolean reverse){
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;
mReverse = reverse;
}

@Override
public void initialize(int width,int height,int parentWidth,int parentHeight){
super.initialize(width,height,parentWidth,parentHeight);
mCamera = new Camera();
}

@Override
protected void applyTransformation(float interpolatedTime,Transformation t){
final float fromDegrees = mFromDegress;
float degrees = fromDrees + ((mToDegrees - fromDegrees) * interpolatedTime);

final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;

final Matrix matrix = t.getMatrix();

camera.save();
if(mReverse){
camera.translate(0.0f,0.0f,mDepthZ * interpolatedTime);
}else{
camera.translate(0.0f,0.0f,mDepthZ * (1.0f - interpolatedTime));
}
camera.rotateY(degrees);
camera.getMatrix(matrix);
camera.restore();

matrix.preTranslate(-centerX,-centerY);
matrix.postTranslate(centerX,centerY);
}
}

1.3 帧动画

帧动画是顺序播放一组预先定义好的图片,类似电影播放。不同于View动画,系统提供了另外一个类AnimationDrawable来使用帧动画。帧动画使用比较简单,首先需要通过XML来定义一个AnimationDrawable,如下所示:

1
2
3
4
5
6
7
8
// res/drawable/frame_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/image1" android:duration="500"/>
<item android:drawable="@drawable/image2" android:duration="500"/>
<item android:drawable="@drawable/image3" android:duration="500"/>
</animation-list>

然后将上述的Drawable作为View的背景并通过Drawable来播放动画即可:

1
2
3
4
Button mButton = (Button)findViewById(R.id.button1);
mButton.setBackgroundResource(R.drawable.frame_animation);
AnimationDrawable drawable = (AnimationDrawable)mButton.getBackground();
drawable.start();

帧动画使用比较简单,但是比较容易引起OOM,所以使用帧动画时应尽量避免使用过大尺寸的图片。

二、 View动画的特殊使用场景

在1.1中介绍了View动画的四种形式,除了这四种形式外,View动画还可以在一些特殊场景下使用,比如在ViewGrop中可以控制子元素的出场效果,在Activity中可以实现不同Activity之间的切换效果。

2.1 LayoutAnimation

LayoutAnimation作用于ViewGroup,为ViewGroup指定一个动画,这样当它的子元素出场时都会具有这种动画效果。这种效果常常被用在ListView上,我们时常会看到一种特殊的ListView,它的每个item都以一定的动画的形式出现,其实这并非什么高深的技术,它是使用的就是LayoutAnimation。LayoutAnimation也是一个View动画,为了给ViewGroup的子元素加上出场效果,遵循如下步骤。

(1) 定义LayoutAnimation,如下所示

1
2
3
4
5
6
// res/anim/anim_layout.xml
<layoutAnimation
xmlns:android="http://schemas.android.com/apk/res/android"
android:delay="0.5"
android:animationOrder="normal"
android:animation="@anim/anim_item"/>

它的属性的含义如下

android:delay

表示子元素开始动画的时间延迟,比如子元素入场动画的时间周期为300ms,那么0.5表示每个子元素都需要延迟150ms才能播放入场动画。总体来说,第一个子元素延迟150ms开始播放入场动画,第二个子元素延迟300ms开始播放动画,依次类推。

android:animationOrder

表示子元素动画的顺序,有三种选项: normal、reverse 和 random,其中 nomal 表示顺序显示,即排在前面的子元素先开始播放入场动画;reverse 表示逆向显示,即排在后面的子元素先开始播放入场动画;random 则是随机播放入场动画。

android:animation

为子元素指定具体的入场动画。

(2) 为子元素指定具体的入场动画,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// res/anim/anim_item.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:interpolator="@android:anim/accelerate_iterpolator"
android:shareInterpolator="true" >
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0"/>

<translate
android:fromXDelta="500"
android:toXDelta="0" />
</set>

(3) 为ViewGroup指定android:layoutAnimation属性: android:layoutAnimation=”@anim/anim_layout”。对于ListView来说,这样ListView的item就具有出场动画了,这种方式适用于所有的ViewGroup,如下所示

1
2
3
4
5
<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutAnimation="@anim/anim_layout" />

除了在XML中指定LayoutAnimation外,还可以通过LayoutAnimationController来实现,具体代码如下

1
2
3
4
5
6
ListView listView = (ListView)layout.findViewById(R.id.list);
Animation animation = AnimationUtils.loadAnimation(this,R.anim.anim_item);
LayoutAnimationController controller = new LayoutAnimationController(animation);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
listView.setLayoutAnimation(controller);

2.2 Activity 的切换效果

Activity 有默认的切换效果,但这个效果我们可以自定义,主要用到overridePendingTransition(int enterAnim,int exitAnim)这个方法,这个方法必须在startActivity(Intent)或者finish()之后被调用才能生效,它的参数含义如下:

  • enterAnim——Activity被打开时,所需的动画资源id;

  • exitAnim——Activity被暂停时,所需的动画资源id。

当启动一个Activity时,可以按照如下方式为其添加自定义的切换效果:

1
2
3
Intent intent = new Intent(this,TestActivity.class);
startActivity(intent);
overridePendingTransition(R.anim.enter_anim,R.anim.exit_anim);

当Activity退出时,也可以为其指定自己的切换效果,如下

1
2
3
4
5
@Override
public void finish(){
super.finish();
overridePendingTransition(R.anim.enter_anim,R.anim.exit_anim);
}

需要注意的是,overridePendingTransition这个方法必须位于startActivity 或者finish的后面,否则动画效果将不起作用。

Fragment也可以添加切换动画,由于Fragment是在API 11中新引入的类,因此为了兼容性我们需要使用support-v4这个兼容包,在这种情况下可以通过FragmentTransation中的setCustomAnimations()方法来添加切换动画,这个切换动画需要是View动画,之所以不能采用属性动画是因为属性动画也是API 11新引入的。

三、 属性动画

属性动画是API 11 新加入的特性,和 View 动画不同,它对作用对象进行了扩展,属性动画可以对任何对象做动画,甚至还可以没有对象。属性动画中有ViewPropertyAnimator、ObjectAnimator、ValueAnimator 和 AnimatorSet 等概念,通过他们可以实现绚丽的动画。

3.1 使用属性动画

属性动画可以对任意对象的属性进行动画而不仅仅是View,动画默认的时间间隔300ms,默认帧率 10ms/帧。其可以达到的效果是: 在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。因此,属性动画几乎是无所不能的,只要对象有这个属性,它都能实现动画效果。可以采用开源动画库nineoldandroids来兼容以前的版本。

Nineoldandroids 对属性动画做了兼容,在API 11以前的版本其内部是通过代理View动画来实现的,因此在Android低版本上,它的本质还是View动画。比较常用的几个动画类是:ValueAnimator、ObjectAnimator 和 AnimatorSet,其中ObjectAnimator继承自ValueAnimator,AnimatorSet是动画集合,可以定义一组动画,如何使用属性动画呢?下面举几个简单例子。

(1) 使用ViewPropertyAnimator 改变View属性

使用方式:View.animate() 后跟 translationX() 等方法,动画会自动执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
view.animate()//获取ViewPropertyAnimator对象
//动画持续时间
.setDuration(5000)

//透明度
.alpha(0)
.alphaBy(0)

//旋转
.rotation(360)
.rotationBy(360)

//插值器
.setInterpolator(new BounceInterpolator())//回弹

//动画延迟
.setStartDelay(1000)

//是否开启硬件加速
.withLayer()

.start();

(2) 改变一个对象(myObject)的translationY属性,让其沿着Y轴向上平移一段距离:

1
ObjectAnimator.ofFloat(myObject,"translationY",-myObject.getHeight()).start();

(3) 改变一个对象的背景色属性,典型的情形是改变View的背景色,下面动画可以让背景色在3秒内实现从0XFFFF8080到0XFF8080FF的渐变,动画会无限循环而且会有反转的效果。

1
2
3
4
5
6
ValueAnimator colorAnim = ObjectAnimator.ofInt(this,"backgroundColor",0xFFFF8080,0xFF8080FF);
colorAnim.setDuration(3000);
colorAnim.setEvaluator(new ArgbEvaluator());
colorAnim.setRepeatCount(ValueAnimator.INFINITE);
colorAnim.setRepeatMode(ValueAnimator.REVERSE);
colorAnim.start();

(4) 动画集合,5秒内对View 的旋转、平移、缩放和透明度都进行了改变

1
2
3
4
5
6
7
8
AnimatorSet set = new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(myView,"rotationX",0,360),
ObjectAnimator.ofFloat(myView,"translationX",0,90),
ObjectAnimator.ofFloat(myView,"scaleX",1,1.5f),
ObjectAnimator.ofFloat(myView,"alpha",1,0.25f,1)
);
set.setDuration(5 * 1000).start();

(5) PropertyValuesHolder 同一个动画中改变多个属性

很多时候,你在同一个动画中会需要改变多个属性,例如在改变透明度的同时改变尺寸。如果使用ViewPropertyAnimator,你可以直接用连写的方式来在一个动画中同时改变多个属性:

1
2
3
4
view.animate()  
.scaleX(1)
.scaleY(1)
.alpha(1);

而对于 ObjectAnimator,是不能这么用的。不过你可以使用 PropertyValuesHolder 来同时在一个动画中改变多个属性。

1
2
3
4
5
6
PropertyValuesHolder holder1 = PropertyValuesHolder.ofFloat("scaleX", 1);  
PropertyValuesHolder holder2 = PropertyValuesHolder.ofFloat("scaleY", 1);
PropertyValuesHolder holder3 = PropertyValuesHolder.ofFloat("alpha", 1);

ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, holder1, holder2, holder3)
animator.start();

PropertyValuesHolder 的意思从名字可以看出来,它是一个属性值的批量存放地。所以你如果有多个属性需要修改,可以把它们放在不同的 PropertyValuesHolder 中,然后使用 ofPropertyValuesHolder() 统一放进 Animator。这样你就不用为每个属性单独创建一个 Animator 分别执行了。

(6) PropertyValuesHolders.ofKeyframe() 把同一个属性拆分

除了合并多个属性和调配多个动画,你还可以在 PropertyValuesHolder 的基础上更进一步,通过设置 Keyframe(关键帧),把同一个动画属性拆分成多个阶段。例如,你可以让一个进度增加到 100% 后再「反弹」回来。

1
2
3
4
5
6
7
8
9
10
// 在 0% 处开始
Keyframe keyframe1 = Keyframe.ofFloat(0, 0);
// 时间经过 50% 的时候,动画完成度 100%
Keyframe keyframe2 = Keyframe.ofFloat(0.5f, 100);
// 时间见过 100% 的时候,动画完成度倒退到 80%,即反弹 20%
Keyframe keyframe3 = Keyframe.ofFloat(1, 80);
PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("progress", keyframe1, keyframe2, keyframe3);

ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, holder);
animator.start();

属性动画除了通过代码实现以外,还可以通过XML来定义。属性动画需要定义在res/animator/目录下,它的语法如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<set 
android:ordering=["together" | "sequentially"]>

<objectAnimator
anddroid:propertyName="string"
android:duration="int"
android:valueFrom="float | int | color"
android:valueTo="float | int | color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode=["restart" | "reverse"]
android:valueType=["intType" | "floatType"]/>

<animator
android:duration="int"
android:valueFrom="float | int | color"
android:valueTo="float | int | color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode=["restart" | "reverse"]
android:valueType=["intType" | "floatType"]/>

<set>
...
</set>

</set>

属性动画的各种参数都比较好理解,在XML中可以定义ValueAnimator 、ObjectAnimator以及AnimatorSet,其中<set>标签对应AnimatorSet,<animator>标签对应ValueAnimator,而<objectAnimator>则对应ObjectAnimator。<set>标签的android:ordering属性有两个可选值:”together”和”sequentially”,其中”together”表示动画集合中的子动画同时播放,”sequentially”则表示动画集合中的子动画按照前后顺序依次播放,android:ordering属性的默认值是”together”。

对于<objectAnimator>标签的各个属性的含义,下面简单说明一下:

  • android:propertyName——表示属性动画的作用对象的属性的名称;

  • android:duration——表示动画的时长;

  • android:valueFrom——表示属性的起始值;

  • android:valueTo——表示属性的结束值

  • android:startOffset——表示动画的延迟时间,当动画开始后,需要延迟多少毫秒才会真正播放此动画;

  • android:repeatCount——表示动画的重复次数;

  • android:repeatMode——表示动画的重复模式;

  • android:valueType——表示android:propertyName所指定的属性的类型,有”intType”和”floatType”两个可选项,分别表示属性的类型为整型和浮点型。另外,如果android:propertyName所指定的属性表示的是颜色,那么不需要指定android:valueType,系统会自动对颜色类型的属性做处理。

对于一个动画来说,有两个属性需要特殊说明一下,一个是android:repeatCount,它表示动画循环的次数,默认值为0,其中-1表示无限循环;另一个是android:repeatMode,它表示动画循环的模式,有两个选项:”restart” 和 “reverse”,分别表示连续重复和逆向重复。连续重复就是动画每次都重新开始播放,而逆向重复是指第一次播放完后,第二次会倒着播放动画,第三次再重头开始播放,第四次再倒着播放,如此反复。

下面是一个具体的例子,通过XML定义一个属性动画并将其作用在View上,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
// res/animator/property_animator.xml
<set android:ordering="together">
<objectAnimator
android:propertyName="x"
android:duration="300"
android:valueTo="200"
android:valueType="intType"/>
<objectAnimator
android:propertyName="y"
android:duration="300"
android:valueTo="300"
android:valueType="intType"/>
</set>

下面介绍如何使用,如下所示

1
2
3
AnimatorSet set = (AnimatorSet)AnimatorInflater.loadAnimator(myContext,R.anim.property_animator);
set.setTarget(mButton);
set.start();

在实际开发中建议采用代码来实现属性动画,这是因为通过代码来实现比较简单,更重要的是,很多时候一个属性的起始值是无法提前确定的。另外,
ViewPropertyAnimator、ObjectAnimator、ValueAnimator 这三种 Animator,
它们其实是一种递进的关系:从左到右依次变得更加难用,也更加灵活。

它们的性能是一样的,因为 ViewPropertyAnimator 和 ObjectAnimator 的内部实现其实都是 ValueAnimator,ObjectAnimator 更是本来就是 ValueAnimator 的子类,它们三个的性能并没有差别。

它们的差别只是使用的便捷性以及功能的灵活性。
所以在实际使用时候的选择,只要遵循一个原则就行:尽量用简单的。
能用 View.animate() 实现就不用 ObjectAnimator,
能用 ObjectAnimator 就不用 ValueAnimator。

3.2 理解插值器和估值器

TimeInterpolator 时间插值器,它的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有LinearInterpolator(线性插值器: 匀速动画)、AccelerateDecelerateInterpolator(加速减速插值器:动画两头慢中间快)和DecelerateInterpolator(减速插值器:动画越来越慢)等。TypeEvaluator 为类型估值算法,也叫估值器,它的作用是根据当前属性改变的百分比来计算改变后的属性值,系统预置的有IntEvaluator(针对整型属性)、FloatEvaluator(针对浮点型属性)和ArgbEvaluator(针对Color属性)。属性动画中的插值器(Interpolator)和估值器(TypeEvaluator)很重要,它们是实现非匀速动画的重要手段。

如下表所示,它表示一个匀速动画,采用了线性插值器和整型估值器算法,在40ms内,View的x属性实现从0到40的变换。

x = 0 x = 10 x = 20 x = 30 x = 40
t = 0ms t = 10ms t = 20ms t = 30ms t = 40ms
duration = 40ms

由于动画的默认刷新率为10ms/帧,所以该动画将分5帧进行,我们来考虑第三帧(x=20,t=20ms),当时间t=20ms的时候,时间流逝的百分比是0.5(20/40=0.5),意味着现在时间过了一半,那x应该改变多少呢?这个就由插值器和估值器算法来确认。对于线性插值器来说,当时间流逝一半的时候,x的变换也应该是一半,即x的改变为0.5,下面是它的源码:

1
2
3
4
5
6
7
8
9
10
11
public class LinearInterpolator implements Interpolator{
public LinearInterpolator(){

}

public LinearInterpolator(Context context, AttributeSet attrs){}

public float getIntterpolation(float input){
return input;
}
}

很显然,线性插值器的返回值和输入值一样,因此插值器返回的值是0.5,这意味着x的改变是0.5,这个时候插值器的工作就完成了。具体x变成了什么值,这个需要估值算法来确定,下面是整型估值算法源码:

1
2
3
4
5
6
public class IntEvaluator implements TypeEvaluator<Integer>{
public Integer evaluate(float fraction,Integer startValue,Integer endValue){
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}
}

上述算法很简单,evaluate的三个参数分别表示估值小数、开始值和结束值,对应于本例分别是0.5,0,40。根据上述算法,整型估值返回给我们的结果是20,这就是(x=20,t=20ms)的由来。

属性动画要求对象的该属性有set方法和get方法(可选)。插值器和估值算法除了系统提供的外,我们还可以自定义。自定义插值器需要实现Interpolator或TimerInterpolator接口,自定义估值算法需要实现TypeEvaluator。

自定义 TypeEvaluator

借助于 TypeEvaluator,属性动画就可以通过 ofObject() 来对不限定类型的属性做动画了。方式很简单:

  • 为目标属性写一个自定义的 TypeEvaluator

  • 使用 ofObject() 来创建 Animator,并把自定义的 TypeEvaluator 作为参数填入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private class PointFEvaluator implements TypeEvaluator<PointF> {  
PointF newPoint = new PointF();

@Override
public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
float x = startValue.x + (fraction * (endValue.x - startValue.x));
float y = startValue.y + (fraction * (endValue.y - startValue.y));

newPoint.set(x, y);

return newPoint;
}
}

ObjectAnimator animator = ObjectAnimator.ofObject(view, "position",
new PointFEvaluator(), new PointF(0, 0), new PointF(1, 1));
animator.start();

3.3 属性动画的监听器

属性动画提供了监听器用于监听动画的播放过程,主要有如下两个接口:AnimatorUpdateListener和AnimatorListener。

AnimatorListener的定义如下:

1
2
3
4
5
6
public static interface AnimatorListener{
void onAnimationStart(Animator animation);
void onAnimationEnd(Animator animation);
void onAnimationCancel(Animator animation);
void onAnimationRepeat(Animator animation);
}

从AnimatorListener的定义可以看出,它可以监听动画的开始、结束、取消以及重复播放。同时为了方便开发,系统还提供了AnimatorListenerAdapter这个类,它是AnimatorListener的适配器类,这样我们就可以有选择地实现上面的4个方法了。

下面再看下AnimatorUpdateListener的定义,如下

1
2
3
public static interface AnimatorUpdateListener{
void onAnimationUpdate(ValueAnimator animation);
}

AnimatorUpdateListener比较特殊,它会监听整个动画过程,动画是由许多帧组成的,每播放一帧,onAnimationUpdate就会被调用一次,利用这个特性,可以做一些特殊的事情。

3.4 对任意属性做动画

如果给一个Button加一个动画,让这个Button的宽度从当前宽度增加到500px,会发现View动画根本不支持对宽度进行动画,View动画只支持四种类型:平移(Translate)、旋转(Rotate)、缩放(Scale)、透明度(Alpha)。我们可以使用属性动画来实现,如下所示:

1
2
3
4
5
6
7
8
9
10
private void performAnimate(){
ObjectAnimator.ofInt(mButton,"width",500).setDuration(5000).start();
}

@Override
public void onClick(View v){
if(v == mButton){
performAnimate();
}
}

上述代码运行后发现没效果,其实没效果是对的,如果随便传递一个属性过去,轻则没动画效果,重则程序直接Crash。

下面分析属性动画的原理:属性动画要求动画作用的对象提供该属性的get和set方法,属性动画根据外界传递的属性的初始值和最终值,以动画的效果多次去调用set方法,每次传递给set方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。总结一下,我们对object的属性abc做动画,如果想让动画生效,要同时满足两个条件:

(1) object必须要提供setAbc方法,如果动画的时候没有传递初始值,那么还要提供getAbc方法,因为系统要去取abc属性的初始值(如果这条不满足,程序直接Crash)。

(2) object的setAbc对属性abc所做的改变必须能够通过某种方法反应出来,比如会带来UI的改变之类的(如果这条不满足,动画无效果但不会Crash)。

以上条件缺一不可。那么为什么我们对Button的width属性做动画会没有效果?这是因为Button内部虽然提供了getWidth和setWidth方法,但是这个setWidth方法并不是改变视图的大小,它是TextView新添加的方法,View是没有这个setWidth方法的,由于Button继承了TextView,所以Button也就有了setWidth方法。下面看下getWidth和setWidth方法的源码:

1
2
3
4
5
6
7
8
9
10
11
public void setWidth(int pixels){
mMaxWidth = mMinWidth = pixels;
mMaxWidthMode = mMinWidthMode = PIXELS;

requestLayout();
invalidate();
}

public final int getWidth(){
return mRight - mLeft;
}

从上述源码可以看出,getWidth的确是获取View的宽度的,而setWidth是TextView和其子类的专属方法,它的作用不是设置View的宽度,而是设置TextView的最大宽度和最小宽度的,这个和TextView的宽度不是一个东西,具体来说,TextView的宽度对应XML中的android:layout_width属性,而TextView还有一个属性android:width,这个android:width属性就对应了TextView的setWidth方法。总之,TextView和Button的setWidth、getWidth干的不是同一件事情,通过setWidth无法改变控件的宽度,所以对width做属性动画没有效果。对应于属性动画的两个条件来说,本例中动画不生效的原因是只满足了条件1而未满足条件2。

针对上述问题,官方文档上告诉我们有3种解决方法:

  • 给你的对象加上get和set方法,如果你有权限的话;

  • 用一个类来包装原始对象,间接为其提供get和set方法;

  • 采用ValueAnimator,监听动画过程,自己实现属性的改变。

针对上面提供的三种解决方法,下面给出具体介绍。

1. 给你的对象加上get和set方法,如果你有权限的话

这个的意思很好理解,如果你有权限的话,加上get和set就搞定了,但是很多时候我们没权限去这么做,比如本例中无法给Button加一个setWidth方法,这是Android SDK内部实现的。

2. 用一个类来包装原始对象,间接为其提供get和set方法

下面看一个具体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void performAnimate(){
ViewWrapper wrapper = new ViewWrapper(mButton);
ObjectAnimator.ofInt(wrapper,"width",500).setDuration(5000).start();
}

@Override
public void onClick(View v){
if(v == mButton){
performAnimate();
}
}

private static class ViewWrapper{
private View mTarget;

public ViewWrapper(View target){
mTarget = target;
}

public int getWidth(){
return mTarget.getLayoutParams().width;
}

public void setWidth(int width){
mTarget.getLayoutParams().width = width;
mTarget.requestLayout();
}
}

上述代码在5s内让Button的宽度增加到了500px,为了达到这个效果,我们提供了ViewWrapper类专门用于包装View,具体到本例是包装Button。然后我们对ViewWrapper的width属性做动画,并且在setWidth方法中修改其内部的target的宽度,而target实际上就是我们包装的Button。这样一个间接属性动画就完成了,上述代码同样适用于一个对象的其它属性。

3. 采用ValueAnimator,监听动画过程,自己实现属性的改变

首先说说什么是ValueAnimator,ValueAnimator本身不作用于任何对象,也就是说直接使用它没有任何动画效果。它可以对一个值做动画,然后我们可以监听其动画过程,在动画过程中修改我们的对象的属性值,这样也就相当于我们的对象做了动画。如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private void performAnimate(final View target,final int start, final int end){
ValueAnimator valueAnimator = ValueAnimator.ofInt(1,100);
valueAnimator.addUpdateListener(new AnimatorUpdateListener(){
//持有一个IntEvaluator对象,方便下面估值的时候使用
private IntEvaluator mEvaluator = new IntEvaluator();

@Override
public void onAnimationUpdate(ValueAnimator animator){
//获取当前动画的进度值,整型,1~100之间
int currentValue = (Integer) animator.getAnimateValue();
Log.d(TAG,"current value :" + currentValue);

//获取当前进度占整个动画过程的比例,浮点型,0~1之间
float fraction = animator.getAnimatedFraction();
//直接调用整型估值器,通过比例计算出宽度,然后再设给Button
target.getLayoutParams().width = mEvaluator.evaluate(fraction,start,end);
target.requestLayout();
}
});

valueAnimator.setDuration(5000).start();
}

@Override
public void onClick(View v){
if(v == mButton){
performAnimate(mButton,mButton.getWidth(),500);
}
}

上述代码的效果和采用ViewWrapper是一样的,关于ValueAnimator,它会在5000ms内将一根数从1变成100,然后动画的每一帧会回调onAnimationUpdate方法。在这个方法里,我们可以获取当前的值(1~100)和当前值所占用的比例,我们可以计算出Button现在的宽度应该是多少。比如时间过了一半,当前值是50,比例为0.5,假设Button的起始宽度是100px,最终宽度是500px,那么Button增加的宽度也应该占总增加宽度的一半,总增加宽度是500-100=400,所以这个时候Button应该增加的宽度是400x0.5=200,那么当前Button的宽度应该为初始宽度+增加宽度(100+200=300)。

3.5 使用动画的注意事项

1. OOM问题

这个问题主要出现在帧动画中,当图片数量较多且图片较大时就极易出现OOM,这个在实际开发中尤其注意,尽量避免使用帧动画。

2. 内存泄漏

在属性动画中有一类无限循环的动画,比如:animator.setRepeatCount(ValueAnimator.INFINITE);这类动画需要在Activity退出时及时停止,否则将导致Activity无法释放从而造成内存泄漏,View动画并不存在此问题,因为在view的onDetachedFromWindow方法会取消掉动画,所以不会导致内存泄露。

3. 兼容性问题

动画在3.0以下的系统上有兼容性问题,在某些特殊场景可能无法正常工作,因此要做好适配工作。

4. View动画的问题

View动画是对View的影像做动画,并不是真正的改变View的状态,因此有时候回出现动画完成后Viwe无法隐藏的现象,即setVisibility(View.GONE)失效了,这个时候只要调用view.clearAnimation()清除View动画即可解决此问题。

5. 不要使用px

在进行动画的过程中,要尽量使用dp,使用px会导致在不同的设备上有不同效果。

6. 动画元素的交互

将view移动后,在Android 3.0以前的系统上,不管View动画还是属性动画,新位置均无法触发单击事件,同时,老位置仍然可以触发单击事件,尽管View已经在视觉上不存在了,将View移回原位置以后,原位置的单击事件继续生效。从3.0开始,属性动画的单击事件触发位置为移动后的位置,但是View动画仍然在原位置。

7. 硬件加速

使用动画的过程中,建议开启硬件加速,这样会提高动画的流畅性,但是这种方式不适用于基于自定义属性绘制的动画。

-------------本文结束谢谢阅读-------------