Android 动画解析

Android 的动画分类:

  1. View视图动画(补间动画 / 逐帧动画)
  2. 属性动画


    Android 动画

0x01 View视图动画

1.1 逐帧动画

frame-by-frame animation

帧动画是顺序播放一组预先定义好的图片,类似电影播放。

不同于补间动画,系统提供了另外一个类 AnimationDrawable 来使用帧动画。

实现方式 2 种:XML定义代码动态创建

1.1.1 实现方式1: XML定义

创建动画的 XML 文件路径:res/drawable/frame_animation.xml

根标签:animation-list

属性名称 说明
oneshot 是否仅播放一次
duration 每一帧的显示时长

XML 示例代码如下:

// 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 >

动画加载 Java 示例代码如下:

ImageView imageView  = new ImageView(this);
imageView.setImageResource(R.drawable.frame_animation);
AnimationDrawable animationDrawable = (AnimationDrawable)imageView.getDrawable();
// 播放开始
animationDrawable.start();
// 播放结束
animationDrawable.stop();
1.1.2 实现方式2: 代码动态创建

Java 示例代码如下:

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();
1.1.3 优缺点

缺点:使用大量图片,易导致OOM
建议:直接使用 gif,大小可以明显改善

1.2 补间动画

View的作用对象是 View

它支持4种动画效果,分别是 平移动画缩放动画旋转动画透明度动画

帧动画 的表现形式和上面4种变换效果不太一样,单独介绍。

View 动画的 4 种变换效果对应着 Animation 的 4 个子类:TranslateAnimationScaleAniamtionRotateAnimationAlphaAnimation

名称 XML标签 Animation子类 效果
平移动画 <translate> TranslateAnimation 移动 View
缩放动画 <scale> ScaleAniamtion 放大/缩小 View
旋转动画 <rotate> RotateAnimation 旋转 View
透明度动画 <alpha> AlphaAnimation 改变 View 的透明度
组合动画 <set> AnimationSet 多个动画的结合

实现方式 2 种:XML定义代码动态创建

对于 View 动画来说,建议采用 XML 定义动画,可读性更好。

View 动画既可以是单个动画,也可以由一系列动画组成。

<set>标签表示动画集合,对应 AnimationSet 类,它可以包含若干个动画,并且它的内部也是可以嵌套其他动画集合的。

1.2.1 实现方式1: XML定义

创建动画的 XML 文件路径:res/anim/view_animation.xml

XML 示例代码如下:

// res/anim/view_animation.xml
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="3000" // 动画时长
    android:fillAfter="true" // 动画结束后,是否留在结束位置,优先级高于fillBefore属性,默认是false
    android:fillBefore="false" //动画结束后,是否留在开始位置,默认是true。
    android:fillEnabled="true" //是否应用fillBefore的值,对fillAfter无影响,默认是true
    android:repeatCount="0" //infinite无限重复
    android:repeatMode="restart" // restart正序重播/reverse反转重播
    android:interpolator="@android:anim/overshoot_interpolator"
    android:shareInterpolator="true"  // 组合动画属性,组合动画是否和集合(<set></set>)共享一个插值器,去过不指定,子动画需要单独设定
    android:startOffset="100" //组合动画默认是全部动画同时开始,如果不同动画不同开始需要使用该属性延迟开始时间
    />
    
    <translate
     android:fromXDelta="0" // 水平x方向的起始值
     android:fromYDelta="0" // 竖直y方向的起始值
     android:toXDelta="500" // 水平x方向的结束值
     android:toYDelta="500" // 竖直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坐标
    />

    <rotate
     android:fromDegrees="0" // 开始的角度
     android:toDegrees="180" // 结束的角度
     android:pivotX="20%" // 旋转中心点 x 坐标
     android:pivotY="20%" // 旋转中心点 y 坐标
    />

    <alpha
     android:fromAlpha="1.0" // 开始透明度 0.0-1.0
     android:toAlpha="0.0" //结束透明度 0.0-1.0
    />
