黄金龙的博客

属性动画的使用原理

属性动画是API 11新加入的特性,和View动画不同,它对作用对象进行了扩展,属性动画可以对任何对象做动画,甚至还可以没有对象。除了作用对象进行了扩展以外,属性动画的效果也得到了加强,属性动画中有ValueAnimator(对值进行动画)、ObjectAnimator(对对象进行动画)、AnimatorSet(动画集合)等概念,通过他们可以实现绚丽的动画效果。

使用属性动画

属性动画可以对任意的对象属性进行动画而不仅仅是View,动画默认的时间为300ms,默认帧率为10ms/帧。其可以达到的效果是在一个时间间隔内完成对象从一个属性值到另一个属性值到改变。因此属性动画几乎无所不能。因为现在android版本都在4.0以上,所以我们不用考虑API 11以下手机。

  • 例如我们想改变一个对象的translationY的属性,让其沿着Y轴向上平移一段距离:它的高度。该动画在默认时间内完成,动画的完成时间是可以定义的。想要更灵活的效果我们还可以定义插值器和估值算法,但是一般我们不需要自定义,系统已经预置了一些,能够满足常用动画
1
ObjectAnimator.ofFloat(myObject, "translationY", -myObject.getHeight()).start();
  • 改变一个对象的背景色属性,典型的情形是改变View的背景色,下面的动画可以让View在3s内实现从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();
  • 动画集合,5s内对View的旋转、平移、缩放和透明度都进行了改变
1
2
3
4
5
6
7
8
9
10
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, "scaleX", 1, 0.5f),
ObjectAnimator.ofFloat(myView, "alpha", 1, 0.25, 1),);
set.setDuration(5 * 1000).start();

属性动画在XML定义就不在多讲,因为它不如直接在代码中写起来方便,当然如果多个对象使用同一个动画,写在XML里面更好。

插值器和估值器

插值器

插值器只是一个概念,系统中与之相关的类叫做 TimeInterpolator ,其只是一个接口,准确来说叫做“时间插值器”。该接口的注释为:

1
A time interpolator defines the rate of change of an animation. This allows animations to have non-linear motion, such as acceleration and deceleration.

意思是:该时间插值器定义了动画的变化率,允许动画做非线性的运动,比如加速、减速。

这样,插值器的主要作用我们就明白了。接下来,看看这个接口的代码,该接口只有一个接口方法:

1
2
3
4
5
6
7
8
9
/**
* @param input A value between 0 and 1.0 indicating our current point
* in the animation where 0 represents the start and 1.0 represents
* the end
* @return The interpolation value. This value can be more than 1.0 for
* interpolators which overshoot their targets, or less than 0 for
* interpolators that undershoot their targets.
*/
float getInterpolation(float input);

不管是系统内置的插值器,还是我们自定义插值器,只需要实现接口并重写该方法,就可以起到插值器的作用。

该方法的作用是什么呢?上图保留了源码中对方法的注释,我就不直译了,说下大概的意思:

方法参数 input 接收 0 和 1.0 之间的值表示动画的当前进度,是线性变化的,其中0表示开始,1.0表示结束;
返回值表示对 input 进行插值之后的值,我们就是在这儿做“手脚”,让返回值不再是线性的,就完成自己定义动画的变化率了。
TimeInterpolator中文翻译为时间插值器,它的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有LinearInterpolator(线性插值器:匀速动画)、AccelerateDecelerateInterpolator(加速减速插值器:动画两头慢中间快)和DecelerateInterpolator(减速插值器:动画越来越慢)等。

估值器

TypeEvalutor等中文翻译为类型估值算法,也叫作估值器。估值器的是用来决定属性的计算方式,最终使用反射机制来改变属性变化。它的作用是根据当前属性改变的百分比来计算改变后的属性值。系统预置的有IntEvalutor(针对整形属性)、FloatEvalutor(针对浮点型属性)和ArgbEvalutor(针对color属性)。

属性动画的监听

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

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
/**	
* <p>An animation listener receives notifications from an animation.
* Notifications indicate animation related events, such as the end or the
* repetition of the animation.</p>
*/
public static interface AnimatorListener {
/**
* <p>Notifies the start of the animation.</p>
*
* @param animation The started animation.
*/
void onAnimationStart(Animator animation);

/**
* <p>Notifies the end of the animation. This callback is not invoked
* for animations with repeat count set to INFINITE.</p>
*
* @param animation The animation which reached its end.
*/
void onAnimationEnd(Animator animation);

/**
* <p>Notifies the cancellation of the animation. This callback is not invoked
* for animations with repeat count set to INFINITE.</p>
*
* @param animation The animation which was canceled.
*/
void onAnimationCancel(Animator animation);

/**
* <p>Notifies the repetition of the animation.</p>
*
* @param animation The animation which was repeated.
*/
void onAnimationRepeat(Animator animation);
}

