Android动画(四)之属性动画
属性动画是API 11
新加入的特性,和View
动画不同,它对作用对象进行了扩展,属性动画可以对任何对象做动画,甚至还可以没有对象。除了作用对象进行了扩展外,属性动画的效果也得到了加强,不再像View
动画那样只能支持四种简单的变换。属性动画中有ValueAnimator
、ObjectAnimator
和Animator
等概念,通过它们可以实现绚丽的动画。
一、使用属性动画
属性动画的默认时间间隔为300ms
,默认帧率为10ms/帧
。其可以达到的效果是:在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。因此,属性动画几乎是无所不能的,只要对象有这个属性,它都能实现动画效果。但是属性动画是API 11
才有的,所以在API 11
之前想要使用属性动画的话可以使用nineoldandroids
来兼容,Nineoldandroids
内部是通过代理View
动画来实现的,它的本质是View
动画。
比较常用的几个动画类是:ValueAnimator
、ObjectAnimator
和AnimatorSet
,其中ObjectAnimator
继承自ValueAnimator
,AnimatorSet
是动画集合,可以定义一组动画,它们使用起来也是极其简单的。下面简单的举几个小例子:
(1)改变一个对象(myObject
)的translationY
属性,让其沿着Y轴向上平移一段距离:它的高度,该动画在默认时间内完成,动画的完成时间是可以定义的。想要更灵活的效果我们还可以定义插值器和估值算法,但是一般来说我们不需要自定义,系统已经预置了一些,能够满足常用的动画。
ObjectAnimator.ofFloat(myObject,"translationY",-myObject.getHeight()).start();
(2)改变一个对象的背景色属性,典型的情形是改变View
的背景色,下面的动画可以让背景色在3
秒内实现从0xFFFF8080
到0xFF8080FF
的渐变,动画会无限循环且会有反转的效果。
ValueAnimator colorAnim = ObjectAnimator.ofInt(this, "backgroundColor"
,0xFFFF8080,0xFF8080FF);
colorAnim.setDuration(3000);
colorAnim.setEvaluator(new ArgbEvaluator());
colorAnim.setRepeatCount(ValueAnimator.INFINITE);
colorAnim.setRepeatMode(ValueAnimator.RESTART);
colorAnim.start();
(3)动画集合,5
秒内对View
的旋转、平移、缩放和透明度都进行了改变。
AnimatorSet set = new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(myView,"rotationX",0,360),
ObjectAnimator.ofFloat(myView,"rotationY",0,180),
ObjectAnimator.ofFloat(myView,"rotation",0,-90),
ObjectAnimator.ofFloat(myView,"translationX",0,90),
ObjectAnimator.ofFloat(myView,"translationY",0,90),
ObjectAnimator.ofFloat(myView,"scaleX",1,1.5f),
ObjectAnimator.ofFloat(myView,"scaleY",1,0.5f),
ObjectAnimator.ofFloat(myView,"alpha",1,0.25f,1)
);
set.setDuration(5 * 1000).start();
属性动画除了通过代码实现以外,还可以通过XML
来定义。属性动画需要定义在res/animator/
目录下,它的语法如下所示:
<?xml version="1.0" encoding="utf-8"?>
<set
android:ordering="together"
xmlns:android="http://schemas.android.com/apk/res/android">
<objectAnimator
android:propertyName="string"
android:duration="int"
android:valueFrom="float | int | color"
android:valueTo="float | int | color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode=-["reverse"|"restart"]
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=-["reverse"|"restart"]
android:valueType=["intType"|"floatType"]/>
</set>
属性动画的各种参数都比较好理解,在XML
中可以定义ValueAnimator
、ObjectAnimator
以及AnimatorSet
,其中<set>
标签对应AnimatorSet
,<animator>
标签对应ValueAnimator
,而<objectAnimator>
标签则对应ObjectAnimator
。<set>
标签的android:ordering
属性有两个可选值:"together"
和"sequentially"
,其中"together"
表示动画集合中的子动画同时播放,"sequentially"
则表示动画集合中的子动画按照前后顺序依次播放,android:ordering
的默认值是"together"
。
对于<objectAnimator>
标签各个属性的含义,下面简单说明一下,对于<animator>
标签就不再介绍了,因为它只是比<objectAnimator>
少了一个android:propertyName
属性而已,其他都是一样的。
属性名 | 作用 |
---|---|
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
上,如下所示:
<!-- res/animator/property_animator.xml -->
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together">
<objectAnimator
android:duration="300"
android:propertyName="x"
android:valueTo="200"
android:valueType="intType" />
<objectAnimator
android:duration="300"
android:propertyName="y"
android:valueTo="300"
android:valueType="intType" />
</set>
使用上面的属性动画:
AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(mContext,R.animator.property_animator);
set.setTarget(mButton);
set.start();
在实际开发中建议采用代码来实现属性动画,这是因为通过代码来实现比较简单。更重要的是,很多时候一个属性的起始值是无法提前确定的,比如让一个Button
从屏幕左边移动到屏幕右边,由于我们无法提前知道屏幕的宽度,因此无法将属性动画定义在XML
中,在这种情况下就必须通过代码来动态地创建属性动画。
二、属性动画的监听器
属性动画提供了监听器用于监听动画的播放过程,主要有如下两个接口:AnimatorUpdateListener
和AnimatorListener
。
AnimatorListener
的定义如下:
public static interface AnimationListener {
void onAnimationStart(Animation animation);
void onAnimationEnd(Animation animation);
void onAnimationCancel(Animator animation);
void onAnimationRepeat(Animation animation);
}
从AnimatorListener
的定义可以看出,它可以监听动画的开始。结束、取消以及重复播放。同时为了方便开发,系统还提供了AnimatorListenerAdapter
这个类,它是AnimatorListener
的适配器类,这样我们就可以有选择地实现上面4个方法了,毕竟不是所有方法都是我们感兴趣的。
下面再来看一下AnimatorUpdateListener
的定义,如下所示:
public static interface AnimatorUpdateListener {
void onAnimationUpdate(ValueAnimator animation);
}
AnimatorUpdateListener
比较特殊,它会监听整个动画过程,动画是由许多帧组成的,每播放一帧,onAnimationUpdate
就会被调用一次,利用这个特性,我们可以做一些特殊的事情。
三、对任意属性做动画
现有有一个Button
,我想让这个Button
的宽度从当前宽度增加到500px
。首先View
动画是不行的,因为它只能进行四种类型的动画(平移,旋转,缩放,透明度),所以只能使用属性动画了。
在使用前,先来讲一下属性动画的原理:属性动画要求动画作用的对象提供该属性的get
和set
方法,属性动画根据外界传递的该属性的初始值和最终值,以动画的效果多次去调用set
方法,每次传递给set
方法的值不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。总结一下,我们队object
的属性abc
做动画,如果想让动画生效,要同时满足两个条件:
-
object
必须提供setAbc
方法,如果动画的时候没有传递初始值,那么还要提供getAbc
方法,因为系统要去取abc
属性的初始值(如果这条不满足,程序直接Crash
)。 -
object
的setAbc
对属性abc
所做的改变必须能够通过某种方法反映出来,比如会带来UI
的改变之类的(如果这条不满足,动画无效果但不会Crash
)。
以上条件缺一不可。由于Button
内部虽然提供了getWidth
和setWidth
方法,但是setWidth
并不是改变视图的大小,Button
继承了TextView
,TextView
中的setWidth
方法如下:
/**
* Sets the width of the TextView to be exactly {@code pixels} wide.
* <p>
* This value is used for width calculation if LayoutParams does not force TextView to have an
* exact width. Setting this value overrides previous minimum/maximum width configurations
* such as {@link #setMinWidth(int)} or {@link #setMaxWidth(int)}.
*
* @param pixels the exact width of the TextView in terms of pixels
*
* @see #setEms(int)
*
* @attr ref android.R.styleable#TextView_width
*/
@android.view.RemotableViewMethod
public void setWidth(int pixels) {
mMaxWidth = mMinWidth = pixels;
mMaxWidthMode = mMinWidthMode = PIXELS;
requestLayout();
invalidate();
}
此方法是在设置最大宽度和最小宽度,而不是我们所预期的设置Button
的实际宽度。针对这种情况,官方文档给了我们3种解决方法:
- 给你的对象加上
get
和set
方法,如果你有权限的话; - 用一个类来包装原始对象,间接为其提供
get
和set
方法; - 采用
ValueAnimator
,监听动画过程,自己实现属性的改变。
针对上面提出的三种解决方法,下面给出具体的介绍。
1.给你的对象加上get
和set
方法,如果你有权限的话
这个的意思很好理解,如果你有权限的话,加上get
和set
就搞定了。但是很多时候我们没有权限去这么做。比如本文开头所提到的问题,你无法给Button
加上一个合乎要求的setWidth
方法,因为这是Android SDK
内部实现的。这个方法最简单,但是往往是不可行的,这里就不对其进行更多的分析了。
2.用一个类来包装原始对象,间接为其提供get
和set
方法
这是一个很有用的解决方法,用起来很方便,也很好理解:
@Override
public void onClick(View v) {
if (v == mButton) {
performAnimate();
}
}
private void performAnimate(){
ViewWrapper wrapper = new ViewWrapper(mButton);
ObjectAnimator.ofInt(wrapper,"width",500).setDuration(5000).start();
}
private static class ViewWrapper {
private View mTarget;
public ViewWrapper(View target) {
this.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
本身不作用于任何对象,也就是说直接使用它没有任何动画效果。它可以对一个值做动画,然后我们可以监听其动画过程,在动画过程中修改我们的对象的属性值,这样也就相当于我们的对象做了动画。下面是一个例子:
private void performAnimate(final View target,final int start,final int end) {
ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 100);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
//持有一个IntEvaluator对象,方便下次估值的时候使用
private IntEvaluator mEvaluator = new IntEvaluator();
@Override
public void onAnimationUpdate(ValueAnimator animation) {
//获取当前动画的进度值,整型,1-100之间
int currentValue = (int) animation.getAnimatedValue();
Log.d(TAG, "current value = "+currentValue);
//获取当前进度占整个动画过程的比例,浮点型,0-1之间
float fraction = animation.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);
}
}
上面的例子中,我们在5000ms
内将一个数从1
变到100
,然后动画的每一帧会回调onAnimationUpdate
方法。在这个方法中,我们获取当前的动画进度,然后去设置Button
的宽度。
四、与View动画的区别
View
动画(animation
)的原理是通过不断的重绘View
(draw
方法)来改变View
的内容的位置,而属性动画(animator
)则是通过get
和set
值去实际的改变View
的位置。