在App中合理地使用动画能够获得友好愉悦的用户体验,Android中的动画有View动画、属性动画、帧动画、布局动画、转场动画等,在5.x以后有又新增了矢量动画,这些动画在平常开发中使用较为普遍,所以有必要做一次完整的总结。
一、View动画
View动画定义了渐变Alpha、旋转Rotate、缩放Scale、平移Translate四种基本动画,并且通过这四种基本动画的组合使用,可以实现多种交互效果。
View动画使用非常简单,不仅可以通过XML文件来定义动画,同样可以通过Java代码来实现动画过程。
1.Xml文件定义View动画
通过xml来定义View动画涉及到一些公有的属性(在AndroidStudio上不能提示):
android:duration 动画持续时间
android:fillAfter 为true动画结束时,View将保持动画结束时的状态
android:fillBefore 为true动画结束时,View将还原到开始开始时的状态
android:repeatCount 动画重复执行的次数
android:repeatMode 动画重复模式 ,重复播放时restart重头开始,reverse重复播放时倒叙回放,该属性需要和android:repeatCount一起使用
android:interpolator 插值器,相当于变速器,改变动画的不同阶段的执行速度
这些属性是从Animation中继承下来的,在alpha
、rotate
、scale
、translate
标签中都可以直接使用。
利用xml文件定义View动画需要在工程的res目录下创建anim文件夹,所有的xml定义的View动画都要放在anim目录下。
渐变view_anim_alpha.xml
:
旋转view_anim_rotate.xml
:
缩放view_anim_scale.xml
:
平移view_anim_translate.xml
:
rotate
、scale
动画的android:pivotX
和android:pivotY
属性、translate
动画的android:toXDelta
和android:toYDelta
属性的取值都可以是都可以数值、百分数、百分数p
,比如:50
、50%
、50%p
,他们取值的代表的意义各不相同:
50
表示以View左上角为原点沿坐标轴正方向(x
轴向右,y
轴向下)偏移50px
的位置;
50%
表示以View左上角为原点沿坐标轴正方向(x
轴向右,y
轴向下)偏移View宽度或高度的50%处的位置;
50%p
表示以View左上角为原点沿坐标轴正方向(x
轴向右,y
轴向下)偏移父控件宽度或高度的50%处的位置(p
表示相对于ParentView
的位置)。
通过定义xml动画资源文件,在Activity中调用:
public void clickToAlpha(View view) {
Animation alphaAnim = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.view_anim_alpha);
mTargetView.startAnimation(alphaAnim);
}
public void clickToRotate(View view) {
Animation rotateAnim = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.view_anim_rotate);
mTargetView.startAnimation(rotateAnim);
}
public void clickToScale(View view) {
Animation scaleAnim = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.view_anim_scale);
mTargetView.startAnimation(scaleAnim);
}
public void clickToTranslate(View view) {
Animation translateAnim = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.view_anim_translate);
mTargetView.startAnimation(translateAnim);
}
public void clickToSet(View view) {
Animation setAnim = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.view_anim_set);
mTargetView.startAnimation(setAnim);
}
2.Java代码实现View动画
在平常的业务逻辑中也可以直接用Java代码来实现Veiw动画,Android系统给我们提供了AlphaAnimation
、RotateAnimation
、ScaleAnimation
、TranslateAnimation
四个动画类分别来实现View的渐变、旋转、缩放、平移动画。
渐变:
public void clickToAlpha(View view) {
AlphaAnimation alphaAnimation = new AlphaAnimation(1, 0);
alphaAnimation.setDuration(2000);
mTargetView.startAnimation(alphaAnimation);
}
旋转:
public void clickToRotate(View view) {
RotateAnimation rotateAnimation = new RotateAnimation(
0, 360,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
rotateAnimation.setDuration(2000);
mTargetView.startAnimation(rotateAnimation);
}
缩放:
public void clickToScale(View view) {
ScaleAnimation scaleAnimation = new ScaleAnimation(
1, 0.5f,
1, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setDuration(2000);
mTargetView.startAnimation(scaleAnimation);
}
平移:
public void clickToTranslate(View view) {
TranslateAnimation translateAnimation = new TranslateAnimation(
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 1,
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 1);
translateAnimation.setDuration(2000);
mTargetView.startAnimation(translateAnimation);
}
组合:
public void clickToSet(View view) {
AlphaAnimation alphaAnimation = new AlphaAnimation(1, 0);
alphaAnimation.setDuration(2000);
RotateAnimation rotateAnimation = new RotateAnimation(
0, 360,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
rotateAnimation.setDuration(2000);
ScaleAnimation scaleAnimation = new ScaleAnimation(
1, 0.5f,
1, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setDuration(2000);
TranslateAnimation translateAnimation = new TranslateAnimation(
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 1,
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 1);
translateAnimation.setDuration(2000);
AnimationSet animationSet = new AnimationSet(true);
animationSet.addAnimation(alphaAnimation);
animationSet.addAnimation(rotateAnimation);
animationSet.addAnimation(scaleAnimation);
animationSet.addAnimation(translateAnimation);
mTargetView.startAnimation(animationSet);
}
View动画可以设置一个动画执行的监听器:
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
// 动画开始
}
@Override
public void onAnimationEnd(Animation animation) {
// 动画结束
}
@Override
public void onAnimationRepeat(Animation animation) {
//动画重复
}
});
通过设置监听器可以在动画执行的开始、结束、重复时做一些其他的业务逻辑。
二、属性动画
所谓属性动画,就是改变对象Object
的属性来实现动画过程。属性动画是对View的动画的扩展,通过它可以实现更多漂亮的动画效果。同时属性动画的作用对象不仅仅是View,任何对象都可以。
属性动画的作用效果就是:在一个指定的时间段内将对象的一个属性的属性值动态地变化到另一个属性值。
1.ObjectAnimator
ObjectAnimator
是最常用的属性动画执行类。
private void startJavaPropertyAnimator() {
ObjectAnimator
.ofFloat(mImageView, "rotationY", 0f, 360f)
.setDuration(2000)
.start();
}
上面的代码就是通过ObjectAnimator
在2000ms内将mImageView
的rotationY
属性的属性值从0f
变化的360f
。
ObjectAnimtor
可以用
ofInt
、
ofFloat
、
ofObject
等静态方法,传入动画作用的
目标Object
、属性字段、属性开始值、属性中间值、属性结束值等参数来构造动画对象。
在动画更新的过程中,通过不断去调用对象属性的
setter
方法改变属性值,不断重绘实现动画过程。如果没有给定动画开始属性值,那么系统会通过反射去获取
Object
对象的初始值作为动画的开始值。
属性动画也同样可以通过xml文件来定义,同样在工程的res目录下创建animator文件夹,xml文件定义的
objectAnimator
动画要放在该文件夹下。
property_animator.xml
:
Java代码调用:
private void startXmlPropertyAnimator() {
Animator animator = AnimatorInflater.loadAnimator(getApplicationContext(),
R.animator.property_animator);
animator.setTarget(mImageView);
animator.start();
}
最终效果如上图。
属性动画也同样可以组合使用,通过AnimatorSet
类和xml文件的set
标签都可以同时改变对象的多个属性,实现更加丰富的动画效果。
通过AnimatorSet
创建动画集:
private void startJavaPropertyAnimatorSet() {
Animator scaleXAnimator = ObjectAnimator.ofFloat(mImageView, "scaleX", 1, 0.5f);
scaleXAnimator.setDuration(2000);
Animator scaleYAnimator = ObjectAnimator.ofFloat(mImageView, "scaleY", 1, 0.5f);
scaleYAnimator.setDuration(2000);
Animator rotationXAnimator = ObjectAnimator.ofFloat(mImageView, "rotationX", 0, 360);
rotationXAnimator.setDuration(2000);
Animator rotationYAnimator = ObjectAnimator.ofFloat(mImageView, "rotationY", 0, 360);
rotationYAnimator.setDuration(2000);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(scaleXAnimator)
.with(scaleYAnimator)
.before(rotationXAnimator)
.after(rotationYAnimator);
animatorSet.start();
}
AnimatorSet
通过before
、with
、after
三个方法可以组合多个属性动画,with
表示与给定动画同时执行,before
在给定动画执行之前执行,after
表示在给定动画执行之后执行。
通过xml文件定义属性动画集:
property_animator_set.xml
在Java代码中调用属性动画集:
private void startxmlPropertyAnimatorSet() {
Animator animator = AnimatorInflater.loadAnimator(getApplicationContext(),
R.animator.property_animator_set);
animator.setTarget(mImageView);
animator.start();
}
同样,属性动画也可以添加动画执行监听器:
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
// 动画开始
}
@Override
public void onAnimationEnd(Animator animation) {
// 动画结束
}
@Override
public void onAnimationCancel(Animator animation) {
// 动画取消
}
@Override
public void onAnimationRepeat(Animator animation) {
// 动画重复
}
});
在监听到属性动画开始、结束、取消、重复时可以去做一些其他的逻辑业务。
2.ValueAnimator
ValueAnimator
是ObjectAnimator
的父类,它继承自Animator
。ValueAnimaotor
同样提供了ofInt
、ofFloat
、ofObject
等静态方法,传入的参数是动画过程的开始值、中间值、结束值来构造动画对象。可以将ValueAnimator
看着一个值变化器,即在给定的时间内将一个目标值从给定的开始值变化到给定的结束值。在使用ValueAnimator
时通常需要添加一个动画更新的监听器,在监听器中能够获取到执行过程中的每一个动画值。
private void startValueAnimator() {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
valueAnimator.setDuration(300);
valueAnimator.start();
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 动画更新过程中的动画值,可以根据动画值的变化来关联对象的属性,实现属性动画
float value = (float) animation.getAnimatedValue();
Log.d("ValueAnimator", "动画值:" + value);
}
});
}
在300ms内将数值0变化到1的动画值的变化log:
02-25 23:16:57.586 D/ValueAnimator: 动画值:0.0
02-25 23:16:57.596 D/ValueAnimator: 动画值:0.007902175
02-25 23:16:57.616 D/ValueAnimator: 动画值:0.029559612
02-25 23:16:57.636 D/ValueAnimator: 动画值:0.066987276
02-25 23:16:57.646 D/ValueAnimator: 动画值:0.118102014
02-25 23:16:57.666 D/ValueAnimator: 动画值:0.18128797
02-25 23:16:57.686 D/ValueAnimator: 动画值:0.2545482
02-25 23:16:57.706 D/ValueAnimator: 动画值:0.33063102
02-25 23:16:57.716 D/ValueAnimator: 动画值:0.4166157
02-25 23:16:57.736 D/ValueAnimator: 动画值:0.5052359
02-25 23:16:57.746 D/ValueAnimator: 动画值:0.5936906
02-25 23:16:57.766 D/ValueAnimator: 动画值:0.67918396
02-25 23:16:57.786 D/ValueAnimator: 动画值:0.7545208
02-25 23:16:57.796 D/ValueAnimator: 动画值:0.82671034
02-25 23:16:57.826 D/ValueAnimator: 动画值:0.88857293
02-25 23:16:57.836 D/ValueAnimator: 动画值:0.93815327
02-25 23:16:57.856 D/ValueAnimator: 动画值:0.9721882
02-25 23:16:57.876 D/ValueAnimator: 动画值:0.99384415
02-25 23:16:57.886 D/ValueAnimator: 动画值:1.0
ValueAnimator
的使用一般会结合更新监听器AnimatorUpdateListener
,大多数时候是在自定义控件时使用。
下面是用ValueAnimator
自定义控件实现动画打开关闭效果。
expanded_veiw.xml
:
public class ExpandedView extends FrameLayout {
private TextView mTvAnswer;
private boolean isClosed;
private ImageView mIvIndicator;
public ExpandedView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
View view = LayoutInflater.from(context).inflate(R.layout.expanded_view, this, true);
LinearLayout llQuestion = (LinearLayout) view.findViewById(R.id.ll_expanded_question);
llQuestion.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
anim();
}
});
mTvAnswer = (TextView) view.findViewById(R.id.tv_expanded_answer);
mIvIndicator = (ImageView) view.findViewById(R.id.iv_expanded_indicator);
}
private void anim() {
// 指示器旋转
ValueAnimator valueAnimator1 = isClosed
? ValueAnimator.ofFloat(180, 0)
: ValueAnimator.ofFloat(0, 180);
valueAnimator1.setDuration(500);
valueAnimator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (float) animation.getAnimatedValue();
mIvIndicator.setRotation(value);
}
});
valueAnimator1.start();
// 打开开关闭操作
final int answerHeight = mTvAnswer.getMeasuredHeight();
ValueAnimator valueAnimator2 = isClosed
? ValueAnimator.ofInt(-answerHeight, 0)
: ValueAnimator.ofInt(0, -answerHeight);
valueAnimator2.setDuration(500);
final MarginLayoutParams params = (MarginLayoutParams) mTvAnswer.getLayoutParams();
valueAnimator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
params.bottomMargin = value;
mTvAnswer.setLayoutParams(params);
}
});
valueAnimator2.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
isClosed = !isClosed;
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
valueAnimator2.start();
}
}
3.TypeEvaluator
ObjectAnimator
和ValueAnimator
都有ofObject
方法,传入的都有一个TypeEvaluator
类型的参数。TypeEvaluator
是一个接口,里面也只有一个抽象方法:
public T evaluate(float fraction, T startValue, T endValue);
再看ofInt
方法中没有传入该参数,但实际上调用ofInt
方法时,系统已经有实现了TypeEvaluator
接口的IntEvaluator
,它的源码也非常简单:
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}
fraction
范围为0到1,表示动画执行过程中已完成程度。
泛型T
即为动画执行的属性类型。
所以我们要用属性动画来执行复杂对象的动画过程,就需要自定义TypeEvaluator
,实现动画逻辑。
先来定义一个对象
public class Circle {
private int raduis; // 半径
private int color; // 颜色
private int elevation; // 高度
public Circle(int raduis, int color, int elevation) {
this.raduis = raduis;
this.color = color;
this.elevation = elevation;
}
public int getRaduis() {
return raduis;
}
public void setRaduis(int raduis) {
this.raduis = raduis;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public int getElevation() {
return elevation;
}
public void setElevation(int elevation) {
this.elevation = elevation;
}
}
CircleEvaluator
,实现TypeEvaluator
接口:
public class CircleEvaluator implements TypeEvaluator {
@Override
public Circle evaluate(float fraction, Circle startValue, Circle endValue) {
int startRaduis = startValue.getRaduis();
int endRaduis = endValue.getRaduis();
int raduis = (int) (startRaduis + fraction * (endRaduis - startRaduis));
int startColor = startValue.getColor();
int endColor = endValue.getColor();
int startColorRed = Color.red(startColor);
int startColorGreen = Color.green(startColor);
int startColorBlue = Color.blue(startColor);
int endColorRed = Color.red(endColor);
int endColorGreen = Color.green(endColor);
int endColorBlue = Color.blue(endColor);
int colorRed = (int) (startColorRed + fraction * (endColorRed - startColorRed));
int colorGreen = (int) (startColorGreen + fraction * (endColorGreen - startColorGreen));
int colorBlue = (int) (startColorBlue + fraction * (endColorBlue - startColorBlue));
int color = Color.rgb(colorRed, colorGreen, colorBlue);
int startElevation = startValue.getElevation();
int endElevation = endValue.getElevation();
int elevation = (int) (startElevation + fraction * (endElevation - startElevation));
return new Circle(raduis, color, elevation);
}
}
自定义控件CircleView,将Circle作为它的一个属性:
public class CircleView extends View {
private Circle circle;
private Paint mPaint;
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
circle = new Circle(168, Color.RED, 0);
mPaint = new Paint();
mPaint.setAntiAlias(true);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
setElevation(circle.getElevation());
mPaint.setColor(circle.getColor());
canvas.drawCircle(getMeasuredHeight() / 2, getMeasuredHeight() / 2, circle.getRaduis(), mPaint);
}
public void setCircle(Circle circle) {
this.circle = circle;
postInvalidate();
}
public Circle getCircle() {
return circle;
}
}
ObjectAnimator
使用:
private void start1() {
Circle startCircle = new Circle(168, Color.RED, 0);
Circle middleCircle = new Circle(300, Color.GREEN, 15);
Circle endCircle = new Circle(450, Color.BLUE, 30);
ObjectAnimator.ofObject(mCircleView, "circle", new CircleEvaluator(), startCircle, middleCircle, endCircle)
.setDuration(5000)
.start();
}
ValueAnimator
使用:
private void start2() {
Circle startCircle = new Circle(168, Color.RED, 0);
Circle middleCircle = new Circle(300, Color.GREEN, 15);
Circle endCircle = new Circle(450, Color.BLUE, 30);
ValueAnimator valueAnimator = ValueAnimator.ofObject(new CircleEvaluator(), startCircle, middleCircle, endCircle);
valueAnimator.setDuration(5000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Circle circle = (Circle) animation.getAnimatedValue();
mCircleView.setCircle(circle);
}
});
valueAnimator.start();
}
需要注意的是,系统调用获取控件setter、getter方法是通过反射获取的,属性的名称必须和getter、setter方法名称后面的字符串一致,比如上面的getter、setter方法分别为
getCircle
、
setCircle
,那么属性名字就必须为
circle
。
三、帧动画
帧动画需要开发者制定好动画每一帧,系统一帧一帧的播放图片。
private void start1() {
AnimationDrawable ad = new AnimationDrawable();
for (int i = 0; i < 7; i++) {
Drawable drawable = getResources().getDrawable(getResources().getIdentifier("ic_fingerprint_" + i, "drawable", getPackageName()));
ad.addFrame(drawable, 100);
}
ad.setOneShot(false);
mImageView.setImageDrawable(ad);
ad.start();
}
帧动画同样也可以在xml文件中配置,直接在工程drawable
目录新建animation-list
标签:
private void start2() {
mImageView.setImageResource(R.drawable.frame_anim);
AnimationDrawable animationDrawable = (AnimationDrawable) mImageView.getDrawable();
animationDrawable.start();
}
其中android:onshot
属性和setOneShot
方法表示是否只执行一次。
AnimationDrawable
实际是上是一个Drawable
动画,动画执行的过程总会给你不断重绘Drawable
的每一帧图像,实现动态播放效果。
源码:https://github.com/xiaoyanger0825/AnimationSummary