AnimatorUpdateListener的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Implementors of this interface can add themselves as update listeners
* to an <code>ValueAnimator</code> instance to receive callbacks on every animation
* frame, after the current frame's values have been calculated for that
* <code>ValueAnimator</code>.
*/
public static interface AnimatorUpdateListener {
/**
* <p>Notifies the occurrence of another frame of the animation.</p>
*
* @param animation The animation which was repeated.
*/
void onAnimationUpdate(ValueAnimator animation);

}

对任意属性做动画

比如我们用属性动画对Button做宽度的增加,会首先想到

1
ObjectAnimator.ofInt(mButton, "width", 500).setDuration(5000).start();

上述代码运行后会发现没有效果,其实没有效果是正常现象,因为如果随便传一个属性(例如width)轻则没有动画效果,重则程序直接crash。
下面分析属性动画的原理:属性动画要求动画的作用对象提供该属性的get和set方法,属性动画根据该属性的初始值和最终值,以动画的效果多次去调用set方法,每次传递给set方法等值都不一样,确切的来说是随着时间的推移,所传递的值越来越接近最终值

  1. 作用对象必须提供该属性的get和set方法
  2. 作用对象的set方法必须要能使UI效果改变,否则动画无效果

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

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
/**
* Makes the TextView exactly this many pixels wide.
* You could do the same thing by specifying this number in the
* LayoutParams.
*
* @see #setMaxWidth(int)
* @see #setMinWidth(int)
* @see #getMinWidth()
* @see #getMaxWidth()
*
* @attr ref android.R.styleable#TextView_width
*/
@android.view.RemotableViewMethod
public void setWidth(int pixels) {
mMaxWidth = mMinWidth = pixels;
mMaxWidthMode = mMinWidthMode = PIXELS;

requestLayout();
invalidate();
}

/**
* Return the width of the your view.
*
* @return The width of your view, in pixels.
*/
@ViewDebug.ExportedProperty(category = "layout")
public final int getWidth() {
return mRight - mLeft;
}

从上述源码中可以看出,getWidth的确是获取View的宽度,而setWidth是TextView和其子类的专属方法,它的作用不是设置宽度,而是设置最大宽度和最小宽度的。具体来说在布局XML中android:layout_width对应宽度,android:width对应最大最小宽度。所以setWidth无法改变控件的宽度。
针对上述问题,官方告诉我们有三种解决方法

  • 给你的对象加上set和get方法,如果你有权限的话
  • 用一个类来包装原始对象,间接为其提供set和get方法
  • 采用ValueAnimator,监听动画的过程,自己实现属性的改变。

针对以上解决方法,结合实际情况我们得出,Button设置宽度无法使用第一种方法,因为我们没有权限更改它的源码。所以我们可以采用第二种和第三种解决方法。

采用第二种解决方法,创建包裹类:

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
/**
* 改变宽度
*/
private void changeWidth(){
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();
}
}

采用第三种解决方法,利用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
/**
*
* @param target 要改变的目标View
* @param start 开始的宽度
* @param end 结束的宽度
*/
private void performAnimte(final View target, final int start, final int end){
ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
private IntEvaluator mEvaluator = new IntEvaluator();
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 获取当前动画的进度
int currentValue = (int) animation.getAnimatedFraction();
// 获取当前进度占整个动画的比例
float fraction = animation.getAnimatedFraction();
// 直接调用整型估值器,通过比例计算出宽度,然后赋值
target.getLayoutParams().width = mEvaluator.evaluate(fraction, start, end);
target.requestLayout();

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

属性动画的工作原理

属性动画要求动画作用的对象提供该属性的set方法,属性动画根据你传递的该属性的初始值和最终值,以动画的效果多次去调用set方法。每次传递给set方法的值都不一样,确切的来说是随着时间的推移,所传递的值越来越接近最终值。如果动画的时候没有传递初始值,那么还要提供get方法,因为系统要去获取属性的初始值。对于属性动画来说,其动画过程中所做的就是这么多。

使用动画的注意事项

  1. 内存泄漏:在属性动画中有一类无限循环的动画,这类动画需要在Activity退出时及时停止,否则将导致Activity无法释放从而造成内存泄漏,View动画并不存在这种现象。
  2. 兼容性问题:动画在3.0以下的系统上存在兼容性问题,现在android系统大部分都是4.0的
  3. 不要使用px:在进行动画的过程中,要尽量使用dp,使用px会导致在不同的设备上有不同的效果
  4. 硬件加速:使用动画的过程中建议开启硬件加速,这样会提高动画的流畅性。