</set>

动画加载 Java 示例代码如下:

Animation translateAnimation = AnimationUtils.loadAnimation(this, R.anim.view_animation);
view.startAnimation(translateAnimation);

pivotX / pivotY 的取值有三个类型

  1. 数字, eg:android:pivotX="50", 该View左上角在x方向上平移50px的点,对应Java代码中设置参数 Animation.ABSOLUTE
  2. 百分比, eg:android:pivotX="50%", 该View左上角在x方向上平移自身宽度50%的点,对应Java代码中设置参数 Animation.RELATIVE_TO_SELF
  3. 百分比p (parent), eg:android:pivotX="50%p"该View左上角在x方向上平移父布局宽度50%的点,对应Java代码中设置参数 Animation.RELATIVE_TO_PARENT
1.2.2 实现方式2: 代码动态创建

Java 示例代码如下:

// 组合动画设置
// 步骤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 scale = 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(scale);

// 步骤5:播放动画
mButton.startAnimation(setAnimation);
1.2.3 优缺点

缺点:

  1. 作用对象局限;根据包分类可知 android.view.animation,只针对View进行动画操作。
  2. 只改变视觉效果,没有改变属性;例如点击只在原始位置有效(经API27测试,当属性动画View完全不可见时,点击位置和范围为原始位置)。
  3. 动画效果单一,只有四种。
1.2.4 自定义 View 动画

自定义 View 动画是一件既简单又复杂的事情。
简单,是因为派生一种新动画只需要继承Animation这个抽象类,然后重写它的 initialize 和 applyTransformation方法,在 initialize 方法中做一些初始化工作,在 applyTransformation 中进行相应的矩阵变换即可,很多时候需要采用 Camera 来简化矩阵变换的过程。
复杂,是因为自定义 View 动画的过程主要是矩阵变换的过程,而矩阵变换是数学上的概念。

1.2.5 View 动画的特殊使用场景

比如在 ViewGroup 中可以控制子元素的出场效果,在 Activity 中可以实现不同 Activity 之间的切换效果。

  1. LayoutAnimation
    LayoutAnimation 作用于 ViewGroup,为 ViewGroup 指定一个动画,这样当它的子元素出场时都会具有这种动画效果。这种效果常常被用在ListView。
    步骤如下:

    1. 定义LayoutAnimation
    2. 定义子元素具体的入场动画
    3. 为 ViewGroup 指定 android:layoutAnimation 属性
//  1  /res/anim/layout_animation
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:animation="@anim/layout_animation_item"
    android:animationOrder="normal"
    android:delay="0.5" />

//  2  /res/anim/layout_animation_item
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:shareInterpolator="true">
    
    <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0" />
    <translate
        android:fromXDelta="500"
        android:toXDelta="0" />
</set>

// 3 在布局文件的ViewGroup中使用 layoutAnimation属性
<ListView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layoutAnimation="@anim/layout_animation" />

除了在 XML 中指定 LayoutAnimation 外,还可以通过 LayoutAnimationController 来实现。

Animation animation = AnimationUtils.loadAnimation(this, R.anim.layout_animation_item);
LayoutAnimationController controller = new LayoutAnimationController(animation);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
listView.setLayoutAnimation(controller);
  1. Activity 的切换效果
    Activity 有默认的切换效果,但是这个效果我们可以通过 overridePendingTransition(int enterAnim, int exitAnim) 这个方法进行自定义,这个方法必须在 startActivity(intent) 或者 finish() 之后被调用才能生效,之前无效。

Fragment 也可以添加动画,由于 Fragment 是在 API 11 中新引入的类,因此为了兼容性我们需要使用support-v4
这个兼容包,在这个情况下我们可以通过 FragmentTransaction 中的 setCustomAnimation(int enter, int exit) 方法来添加切换动画,这个动画需要是 View 动画,之所以不能采用属性动画是因为属性动画也是API 11 新引入的。

