在手机上实现酷炫的动画效果,是灰常让人赏心悦目的事情。Android系统为我们提供了三种动画:补间动画(Tweened Animation)、帧动画(Frame-By-Frame Animation)、属性动画(Property Animation)。其中,属性动画是在Android3.0才推出的,也是最强大的,所谓“来得早不如来得好”。那么属性动画好在哪里,下面我们就来一探究竟。
属性动画的优点
在属性动画出来之前,补间动画已经能满足我们大多数的需求了,比如平移、旋转、缩放、透明度,但还是有一些硬伤:
首先就是补间动画并没有改变View的属性,所谓平移、旋转、缩放、透明度的变化,只是视觉上的欺骗而已,View的真实位置、大小、透明度、角度这些属性并没有改变,也就是说平移之后,View还在原来的位置,缩放之后,View还是原来的大小……如果是设置了点击事件的View比如Button,我们到Button移动到的新的位置去点击,并不会触发点击事件,而点击Button原来的位置时,仍会触发。而属性动画就是真正的依靠改变View的属性,来不断的做出动画的效果。
其次,补间动画只能对View做动画,有人说,动画不就是对View做才有效果吗?事实上,我们能看到的动画,都是View的动画,但是如果我们能对对象做动画,那么就能实现一些不一样的事。而属性动画,就实现了对对象的动画(属性改变)——也就是属性动画的第一种:ObjectAnimator(对象动画)。当我们想根据一个Point的x、y坐标不断的改变一个View的属性,去做属性动画,就可以用ObjectAnimator。这在只有补间动画的时代,是无法做到的。
最后就是补间动画只能做出平移、旋转、缩放、透明度的改变。有人会说,除了这几种,难道还有其他动画吗?当然,比如我们如果想改变一个View的背景色,让其在某两个色值之间渐变,用补间动画就无法实现,这里就需要用到属性动画的第二种了:ValueAnimator(值动画)。
属性动画的使用
这么好,怎么用?
先说说ObjectAnimator
也是最常用的动画,可以实现补间动画能实现的平移、旋转、缩放、透明度,也可以通过改变View的其他属性来实现一些动画,前提是要改变的属性有set和get方法,这个稍后讲,先看一个最简单的使用:
ObjectAnimator animator = ObjectAnimator.ofFloat(textview, "rotation", 0f, 360f);
animator.setDuration(5000);
animator.start();
ObjectAnimator实现动画的核心方法,就是ofFloat,第一个参数,是要做动画的对象;第二个参数,是要改变这个对象的哪个属性,参数类型是String,要实现位移动画就传"translationX"、"translationX",要实现透明度的动画就传"alpha",缩放就传"scaleY"、"scaleY",要改变宽度就传"width",前提是,你要改变的对象有width属性,并且有set和get方法;第三个,第四个是可变长参数,指定要改变属性的初始值、中间值、最终值,中间值可以不指定,也可以指定多个。
这里要说下第二个属性,要实现哪种动画就传对应的属性,而且我们可以传任意字符串,对,没搞错,就是任意。当我们传一个字符串,而对象没有这个属性怎么办呢?这个问题,有两个方面,第一,对象有这个属性,却不是我们想改变的。这时我们可以自定义一个包装类,包裹一下我们的View,在里面设置这个属性,并给出set和get方法,然后再在set方法中,对我们的View相应的属性,进行改变。
比如,我们要改变一个Button的宽度,但是直接传width,改变的是Button上面的文字的宽度,这时我们就可以自己定义一个ViewWrapper包装一下:
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();
}
}
第二种,对象压根没有这个属性,或者这个属性就是我们自己瞎写的。那也没什么,无非就是ObjectAnimator变成ValueAnimator,事实上ObjectAnimator本身就是集成自ValueAnimator的,后面再讲Value Animator的具体用法。
然后再讲讲多个动画一起执行的处理,属性动画有一个集合类,AnimatorSet,可以把想要一起执行的动画全部add进去,然后调用animSet.playTogether(anim1,anim2)方法即可。还可以用ObjectAnimator本身的play(anim1)、with(anim2)、after(anim3)、before(anim4),这几个方法来实现。
下面说ValueAnimator
和ObjectAnimator的实现方法很类似,简单看下Value Animator实现垂直平移的代码:
public void verticalRun(View view) {
ValueAnimator animator = ValueAnimator.ofFloat(0, 400);
animator.setTarget(mBtn);
animator.setDuration(400).start();
}
咋一看是不是跟ObjectAnimator完全一样,哪里有不同?仔细看下会发现,这里只设置了View、初始值、最终值、持续时间,但是并没有设置要改变的属性,比如"translationY",那么毫无疑问,这里是不会有任何动画的视觉效果发生的,怎么才能真正做出动画呢?就需要设置一个监听了。有人会说,好麻烦,还要设置监听,是麻烦了点,但是比起随之而来的灵活性,这算不了什么。比如我们就不再需要做动画的对象有get和set方法,比如我们可以改变对象的背景色,或者在监听的回调中,根据值动画的值的改变,同时改变对象的好几个属性。
实例:我们用值动画让一个小球做自由落体和抛物线运动。
布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/id_container"
>
<ImageView
android:id="@+id/id_ball"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/bol_blue" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="horizontal" >
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="verticalRun"
android:text="垂直" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="paowuxian"
android:text="抛物线" />
</LinearLayout>
</RelativeLayout>
左上角一个小球,底部分别是两个Button,一个让小球垂直运动,一个让小球做抛物线运动。
先看垂直:
/**
* 垂直自由落体
* @param view
*/
public void verticalRun( View view) {
ValueAnimator animator = ValueAnimator.ofFloat(0, mScreenHeight
- mBlueBall.getHeight());
animator.setTarget(mBlueBall);
animator.setDuration(1000).start();
// animator.setInterpolator(value)
animator.addUpdateListener(new AnimatorUpdateListener()
{
@Override
public void onAnimationUpdate(ValueAnimator animation)
{
mBlueBall.setTranslationY((Float) animation.getAnimatedValue());
}
});
}
虽然我们自己写了监听,在回调中手动改变位置,但是提高了灵活性。
下面看抛物线运动,水平方向速度100px/s,垂直方向加速度200px/s*s。好像只和时间有关系,但是随着时间变化,水平和垂直方向的速度是不一样的,这就要用到TypeEvaluator了,因为我们在时间变化的时候,需要同时返回两个值,一个x,一个y。
/**
* 抛物线
* @param view
*/
public void paowuxian(View view) {
ValueAnimator valueAnimator = new ValueAnimator();
valueAnimator.setDuration(3000);
valueAnimator.setObjectValues(new PointF(0, 0));
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setEvaluator(new TypeEvaluator<PointF>()
{
// fraction = t / duration ,单位时间,时间片段
@Override
public PointF evaluate(float fraction, PointF startValue,
PointF endValue)
{
Log.e(TAG, fraction + "");
// x方向速度100 ,则y方向v = at = 200 * t
PointF point = new PointF();
point.x = 100 * fraction ; // s = vt
point.y = 0.5f * 200 * fraction * fraction ; // s = 1/2 * a*a * t
return point;
}
});
valueAnimator.start();
valueAnimator.addUpdateListener(new AnimatorUpdateListener()
{
@Override
public void onAnimationUpdate(ValueAnimator animation)
{
PointF point = (PointF) animation.getAnimatedValue();
mBlueBall.setX(point.x);
mBlueBall.setY(point.y);
}
});
}
上面提到,ObjectAnimator指定的属性字符串不存在时,就变成了ValueAnimator,这时我们就也需要为ObjectAnimator添加监听器了。