最新总结,请移步 Android 动画解析
0x01 分类
视图动画(逐帧动画/补间动画)
属性动画
转场动画
1. 逐帧动画
frame-by-frame animation
包位置:android.graphics.drawable
类名:AnimationDrawable
将多张图片,连贯起来进行播放,类似动画片的工作性质。
2. 补间动画
tweened animation
包位置:android.view.animation
类名:Animation
仅对 View 进行淡入淡出 alpha、平移 translate、旋转 rotate、缩放 scale 四种操作。不可对 View 的属性做操作,例如颜色、长度等
AlphaAnimation
TraslateAnimation
RotateAnimation
ScaleAnimation
AnimationSet
Interpolator
AnimationUtils
3. 属性动画
Property Animator
包位置:android.animation
类名:Animator
Android3.0引入,弥补了补间动画的缺陷(只能对 View 进行操作)
ViewPropertyAnimator(包位于android.view,内部使用ValueAnimator)
ObjectAnimator
ValueAnimator
AnimatorInflater
PropertyValuesHolder 对同一动画属性操作
AnimatorSet 组合多个动画
Interpolator
TypeEvaluator
0x02 使用
1. 逐帧动画
方式1 XML实现
animation-list 标签指定;
oneshot 是否仅播放一次 false;
duration 每一帧的显示时长
# 注意位置 res/drawable/frame_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@drawable/img_0"
android:duration="50" />
<item
android:drawable="@drawable/img_1"
android:duration="50" />
<item
android:drawable="@drawable/img_2"
android:duration="50" />
</animation-list >
--- // 代码加载
ImageView imageView = new ImageView(this);
imageView.setImageResource(R.drawable.frame_animation);
AnimationDrawable animationDrawable = (AnimationDrawable)imageView.getDrawable();
// 播放开始
animationDrawable.start();
// 播放结束
animationDrawable.stop();
方式2 代码实现
AnimationDrawable frameAnimation = new AnimationDrawable();
// 参数1 图片,参数2 出现时间
frameAnimation.addFrame(getResources().getDrawable(R.drawable.img_0), 50);
frameAnimation.addFrame(getResources().getDrawable(R.drawable.img_1), 50);
frameAnimation.addFrame(getResources().getDrawable(R.drawable.img_2), 50);
// 是否重复
frameAnimation.setOneShot(false);
// 播放开始
frameAnimation.start();
// 播放结束
frameAnimation.stop();
优缺点
缺点:使用大量图片,易导致OOM
建议:直接使用 gif,大小可以明显改善
2. 补间动画
方式1 XML 实现
- 平移动画
# 注意位置 res/anim/view_animation.xml
<translate
android:fromXDelta="0" // 水平x方向的起始值
android:fromYDelta="0" // 竖直y方向的起始值
android:toXDelta="500" // 水平x方向的结束值
android:toYDelta="500" // 竖直y方向的结束值
/>
- 透明动画
<alpha
android:fromAlpha="1.0" // 开始透明度 0.0-1.0
android:toAlpha="0.0" //结束透明度 0.0-1.0
/>
- 旋转动画
<rotate
android:fromDegrees="0" // 开始的角度
android:toDegrees="180" // 结束的角度
android:pivotX="20%" // 旋转中心点 x 坐标
android:pivotY="20%" // 旋转中心点 y 坐标
/>
- 缩放动画
<scale
android:fromXScale="0.0" // 起始缩放x的倍数
android:fromYScale="0.0" // 起始缩放y的倍数
android:toXScale="1.0" // 结束缩放x的倍数
android:toYScale="2.0" // 结束缩放y的倍数
android:pivotX="50%" // 缩放中心点的x坐标
android:pivotY="50%" // 缩放中心点的y坐标
/>
pivotX / pivotY 的取值有三个类型
- 数字, eg:android:pivotX="50", 该View左上角在x方向上平移50px的点,对应Java代码中设置参数 Animation.ABSOLUTE
- 百分比, eg:android:pivotX="50%", 该View左上角在x方向上平移自身宽度50%的点,对应Java代码中设置参数 Animation.RELATIVE_TO_SELF
- 百分比p (parent), eg:android:pivotX="50%p"该View左上角在x方向上平移父布局宽度50%的点,对应Java代码中设置参数 Animation.RELATIVE_TO_PARENT
- 公共属性
android:duration="3000" // 动画时长
android:fillAfter="true" // 动画结束后,是否留在结束位置,优先级高于fillBefore属性,默认是false
android:fillBefore="false" //动画结束后,是否留在开始位置,经试验证明只有fillBefore=false没有fillAfter属性时,不会停留在结束位置,默认是true
android:fillEnabled="true" //是否应用fillBefore的值,对fillAfter无影响,默认是true
android:interpolator="@android:anim/overshoot_interpolator"
android:repeatCount="0" //infinite无限重复
android:repeatMode="restart" // restart正序重播/reverse反转重播
android:shareInterpolator="true" // 组合动画属性,组合动画是否和集合(<set></set>)共享一个插值器,去过不指定,子动画需要单独设定
android:startOffset="100" //组合动画默认是全部动画同时开始,如果不同动画不同开始需要使用该属性延迟开始时间
- 在代码中使用
// 步骤1:创建 需要设置动画的 视图View
Button mButton = (Button) findViewById(R.id.Button);
// 步骤2:创建 动画对象 并传入设置的动画效果xml文件
Animation translateAnimation = AnimationUtils.loadAnimation(this, R.anim.view_animation);
// 步骤3:播放动画
mButton.startAnimation(translateAnimation);
方式2 代码实现
Button mButton = (Button) findViewById(R.id.Button);
// 组合动画设置
// 步骤1:创建组合动画shareInterpolator对象(设置为true)
AnimationSet setAnimation = new AnimationSet(true);
// 步骤2:设置组合动画的属性
// 特别说明以下情况
// 因为在下面的旋转动画设置了无限循环(RepeatCount = INFINITE)
// 所以动画不会结束,而是无限循环
// 所以组合动画的下面两行设置是无效的
setAnimation.setRepeatMode(Animation.RESTART);
setAnimation.setRepeatCount(1);// 设置了循环一次,但无效
// 步骤3:逐个创建子动画(方式同单个动画创建方式,此处不作过多描述)
// 子动画1:旋转动画
Animation rotate = new RotateAnimation(0,360,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
rotate.setDuration(1000);
rotate.setRepeatMode(Animation.RESTART);
rotate.setRepeatCount(Animation.INFINITE);
// 子动画2:平移动画
Animation translate = new TranslateAnimation(TranslateAnimation.RELATIVE_TO_PARENT,-0.5f,
TranslateAnimation.RELATIVE_TO_PARENT,0.5f,
TranslateAnimation.RELATIVE_TO_SELF,0
,TranslateAnimation.RELATIVE_TO_SELF,0);
translate.setDuration(10000);
// 子动画3:透明度动画
Animation alpha = new AlphaAnimation(1,0);
alpha.setDuration(3000);
alpha.setStartOffset(7000);
// 子动画4:缩放动画
Animation scale1 = new ScaleAnimation(1,0.5f,1,0.5f,Animation.RELATIVE_TO_SELF,0.5f,Animation.RELATIVE_TO_SELF,0.5f);
scale1.setDuration(1000);
scale1.setStartOffset(4000);
// 步骤4:将创建的子动画添加到组合动画里
setAnimation.addAnimation(alpha);
setAnimation.addAnimation(rotate);
setAnimation.addAnimation(translate);
setAnimation.addAnimation(scale1);
// 步骤5:播放动画
mButton.startAnimation(setAnimation);
优缺点
缺点:
- 作用对象局限:根据包分类可知 android.view.animation,只针对View进行动画操作
- 只改变视觉效果,没有改变属性,例如点击只在原始位置有效(经API27测试,当属性动画View完全不可见时,点击位置和范围为原始位置)
- 动画效果单一
3. 属性动画
ObjectAnimator extends ValueAnimator extends Animator 位于 android.animation
ViewPropertyAnimator 位于 android.view ,其本质内部使用ValueAnimator
ViewPropertyAnimator - ObjectAnimator - ValueAnimator
使用难度越来越高,但越来越灵活。
举个例子:
View view = new View(this);
# ViewPropertyAnimator
view.animate().alphaBy(0.8f) // xxxBy 以当前状态为参考,translate等同理, 默认时间为300ms
# ObjectAnimator
ObjectAnimator objectAnimator = Object.ofFloat(view, "alpha", 1.0f, 0.0f, 1.0f);
objectAnimator.start();
// "alpha" 的本质不是view的属性,而是view的setAlpha 和 getAlpha方法,然后再setAlpha方法中调用 invalidate() 方法不断重绘。方法名严格,属性名可以不同,如mAlpha等,待查看源码体现
# ValueAnimator
ValueAnimator valueAnimator = ValueAnimator.ofFloat(1.0f, 0.0f, 1.0f);
value.addUpdateListener(new ValueAnimator.AnimatorUpdateListener(){
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (float) animation.getAnimatedValue();
view.setAlpha(animatedValue);
}
});
valueAnimator.start();
3.1 ViewPropertyAnimator
为了满足面向对象编程思想,引入了ViewPropertyAnimator,可以链式调用。但是没有 和setRepeatMode() 和 setRepeatCount() 方法
View 中的方法 | 功能 | 对应 ViewPropertyAnimator 的方法 |
---|---|---|
setTranslationX() | 设置x轴偏移 | translationX() translationXBy() |
setTranslationY() | 设置y轴偏移 | translationY() translationYBy() |
setTranslationZ() | 设置z轴偏移 | translationZ() translationZBy() |
setX() | 设置x轴绝对位置 | x() xBy() |
setY() | 设置y轴绝对位置 | y() yBy() |
setZ() | 设置z轴绝对位置 | z() zBy() |
setRotation() | 设置平面旋转 | rotation() rotationBy() |
setRotationX() | 设置沿x轴旋转 | rotationX() rotationXBy() |
setRotationY() | 设置沿y轴旋转 | rotationY() rotationYBy() |
setScaleX() | 设置横向放缩 | scaleX() scaleXBy() |
setScaleY() | 设置纵向放缩 | scaleY() scaleYBy() |
setAlpha() | 设置透明度 | alpha() alphaBy() |
- View#setX()没有动画渐变效果,直接将该View放到设置位置;而 ViewPropertyAnimator#x() 有平移过渡动画。
- ViewPropertyAnimator#x() 如果有动画执行x()将取消。例如view.x(500).translationX(50),将只执行translationX(50),不执行x(500)
- ViewPropertyAnimator#By() 表示在当前位置的基础上进行操作
3.2 Animator的使用
方式1 XML实现
# 注意位置 res/animator/view_animator.xml
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="sequentially">
<objectAnimator
android:duration="2000"
android:propertyName="translationX"
android:valueFrom="0"
android:valueTo="500"
android:valueType="floatType" />
<set android:ordering="together">
<objectAnimator
android:duration="3000"
android:propertyName="rotation"
android:valueFrom="0"
android:valueTo="360"
android:valueType="floatType" />
<set android:ordering="sequentially">
<objectAnimator
android:duration="1500"
android:propertyName="alpha"
android:valueFrom="1"
android:valueTo="0"
android:valueType="floatType" />
<objectAnimator
android:duration="1500"
android:propertyName="alpha"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</set>
</set>
--- // 代码调用,注意区分animation AnimationUtils
Animator animator = AnimatorInflater.loadAnimator(context,R.animator.view_animator);
animator.setTarget(view);
animator.start();
方式2 Java代码实现
ObjectAnimator moveIn = ObjectAnimator.ofFloat(view, "translationX", 0, 500f);
ObjectAnimator rotate = ObjectAnimator.ofFloat(view, "rotation", 0f, 360f);
ObjectAnimator fadeInOut = ObjectAnimator.ofFloat(view, "alpha", 1f, 0f, 1f);
AnimatorSet animSet = new AnimatorSet();
animSet.play(rotate).with(fadeInOut).before(moveIn); // with / before / after
animSet.setDuration(5000);
animSet.start();
自定义控件实现
步骤
- 添加setter / getter 方法
- 用ObjectAnimator.ofXXX() 创建 ObjectAnimator 对象(或者使用ValueAnimator的方式)
- start()执行动画
public class SportsView extends View {
float progress = 0;
......
// 创建 getter 方法
public float getProgress() {
return progress;
}
// 创建 setter 方法
public void setProgress(float progress) {
this.progress = progress;
invalidate();
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
......
canvas.drawArc(arcRectF, 135, progress * 2.7f, false, paint);
......
}
}
......
// 创建 ObjectAnimator 对象
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "progress", 0, 65);
// 执行动画
animator.start();
大部分情况下,初始位置和结束位置不能提前确定,所有一般使用代码实现。
对比ObjectAnimator和ValueAnimator的使用
ofFloat 对比
// 注意setTranslationX参数是float类型,ofInt不能用translationX属性
// View # setTranslationX(float translationX)
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "translationX", 0.0f, 500.0f);
objectAnimator.start();
// value 在回调中更新view的状态
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0.0f, 500.0f);
valueAnimator.addUpdateListener(new valueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (float) animation.getAnimatedValue();
view.setTranslationX(animatedValue);
view.requestLayout();
}
});
valueAnimator.start();
ofObject 对比, 需要自定义 TypeEvaluator<T>
ObjectAnimator objectAnimator = ObjectAnimator.ofObject(view, "customFiled", new MyObjectEvaluator(), object1, object2);
objectAnimator.start();
---
ValueAnimator valueAnimator = ValueAnimator.ofObject(new MyObjectEvaluator(), object1, object2);
valueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
MyObject value = (MyObject) animation.getAnimatedValue();
view.setCustomFiled(value);
view.requestLayout();
}
});
valueAnimator.start();
4. 插值器和估值器
4.1 对比区别
类型 | 定义 | 作用 |
---|---|---|
插值器 Interpolator | 一个辅助动画实现的接口 | 确定属性值0-1变化规律 |
估值器 TypeEvaluator | 一个协助插值器的接口,属性动画特有属性 | 计算属性值0-1具体数值(通过变化规律计算数字) |
4.2 插值器
4.2.1 本质
根据动画的进度(0% - 100%)计算当前属性值改变的百分比
4.2.2 实现
实现下面两个接口
android.animation # interface TimeInterpolator
android.view.animation / interface Interpolator extends TimeInterpolator
重写 float getInterpolation(float input) 方法
补间动画实现 Interpolator接口;属性动画实现TimeInterpolator接口。
TimeInterpolator 接口是属性动画中新增的,用于兼容Interpolator 接口,这使原先的Interpolator实现累都可以直接在属性动画使用
4.2.3 举例说明
public class AccelerateDecelerateInterpolator implements Interpolator, NativeInterpolatorFactory {
// 仅贴出关键代码
...
public float getInterpolation(float input) {
return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
// input的运算逻辑如下:
// 使用了余弦函数,因input的取值范围是0到1,那么cos函数中的取值范围就是π到2π。
// 而cos(π)的结果是-1,cos(2π)的结果是1
// 所以该值除以2加上0.5后,getInterpolation()方法最终返回的结果值还是在0到1之间。只不过经过了余弦运算之后,最终的结果不再是匀速增加的了,而是经历了一个先加速后减速的过程
// 所以最终,fraction值 = 运算后的值 = 先加速后减速
// 所以该差值器是先加速再减速的
}
}
4.2.4 列举官方插值器
Java类 | 描述 |
---|---|
LinearInterpolator | 匀速 |
AccelerateInterpolator | 加速 |
DecelerateInterpolator | 减速 |
AccelerateDecelerateInterpolator | 先加速后减速(默认) |
AnticipateInterpolator | 先退后然后加速前进 |
OvershootInterpolator | 完成动画,超出后回到结束位置 |
AnticipateOvershootInterpolator | 先退后再加速前进,超出终点后再回终点 |
BounceInterpolator | 最后阶段弹球效果 |
CycleInterpolator | 周期运动 |
PathInterpolator | 自定义动画完成度/时间完成度曲线(0-1) |
FastOutLinearInInterpolator | 加速 |
LinearOutSlowInInterpolator | 持续减速 |
FastOutSlowInInterpolator | 先加速再减速 |
最后三个是Android5.0(API21)新增,和之前的类似,但是轨迹稍有区别
4.3 估值器
4.3.1 本质
根据当前属性值变化的百分比、初始值、结束值来计算当前属性的具体数值。当前属性值 = 初始值 + (结束值 - 初始值) * 百分比
4.3.2 实现
- 实现
TypeEvaluator<T>
接口 - 重写
public T evaluate(float fraction, T startValue, T endValue)
方法
4.3.3 官方示例
public class FloatEvaluator implements TypeEvaluator<Number> {
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
}
4.3.4 完整示例
public class Point {
private float x;
private float y;
public Point(float x, float y) {
this.x = x;
this.y = y;
}
public float getX() {
return x;
}
public float getY() {
return y;
}
}
public class PointEvaluator implements TypeEvaluator<Point> {
@Override
public Point evaluate(float fraction, Point startValue, Point endValue) {
float startValueX = startValue.getX();
float startValueY = startValue.getY();
float currentX = startValueX + (endValue.getX() - startValueX) * fraction;
float currentY = startValueY + (endValue.getY() - startValueY) * fraction;
return new Point(currentX, currentY);
}
}
public class PointView extends View {
public static final float RADIUS = 70f;
private Point ccurrentPoint;
private Paint mPaint;
public PointView(Context context) {
this(context, null);
}
public PointView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.BLUE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (ccurrentPoint == null) {
ccurrentPoint = new Point(RADIUS, RADIUS);
}
canvas.drawCircle(ccurrentPoint.getX(), ccurrentPoint.getY(), RADIUS, mPaint);
}
public Point getCurrentPoint() {
return ccurrentPoint;
}
public void setCurrentPoint(Point currentPoint) {
this.ccurrentPoint = currentPoint;
invalidate();
}
}
Point pointStart = new Point(70, 70);
Point pointEnd = new Point(700, 1000);
// 使用 ObjectAnimator
ObjectAnimator objectAnimator = ObjectAnimator.ofObject(point_view, "currentPoint", new PointEvaluator(), pointStart, pointEnd);
objectAnimator.setDuration(1000);
objectAnimator.start();
// 使用 ValueAnimator
ValueAnimator valueAnimator = ValueAnimator.ofObject(new PointEvaluator(), pointStart, pointEnd);
valueAnimator.setDuration(1000);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Point animatedValue = (Point) animation.getAnimatedValue();
point_view.setCurrentPoint(animatedValue);
}
});
valueAnimator.start();
5.设置监听器
添加监听器
ViewPropertyAnimator#setListener()/setUpdateListener()
ObjectAnimator#addListener()/addUpdateListener/addPauseListener()
移除监听器
ViewPropertyAnimator#setListener(null) / setUpdateListener(null) 填null来移除
ObjectAnimator #removeListener() / removeUpdateListener() / removePauseListener()
ObjectAnimator 支持pause()方法暂停
ViewPropertyAnimator 不支持setRepeatMode() / setRepeatCount() 方法
ViewPropertyAnimator 独有withStartAction(Runnable runnable) 和 withEndAction(Runnable runnable) 方法,可设置一次动画开始或结束的监听。即使重新开始动画,也不会回调,是一次性的。而AnimatorListener是持续有效的。
withEndAction() 只有在动画正常结束才会调用,而在动画被取消时是不会执行的。而 AnimatorListener.onAnimationEnd() 在取消之后也会被调用,在调用 onAnimationCancel()之后调用
6. PropertyValuesHolder 同一动画中改变多个属性 ObjectAnimator.ofPropertyValuesHolder
关键字:一边一边,一个动画属性同时执行,区别多个动画先后执行。一个动画需要共享开始时间/结束时间/Interpolator等等设定,PropertyValuesHolder不能有先后次序执行动画了。
很多时候,在同一个动画中需要改变多个属性,例如改变透明度的同时改变尺寸。
使用 ViewPropertyAnimator如下:
view.animate()
.scaleX(0.0f)
.scaleY(0.0f)
.alpha(0.0f)
但是ObjectAnimator,是不能这么用的。需要使用PropertyValuesHolder来同时在一个动画里改变多个属性
PropertyValuesHolder holder1 = PropertyValuesHolder.ofFloat("scaleX", 0.0f);
PropertyValuesHolder holder2 = PropertyValuesHolder.ofFloat("scaleY", 0.0f);
PropertyValuesHolder holder3 = PropertyValuesHolder.ofFloat("alpha", 0.0f);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, holder1, holder2, holder3)
animator.start();
ViewPropertyAnimator 动画完成之后会停留在结束位置,再次点击执行动作操作不会执行动画
ObjectAnimator 动画完成之后会停留在结束位置,再次点击执行动作操作会从最原始状态重新执行一次动画。且Animator没有Animation的setFillAfter() 和setFillBefore()方法
关于点击范围的测试说明(API27),属性动画执行后属性发生变化,即点击范围和位置会更新,但经测试当View完全不可见时,点击位置和范围为原始位置
ObjectAnimator.ofInt()
ObjectAnimator.ofFloat()
ObjectAnimator.ofMultiFloat()
ObjectAnimator.ofPropertyValuesHolder
6.AnimatorSet 多个动画配合执行
关键字:一边一边,一个动画属性同时执行,区别多个动画先后执行。一个动画需要共享开始时间/结束时间/Interpolator等等设定,PropertyValuesHolder不能有先后次序执行动画了。
区别 AnimationSet,只能通过设置单个动画的 setStartOffset 来延迟时间进行先后执行顺序。
animatorSet.play(a1) / playTogether(a1,a2) / playSequentially(a1,a2)
.with(a3)
.before(a4)
.after(a5)
其中以 playXXX 开始得到 AnimatorSet.Builder 对象,然后调用with/before/after进行管理。
7.PropertyValuesHolders.ofKeyframe() 把同一个属性拆分
把一个属性拆分成多段,执行更加精细的属性动画。
Keyframe keyframe1 = Keyframe.ofFloat(0.0f, 0);
Keyframe keyframe2 = Keyframe.ofFloat(0.5f, 100);
Keyframe keyframe3 = Keyframe.ofFloat(1.0f, 80);
PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("translationX", keyframe1, keyframe2, keyframe3);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(mTextMessage, holder);
animator.start();
0x03 参考资料
感谢以下文章作者
HenCoder Android 自定义 View 1-6:属性动画 Property Animation(上手篇)
Android:这是一份全面 & 详细的补间动画使用教程