0x02 属性动画

属性动画框架类图
PropertyValuesHolder

属性动画是 API 11 新加入的特性,和 View 动画不同,它对作用对象进行了扩展,属性动画可以对任何对象做动画,甚至还可以没有对象。

属性动画中有 ViewPropertyAnimatorValueAnimatorObjectAnimatorAnimatorSet 等概念,通过它们可以实现绚丽的动画。

ObjectAnimator extends ValueAnimator extends Animator 位于 android.animation

ViewPropertyAnimator 位于 android.view,其本质内部使用 ValueAnimator。

ViewPropertyAnimator < ObjectAnimator < ValueAnimator,使用难度越来越高,但越来越灵活。

三种使用方式举例:

// ViewPropertyAnimator
view.animate().alphaBy(0.8f) 

// ObjectAnimator
Object.ofFloat(view, "alpha", 1.0f, 0.0f, 1.0f).start();

// 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();

ValueAnimator 本身不作用于任何对象,也就是说直接使用它没有任何动画效果。它可以对一个值做动画,然后我们可以监听其动画过程,在动画过程中修改我们对象的属性值,这样就相当于我们的对象做了动画。

2.1 ViewPropertyAnimator
view.animate().alphaBy(0.8f);

为了满足面向对象编程思想,View 中引入了ViewPropertyAnimator,可以链式调用。

注意 ViewPropertyAnimator 没有 setRepeatCount() 和 setRepeatMode(),不能重复

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()

注意

  1. View#setX()没有动画渐变效果,直接将该View放到设置位置;而 ViewPropertyAnimator#x() 有平移过渡动画。
  2. ViewPropertyAnimator#x() 如果有动画执行x()将取消。例如view.x(500).translationX(50),将只执行translationX(50),不执行x(500)
  3. ViewPropertyAnimator#By() 表示在当前位置的基础上进行操作
2.2.1 实现方式1: XML定义

创建 XML 文件路径:res/animator/view_animator.xml
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>

动画加载 Java 示例代码如下:

Animator animator = AnimatorInflater.loadAnimator(context,R.animator.view_animator);
animator.setTarget(view);
animator.start();
2.2.2 实现方式2: 代码动态创建
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();
2.3 属性动画的原理

属性动画要求动画作用的对象提供该属性的 get 和 set 方法,属性动画根据外界传递的该属性的初始值和最终值,以动画的效果多次去调用 set 方法,每次传递给 set 方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终的值。

自定义属性示例:

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(); 

对 object 的属性 abc 做动画,如果想让动画生效,需要满足两个条件:

  1. object 必须提供 setAbc 方法,如果动画的时候没有传递初始值,那么还要提供 getAbc 方法,因此系统要去取 abc 属性的初始值。(如果这条不满足,程序直接Crash)
  2. object 的 setAbc 对属性 abc 的改变必须反映出来(requestLayout/invalidate),才会有效果。(如果这条不满足,动画无效果但不会Crash)

动画不生效,只满足条件1而未满足条件2,解决方法有 3 种:

  1. 给你的对象加上 get 和 set 方法,如果你有权限的话;
    这个方法最简单,加上 get 和 set 就搞定了,但针对Android SDK内部实现的View往往没有权限不可行。

  2. 用一个类来包装原始对象,间接为其提供 get 和 set 方法;

private void performAnimation(View view) {

    ViewWrapper wrapper = new ViewWrapper(view);
    ObjectAnimator.ofInt(wrapper, "width", 0)
            .setDuration(5000)
            .start();
}


private static class ViewWrapper {
    private View mTarget;


    public ViewWrapper(View target) {
        this.mTarget = target;
    }

    public int getWidth() {
        return mTarget.getLayoutParams().width;
    }

    public void setWidth(int width) {
        mTarget.getLayoutParams().width = width;
        mTarget.requestLayout();
    }
}
  1. 采用 ValueAnimator,监听动画过程,自己实现属性的改变;
