本篇文章主要介绍属性动画,需要了解补间动画和帧动画相关知识的,建议阅读Android动画详解(上)。属性动画非常强大,运用也非常灵活,为了便于理解,本文首先从类的角度介绍了属性动画的继承关系,然后针对一些重点类介绍了其内的主要方法,最后通过demo的方式对属性的动画的常见用法进行了演示。
属性动画存放在android.animation包下,主要的类继承关系如下:
Animator是一个抽象类,其内提供了一些公共方法。需要注意的是,一些方法是需要子类去重写的,比如getInterpolator()这个方法目前直接返回null,子类需要根据实际情况返回Interpolator。
ValueAnimator是属性动画中非常常用的一个类,除了实现或重写了父类的上述方法外,其还增加了一些新的功能,如常用的估值器的设置。
AnimatorSet主要用来控制动画组合的播放顺序及方式,特别是其的Builder,功能强大,使用方便。
属性动画的用法非常灵活,首先从简单的平移动画开始入手,效果如如下:
代码比较简单,老规矩,介绍xml和Java两种实现方式:
xml实现方式:
Java中调用代码:
ObjectAnimator objectAnimator= (ObjectAnimator)AnimatorInflater.loadAnimator(this,R.animator.propertyanimation);
objectAnimator.setTarget(mAnimationBtn0);
objectAnimator.start();
需要注意的是xml代码是放在res的animator目录下,另外注意repeatCount的值和repeatMode选项。
Java实现方式:
ObjectAnimator translationXAnimation = ObjectAnimator.ofFloat(mAnimationBtn0, "translationX", 0, 500);
translationXAnimation.setDuration(2000);
translationXAnimation.setRepeatCount(1);
translationXAnimation.setRepeatMode(ValueAnimator.REVERSE);
translationXAnimation.start();
上述Java代码之所以能够实现平移效果是因为mAnimationBtn0这个Target属于View包含translationX对应的get/set方法。至于为啥是ofFloat而不是ofInt,是因为其get/set方法对应的方法参数为float类型,它们是相互关联的。
旋转、缩放等动画与平移动画类似,就不再介绍了。
假如我们需要改变一个View的宽度,但是View里面却没有对应的get/set该如何实现呢?方案很多,为了对比学习,挑选了三种典型的方案,效果图如下:
从图中可以看到红、蓝、绿三个View的变化效果一致,但是正如其显示的内容一样,使用了不同的方式,分别为ObjectAnimator.ofInt、ValueAnimator.ofInt 、ValueAnimator.ofObject。
该方式是处理这种没有对应set/get方法却想使用属性动画情况的常用的方式,主要思路是利用装饰者模式,对现有target进行装饰,在装饰类里提供set/get操作,完成相关属性的设置。实现步骤如下:
步骤一:创建装饰类
public class WidthWrapper {
private View mTargetView;
WidthWrapper(View view) {
mTargetView = view;
}
public int getWidth(){
return mTargetView.getLayoutParams().width;
}
public void setWidth(int width) {
mTargetView.getLayoutParams().width = width;
mTargetView.requestLayout();
}
}
步骤二:配置属性动画
ObjectAnimator widthAnimation = ObjectAnimator.ofInt(new WidthWrapper(mAnimationBtn1), "Width", 0, 600);
widthAnimation.setDuration(2000);
widthAnimation.start();
该方式是属性动画运用的通用方式,重要性较高,对一些效果复杂或多个动画关联调用的情况处理比较有优势,其核心是动画回调的运用。核心代码如下:
if (null != widthAnimation2 && widthAnimation2.isRunning()) {
return;
}
widthAnimation2 = ValueAnimator.ofInt(0, 600);
widthAnimation2.setDuration(2000);
widthAnimation2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnimationBtn2.getLayoutParams().width = (int) animation.getAnimatedValue();
mAnimationBtn2.requestLayout();
}
});
widthAnimation2.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mAnimationBtn2.getLayoutParams().width = 0;
mAnimationBtn2.requestLayout();
}
@Override
public void onAnimationEnd(Animator animation) {
mAnimationBtn2.getLayoutParams().width = 600;
mAnimationBtn2.requestLayout();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
widthAnimation2.start();
首先,如果动画存在且正在执行则返回。如果动画不存在则创建动画,并设置监听,在监听中动态改变布局参数并刷新布局。最后启动动画。
该方式主要是为了演示TypeEvaluator的运用,针对没有get/set这种场景并不常见,主要运用于一些类的特殊变化过程,实现过程有点像第一种方式和第二种方式的结合。实现步骤如下:
步骤一:自定义WidthObject
public class WidthObject {
private int mWidth;
WidthObject(int width) {
mWidth = width;
}
public int getWidth() {
return mWidth;
}
}
步骤二:自定义TypeEvaluator
public class WidthTypeEvaluator implements TypeEvaluator {
@Override
public WidthObject evaluate(float fraction, WidthObject startValue, WidthObject endValue) {
return new WidthObject((int)(startValue.getWidth()+fraction*(endValue.getWidth()-startValue.getWidth())));
}
}
自定义TypeEvaluator需要实现TypeEvaluator接口,根据传入参数返回对应值。计算方式为
startValue+fraction*(endValue-startValue)。
步骤三:配置属性动画
if (null != widthAnimation3 && widthAnimation3.isRunning()) {
return;
}
widthAnimation3 = ValueAnimator.ofObject(new WidthTypeEvaluator(),new WidthObject(0), new WidthObject(600));
widthAnimation3.setDuration(2000);
widthAnimation3.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mAnimationBtn3.getLayoutParams().width = ((WidthObject)animation.getAnimatedValue()).getWidth();
mAnimationBtn3.requestLayout();
}
});
widthAnimation3.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mAnimationBtn3.getLayoutParams().width = 0;
mAnimationBtn3.requestLayout();
}
@Override
public void onAnimationEnd(Animator animation) {
mAnimationBtn3.getLayoutParams().width = 600;
mAnimationBtn3.requestLayout();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
widthAnimation3.start();
上述代码跟ValueAnimator.ofInt 方式大同小异,都是获取动态布局参数并请求属性布局。
AnimatorSet用于控制动画的播放,跟AnimationSet作用类似,但还是有一定的区别的,相对更加强大,可以控制多个动画的播放顺序。下面的代码设置了四个不同效果的属性动画,动画1为平移动画,动画2为透明度变化动画,动画3为缩放动画,动画4为旋转动画。
ObjectAnimator animation1 = ObjectAnimator.ofFloat(mAnimationBtn4, "translationX", 0, 500);
ObjectAnimator animation2 = ObjectAnimator.ofFloat(mAnimationBtn4, "alpha", 0, 1);
ObjectAnimator animation3 = ObjectAnimator.ofFloat(mAnimationBtn4, "scaleX", 2);
ObjectAnimator animation4 = ObjectAnimator.ofFloat(mAnimationBtn4,"rotationX",0,270,50);
AnimatorSet animatorSet=new AnimatorSet ();
animatorSet.play(animation1).with(animation2).after(animation3).before(animation4);
animatorSet.setDuration(2000);
animatorSet.start();
AnimatorSet 对其进行了组合调用,将动画1和动画2一起播放,并且在动画3之后,在动画4之前。所以其播放顺序为:动画3>(动画1==动画2)>动画4;效果如下:
除了上述知识点之外,属性动画还有一个非常重要的知识点那就是Interpolator,下面通过一个常用的自定义控件switcher来演示Interpolator的用法。先上一个慢放的效果图:
先处理我们比较熟悉的绘制过程,很明显这个switcher无论什么状态都是由两个图形组成,外围的椭圆和内部的图形(圆或椭圆),且外围椭圆的背景是根据状态变化的。
外围的图形所在矩形坐标好确认,如果不考虑pading之类的参数,左上x、y的起始值都为0,右下x、y值分别为其宽度和高度。内部图形是坐标是变化的,我们定义了mInnerStartX、mInnerStartY、 mInnerEndX、 mInnerEndY 这4个值来代表其所在矩形的左上和右下的x、y值。另外,需要根据当前的背景绘制设置外围椭圆的背景。绘制过程如下:
/**
* 根据当前背景和计算出的坐标信息绘制switcher
* @param canvas
*/
private void drawSwitcher(Canvas canvas) {
RectF rect = new RectF(0, 0, mWidth, mHeight);
mPaint.setColor(mBgOnCurrent);
canvas.drawRoundRect(rect, rect.height() / 2, rect.height() / 2, mPaint);//画外部椭圆
RectF innerRect = new RectF(mInnerStartX, mInnerStartY, mInnerEndX, mInnerEndY);
mPaint.setColor(Color.WHITE);
canvas.drawRoundRect(innerRect, innerRect.height() / 2, innerRect.height() / 2, mPaint);//画内部椭圆
}
@Override
protected void onDraw(Canvas canvas) {
drawSwitcher(canvas);
super.onDraw(canvas);
}
宽度和高度的获取是在onSizeChanged中获得的,代码如下:
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
mInnerStartY = mInterval;
mInnerEndY = mHeight - mInterval;
if (isOpen) {
buildOpenStatusParams();
} else {
buildCloseStatusParams();
}
}
isOpen标识当前switcher是否为打开状态, buildOpenStatusParams为构建打开时的状态, buildCloseStatusParams为构建关闭时的状态,实现过程后面会有介绍。
剩下的主要工作就是构建switcher每个变化过程的绘制参数,这也是难点所在。
首先将switcher变化动画分解,通过观察可以看到,动画主要分为4个状态:起始状态、移动状态1、移动状态2、结束状态。移动状态1和2内部为一个椭圆,其中状态1椭圆的起始X跟起始状态X一致,状态2椭圆的结束X与结束状态X一致。所谓的起始状态和结束状态是相对的,也就是说,开关启动前状态为起始状态,结束时为结束状态,可以是内部圆在左侧的时候,也可以是内部圆在右侧的时候。
将上述分析转换为代码形式:
if (null == mAnimator) {
mAnimator = ValueAnimator.ofInt(0, 3);
mAnimator.setDuration(DURATION_TIME);
mAnimator.setInterpolator(new SwitcherInterpolator());
//设置监听,动画结束改变按钮的当前状态值
mAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
isOpen = !isOpen;
if (null != mCallBack) {
mCallBack.IsOpen(isOpen);
}
}
});
//根据进度判断处于何种状态,计算对应的绘制参数,0-起始状态,1-移动过程1,2-移动过程2,3-结束状态
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
switch ((int) animation.getAnimatedValue()) {
case 0:
if (isOpen) {
buildOpenStatusParams();
} else {
buildCloseStatusParams();
}
break;
case 1:
if (isOpen) {
buildRightToCloseStatusParams();
} else {
buildLeftToOpenStatusParams();
}
break;
case 2:
if (isOpen) {
buildLeftToCloseStatusParams();
} else {
buildRightToOpenStatusParams();
}
break;
case 3:
if (isOpen) {
buildCloseStatusParams();
} else {
buildOpenStatusParams();
}
break;
}
invalidate();
}
});
} else if (mAnimator.isRunning()) {
return;
}
mAnimator.start();
如果动画不存在则构建动画,值的变化范围为0~3,分别代表不同的状态:0-起始状态,1-移动过程1,2-移动过程2,3-结束状态。添加监听,在动画结束时改变按钮的状态。添加进度监听,根据不同的AnimatedValue(实际为不同的状态),构建不同状态的绘制参数。如果动画存在且正在运行则不执行任何操作,否则启动已有动画。动画通过 setInterpolator方法设置了一个自定义Interpolator——SwitcherInterpolator,其代码如下:
public class SwitcherInterpolator implements Interpolator {
@Override
public float getInterpolation(float input) {
if(input<0.1){
return 0;
}else if(input<0.5){
return 0.34f;
}else if(input<0.9){
return 0.67f;
}else{
return 1.0f;
}
}
}
SwitcherInterpolator中根据传入的input(动画执行的比例,取值从0到1)计算出当前动画的进行程度。这里将动画的前1/10时间返回值设为0,1/10-1/2设置为0.34f, 1/2-9/10设置为0.67f,后1/10时间设置为1.0f.
AnimatedValue值需要结合上面所讲的TypeEvaluator知识来计算,例如移动状态1的AnimatedValue=0+0.34f*(3-0).
另外,我们也可以通过自定义TypeEvaluator的方式来达到这种效果,TypeEvaluator是用来计算其对应对象的属性在某个插值下的值。我们完全可以在TypeEvaluator下判断当前的插值,然后返回对应的值(0,1,2,3),跟SwitcherInterpolator中的逻辑类似。
哪Interpolator和TypeEvaluator能否相互取代呢?答案肯定是否定的,因为它俩的职责本身就不同,Interpolator主要是改变插值,而TypeEvaluator是根据插值改变属性的值。当然,如果在TypeEvaluator增加一定的逻辑也能取代Interpolator达到特定的效果,但是这与其设计初衷想背离的。
mAnimator返回值表示当前的动画状态,根据动画状态和switcher的开关状态构建绘制参数,构建方法如下:
/***
* 构建switcher关闭状态参数
*/
private void buildCloseStatusParams() {
mInnerStartX = mInnerStartY;
mInnerEndX = mInnerStartX + (mHeight - 2 * mInterval);
mBgOnCurrent = mBgOnClose;
}
/***
* 构建switcher打开状态参数
*/
private void buildOpenStatusParams() {
mBgOnCurrent = mBgOnOpen;
mInnerEndX = mWidth - mInterval;
mInnerStartX = mInnerEndX - (mHeight - mInterval * 2);
}
/****
* 构建switcher打开过程1参数,启动起始X位置为间隔,
* 结束X位置为宽度的2/3,当前背景为关闭
*/
private void buildLeftToOpenStatusParams() {
mInnerStartX = mInnerStartY;
mInnerEndX = mWidth / 3 * 2;
mBgOnCurrent = mBgOnClose;
}
/****
* 构建switcher打开过程2参数,启动起始X位置为宽度的1/3,
* 结束X位置为宽度-间隔,当前背景为打开
*/
private void buildRightToOpenStatusParams() {
mInnerStartX = mWidth / 3;
mInnerEndX = mWidth - mInterval;
mBgOnCurrent = mBgOnOpen;
}
/***
* 构建switcher关闭过程1参数,启动起始X位置为宽度的1/3,
* 结束X位置为宽度-间隔,当前北京为打开
*/
private void buildRightToCloseStatusParams() {
mInnerStartX = mWidth / 3;
mInnerEndX = mWidth - mInterval;
mBgOnCurrent = mBgOnOpen;
}
/****
* 构建switcher关闭过程2参数,启动起始X位置为间隔,
* 结束X位置为宽度的2/3,当前背景为关闭
*/
private void buildLeftToCloseStatusParams() {
mInnerStartX = mInnerStartY;
mInnerEndX = mWidth / 3 * 2;
mBgOnCurrent = mBgOnClose;
}
打开状态和关闭状态可能为起始状态也可能是结束状态,需要根据switcher的开关状态来判断,如果当前switcher为关闭,则关闭状态为起始状态,打开状态为结束状态;如果当前switcher为打开则打开状态为起始状态,关闭状态为结束状态。switcher的打开和关闭过程逻辑跟上面的逻辑类似,结合代码非常容易理解,就不详细讲解了。
剩下的就是一些完善性工作了
设置开关状态的监听,获取开关的状态变化
/***
* 设置状态改变回调
* @param callback
*/
public void SetCallBack(SwitcherCallback callback) {
mCallBack = callback;
}
public interface SwitcherCallback {
void IsOpen(boolean aIsOpen);
}
设置开关的状态
/***
* 设置开关状态,不回调状态改变接口
* @param isOpen 开关状态
*/
public void setOpen(boolean isOpen) {
setOpen(isOpen,false);
}
/***
* 设置开关状态,根据需要回调状态
* @param isOpen 开关状态
* @param isCallBack 是否需要回调
*/
public void setOpen(boolean isOpen, boolean isCallBack) {
this.isOpen = isOpen;
if (isOpen) {
buildOpenStatusParams();
} else {
buildCloseStatusParams();
}
invalidate();
if (isCallBack && null != mCallBack) {
mCallBack.IsOpen(isOpen);
}
}
switcher的核心代码就这些,主要是对属性动画的灵活运用,以及对Interpolator的理解。这两篇文章对应的代码都放在了一个项目里,有需要的可以下载。项目地址