属性动画是从Android 3.0 开始引入的新特性,和View动画不同,它可以对任何对象做动画,甚至可以没有对象(ValueAnimator)除此之外,属性动画通过ValueAnimator、ObjectAnimator和AnimatorSet 等概念,可以实现比View动画要丰富得多的动画效果。
在Android 3.0之前的版本可以采用开源动画库nineoldandroids来使用属性动画来兼容以前的版本,虽然本质上还是View动画。
PS:属性动画的默认时间间隔为300ms,默认的帧率为10ms/帧
ValueAnimator是整个属性动画机制当中最核心的一个类,因为属性动画的运行机制是通过不断地对值进行操作来实现的,而初始值和结束值之间的动画过渡就是由ValueAnimator这个类来负责计算的。它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。除此之外,ValueAnimator还负责管理动画的播放次数、播放模式、以及对动画设置监听器等,确实是一个非常重要的类。
但是它的用法并不复杂,可是这里我先不给出它的具体用法示例,因为单一的 ValueAnimator 动画仅仅是数值的动画,并没有视觉效果,通常在使用它的时候会和监听器一同使用,因此我会在之后的部分给出它的用法示例。
相比于ValueAnimator,ObjectAnimator可能才是我们最常接触到的类,因为ValueAnimator只不过是对值进行了一个平滑的动画过渡,但我们实际使用到这种功能的场景好像并不多。而ObjectAnimator则就不同了,它是可以直接对任意对象的任意属性进行动画操作的,比如说View的alpha属性。
不过虽说ObjectAnimator会更加常用一些,但是它其实是继承自ValueAnimator的,底层的动画实现机制也是基于ValueAnimator来完成的,因此ValueAnimator仍然是整个属性动画当中最核心的一个类。那么既然是继承关系,说明ValueAnimator中可以使用的方法在ObjectAnimator中也是可以正常使用的,它们的用法也非常类似,比如这里我设置了一个改变背景颜色的动画,如下:
/**
* 背景色变换动画
* */
private void backgroundColorAnim(Object target){
colorAnim = ObjectAnimator.ofInt((View)target,"backgroundColor",0xFFFF8080,0xFF8080FF);
colorAnim.setDuration(3000);
colorAnim.setEvaluator(new ArgbEvaluator());
//无限循环
colorAnim.setRepeatCount(ValueAnimator.INFINITE);
//反转变换
colorAnim.setRepeatMode(ValueAnimator.REVERSE);
colorAnim.start();
}
可以看到,我们调用了ofInt()方法来去创建一个ObjectAnimator的实例。其中第一个参数要求传入一个object对象,我们想要对哪个对象进行动画操作就传入什么。第二个参数是想要对该对象的哪个属性进行动画操作,由于我们这里想要改变背景颜色,因此这里传入"backgroundColor"。后面的参数就是不固定长度了,想要完成什么样的动画就传入什么值,这里传入的值就表示将指定控件从红色变为蓝色。之后调用setDuration()方法来设置动画的时长,之后调用setEvaluator()方法来设置估值器,然后设置成无限反转动画,最后调用start()方法启动动画。
然后我们再看一个简单点的平移动画(旋转,透明度,缩放都可以由此举一反三),如下:
/**
* 向左平移然后归位
* */
private void translateAnim(Object target){
View view = (View) target;
float curTranslationX = view.getTranslationX();
transAnim = ObjectAnimator.ofFloat(view,"translationX",curTranslationX,-20,curTranslationX);
transAnim.setDuration(3000);
transAnim.start();
}
也叫动画集合、组合动画、复合动画。具体的使用方法跟单一动画也没有太大区别,只是创建的是AnimatorSet对象,然后调用了play()方法,如下:
/**
* 复合动画
* */
private void compoundAnim(Object target){
View view = (View) target;
set = new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(view,"rotationX",0,360),
ObjectAnimator.ofFloat(view,"rotationY",0,180),
ObjectAnimator.ofFloat(view,"rotation",0,-90),
ObjectAnimator.ofFloat(view,"alpha",1,0.25f,1),
ObjectAnimator.ofFloat(view,"scaleX",1,0.5f),
ObjectAnimator.ofFloat(view,"scaleY",1,0.5f)
);
set.setDuration(5000).start();
}
可以看到,上述代码调用的是playTogether()方法,运行起来就是上面的六个动画效果复合起来同时播放。但如果我们不想让他们全部同时播放,比如旋转类型的动画全部组合在一起同时播放,之后剩下的动画再组合在一起同时播放呢?简单,AnimatorSet还有一系列方法用来解决这类问题:
所以,想要实现上述效果只用做下更改即可,如下:
set.play(ObjectAnimator.ofFloat(view,"rotationX",0,360))
.with(ObjectAnimator.ofFloat(view,"rotationY",0,180))
.with(ObjectAnimator.ofFloat(view,"rotation",0,-90))
.before(ObjectAnimator.ofFloat(view,"alpha",1,0.25f,1))
.before(ObjectAnimator.ofFloat(view,"scaleX",1,0.5f))
.before(ObjectAnimator.ofFloat(view,"scaleY",1,0.5f));
值得注意的是,虽然后三个动画是同时播放的,但是我们不能想当然的用with。因为这是链式结构,即使是后面的动画,调用方法的主体还是最开始的那个动画集合,因此要使用before。
属性动画除了通过代码来实现以外,还可以通过XML文件来定义。属性动画需要定义在res/animator/目录下,比如想要实现上述六个动画效果同时播放的复合动画,我可以这么定义XML文件:
然后代码控制加载XML文件中的动画即可,如下:
/**
* 通过xml文件实现相同的复合动画
* */
private void compoundAnimByXml(Object target){
set = (AnimatorSet) AnimatorInflater.loadAnimator(this,R.animator.compound_anim);
set.setTarget(target);
set.start();
}
属性动画提供了监听器用于监听动画的播放过程,主要有如下两个接口:AnimatorUpdateListener 和 AnimatorListener
AnimatorListener 的定义如下:
public static interface AnimatorListener {
void onAnimationStart(Animator animation);
void onAnimationEnd(Animator animation);
void onAnimationCancel(Animator animation);
void onAnimationRepeat(Animator animation);
}
可以看出,它可以监听动画的开始、结束、取消以及重复播放。同时为了方便开发,系统还提供了 AnimatorListenerAdapter 这个类,它是AnimatorListener的适配器类,这样我们就可以有选择地实现上面的四个方法了,毕竟不是所有方法我们都感兴趣。
AnimatorUpdateListener 的定义如下:
public static interface AnimatorUpdateListener {
void onAnimationUpdate(ValueAnimator animation);
}
可以看到,只有一个方法,它会监听整个动画过程,动画每播放一帧,onAnimationUpdate就会被调用一次(每10ms调用一次)
其实到这里,属性动画的用法已经介绍的差不多了,但是有关属性动画还有个非常重要的点还没提及。想要让属性动画生效,需要满足两个条件:
可能看上述条件还比较绕,接下来看一个属性动画失效的例子,改变按钮宽度。通过之前的介绍,使用属性动画改变某个控件的宽度属性,应该这么写:
ObjectAnimator.ofInt(mButton,"width",800).setDuration(5000).start();
然而实际运行起来是没有效果的。因为Button是继承TextView的,而TextView的 setWidth 方法设置的不是我们看到的那个宽度,而是最大宽度和最小宽度。因此没有满足上述条件第二条。所以属性动画没有生效。解决办法有两种:
1.用一个类来包装原始对象,间接为其提供get、set方法
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();
}
}
然后就能正常使用了
private void changeWidthAnim(Object target){
ViewWrapper wrapper = new ViewWrapper((View)target);
changeWidthAnim = ObjectAnimator.ofInt(wrapper,"width",800);
changeWidthAnim.setDuration(3000);
changeWidthAnim.start();
}
2.结合ValueAnimator 和 监听器,自己实现属性的改变
/**
* 变更按钮宽度动画(通过监听器来实现)
* */
private void changeWidthAnimByListener(final View target, final int start, final int end){
valueAnimator = ValueAnimator.ofInt(1,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();
//获取当前进度占整个动画过程的比例 0-1 浮点型
float fraction = animation.getAnimatedFraction();
//通过整型估值器计算出当前宽度并设给View
target.getLayoutParams().width = mEvaluator.evaluate(fraction,start,end);
target.requestLayout();
}
});
valueAnimator.setDuration(3000).start();
}
可以看到,这里我设置了一个ValueAnimator让它从1平滑过渡到100,然后在监听器中通过估值器算出随时间变化百分比的宽度变化量,再将结果宽度赋值给操控对象,最后刷新界面即可。
Demo:https://github.com/Ein3614/AnimationTest