private void valueAnimation(View targetView, int start, int end) {
    ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

        private IntEvaluator evaluator = new IntEvaluator();

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            float fraction = animation.getAnimatedFraction();
            Integer curValue = evaluator.evaluate(fraction, start, end);
            targetView.getLayoutParams().width = curValue;
            targetView.requestLayout();
        }
    });
    valueAnimator.setDuration(5000).start();
}
2.4 时间插值器 TimeInterpolator / 类型估值器 TypeEvaluator

TimeInterpolator:时间插值器
根据 时间流逝的百分比 来计算 当前属性值改变的百分比

TypeEvaluator:类型估值算法
根据 当前属性值改变的百分比 来计算改变后的 属性值

2.4.1 时间插值器 TimeInterpolator

android.animation.TimeInterpolator
android.view.animation.Interpolator

Interpolator extends TimeInterpolator

属性动画内部使用的是 TimeInterpolator,补间动画内部使用的是 Interpolator。

TimeInterpolator 接口是属性动画中新增的,用于兼容Interpolator 接口,这样原先 Interpolator 的实现类就可以直接在属性动画中使用

列举官方插值器:

Java类 描述
LinearInterpolator 匀速
AccelerateInterpolator 加速
DecelerateInterpolator 减速
AccelerateDecelerateInterpolator 先加速后减速(默认)
AnticipateInterpolator 先退后然后加速前进
OvershootInterpolator 完成动画,超出后回到结束位置
AnticipateOvershootInterpolator 先退后再加速前进,超出终点后再回终点
BounceInterpolator 最后阶段弹球效果
CycleInterpolator 周期运动
PathInterpolator 自定义动画完成度/时间完成度曲线(0-1)
FastOutLinearInInterpolator 加速
LinearOutSlowInInterpolator 持续减速
FastOutSlowInInterpolator 先加速再减速

最后三个是Android5.0(API21)新增,和之前的类似,但是轨迹稍有区别

代码举例说明:

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值 = 运算后的值 = 先加速后减速
        // 所以该差值器是先加速再减速的
    }  
}
2.4.2 类型估值器 TypeEvaluate

根据当前属性值变化的百分比初始值结束值来计算当前属性的具体数值。当前属性值 = 初始值 + (结束值 - 初始值) * 百分比

实现

  1. 实现TypeEvaluator<T> 接口
  2. 重写public T evaluate(float fraction, T startValue, T endValue) 方法

官方示例

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);
    }
}

完整示例

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();
2.5 属性动画的监听器

属性动画提供了监听器用于监听动画的播放过程,主要:AnimatorUpdateListener、AnimatorPauseListener和 AnimatorListener。

Animator
public static interface AnimatorListener {
    default void onAnimationStart(Animator animation, boolean isReverse) {
        onAnimationStart(animation);
    }
    default void onAnimationEnd(Animator animation, boolean isReverse) {
        onAnimationEnd(animation);
    }
    void onAnimationStart(Animator animation);
    void onAnimationEnd(Animator animation);
    void onAnimationCancel(Animator animation);
    void onAnimationRepeat(Animator animation);
}

public static interface AnimatorPauseListener {
    void onAnimationPause(Animator animation);
    void onAnimationResume(Animator animation);
}

为了便于开发,系统还提供了 AnimatorListenerAdapter 这个类,它是AnimatorListener 和 AnimatorPauseListener 的适配器类,这样我们就可以有选择的实现上面的6个方法。

ValueAnimator extends Animator

public static interface AnimatorUpdateListener {
    void onAnimationUpdate(ValueAnimator animation);
}

AnimatorUpdateListener 比较特殊,它会监听整个动画过程,动画是由许多帧组成的,每播放一帧,onAnimationUpdate 就会被调用一次。

2.5.1 添加监听器

ViewPropertyAnimator # setListener() / setUpdateListener()
ObjectAnimator # addListener() / addUpdateListener / addPauseListener()
VauleAnimator # addListener() / addUpdateListener / addPauseListener()

2.5.2 移除监听器

ViewPropertyAnimator # setListener(null) / setUpdateListener(null) 填null来移除
ObjectAnimator # removeListener() / removeUpdateListener() / removePauseListener()
VauleAnimator # removeListener() / removeUpdateListener() / removePauseListener()

ObjectAnimator 支持pause()方法暂停
ViewPropertyAnimator 不支持setRepeatMode() / setRepeatCount() 方法
ViewPropertyAnimator 独有withStartAction(Runnable runnable) 和 withEndAction(Runnable runnable) 方法,可设置一次动画开始或结束的监听。即使重新开始动画,也不会回调,是一次性的。而AnimatorListener是持续有效的。
withEndAction() 只有在动画正常结束才会调用,而在动画被取消时是不会执行的。而 AnimatorListener.onAnimationEnd() 在取消之后也会被调用,在调用 onAnimationCancel()之后调用

2.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

2.7 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进行管理。

2.8 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 使用动画的注意

  1. OOM问题
    主要出现在帧动画中,但图片数量过多且图片较大时,极易出现OOM,尽量避免使用帧动画

  2. 内存泄漏
    属性动画 中有一类无限循环的动画,这类动画需要在Activity退出时及时停止,否则将导致Activity无法释放从而造成内存泄漏。
    View动画 不存在这个问题。

  3. 兼容性问题
    动画在 3.0 以下的系统上(API 11)有兼容性问题,在某些特殊场景可能无法正常工作,因此要做好适配

  4. View动画的问题
    View动画是对View的影响做动画,并不是真正地改变View的状态,因此有时候会出现动画完成后View无法隐藏的现象,即setVisibility(View.GONE)失效,这时只需调用view.clearAnimation()清除View动画即可解决此问题。

  5. 不要使用px
    在进行动画的过程中,要尽量使用dp。使用px会导致在不同的设备上有不同的效果。

  6. 动画元素的交互
    将 view 移动(平移)后,在Android 3.0 以前的系统上,不管是 View动画 还是 属性动画,新位置 均无法触发单击事件,同时,老位置仍然可以触发单击事件。View只是在视觉上不存在了,移动回原位置后,单击事件继续生效。
    从3.0开始,属性动画的单击事件触发位置为移动后的位置,但View动画仍然在原位置。

  7. 硬件加速
    在使用动画的过程中,开启硬件加速,会提高动画的流畅性。
    但不可滥用!
    1.硬件层会比普通view绘制多做很多的工作。首先将view绘制到GPU的一个层中,然后GPU再把这个层绘制到window上。
    2.与所有的缓存一样,GPU的硬件缓存也会有失败几率。如果动画进行中调用 invalidate(),缓存的层会不得不重新渲染。如果不断有无效的硬件层产生的话,还不如不使用硬件加速。因为增加硬件层缓存会增加额外的开销。
    简单的view绘制 使用硬件加速会增加不必要的开销,不建议开启。
    使用示例:

// 设置硬件加速 
myView.setLayerType(View.LAYER_TYPE_HARDWARE, null);

// 设置动画
ObjectAnimator animator = ObjectAnimator.ofFloat(myView, View.TRANSLATION_X, 150);

// 设置一个回调,在动画完成时取消硬件加速
animator.addListener(new AnimatorListenerAdapter() {  
  @Override
  public void onAnimationEnd(Animator animation) {
    myView.setLayerType(View.LAYER_TYPE_NONE, null);
  }
});

// 开始动画
animator.start();  

在API 14以上使用属性动画,可以更简洁:

myView.animate()  
  .translationX(150)
  .withLayer()
  .start();

0x04 参考资料

感谢以下文章作者
HenCoder Android 自定义 View 1-6:属性动画 Property Animation(上手篇)
Android:这是一份全面 & 详细的补间动画使用教程
Android开发艺术探索 第7章 Android动画深入分析
Android 属性动画详解与源码分析

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容