Android动画详解(下)

本篇文章主要介绍属性动画,需要了解补间动画和帧动画相关知识的,建议阅读Android动画详解(上)。属性动画非常强大,运用也非常灵活,为了便于理解,本文首先从类的角度介绍了属性动画的继承关系,然后针对一些重点类介绍了其内的主要方法,最后通过demo的方式对属性的动画的常见用法进行了演示。

类继承关系

属性动画存放在android.animation包下,主要的类继承关系如下:
Android动画详解(下)

主要方法

Animator是一个抽象类,其内提供了一些公共方法。需要注意的是,一些方法是需要子类去重写的,比如getInterpolator()这个方法目前直接返回null,子类需要根据实际情况返回Interpolator。
Android动画详解(下)
ValueAnimator是属性动画中非常常用的一个类,除了实现或重写了父类的上述方法外,其还增加了一些新的功能,如常用的估值器的设置。
Android动画详解(下)
AnimatorSet主要用来控制动画组合的播放顺序及方式,特别是其的Builder,功能强大,使用方便。
Android动画详解(下)

常见用法

属性动画的用法非常灵活,首先从简单的平移动画开始入手,效果如如下:
Android动画详解(下)

代码比较简单,老规矩,介绍xml和Java两种实现方式:
xml实现方式:

<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="translationX"
android:duration="2000"
android:valueFrom="0"
android:valueTo="500"
android:repeatCount="1"
android:repeatMode= "reverse"
 />

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该如何实现呢?方案很多,为了对比学习,挑选了三种典型的方案,效果图如下:
Android动画详解(下)

从图中可以看到红、蓝、绿三个View的变化效果一致,但是正如其显示的内容一样,使用了不同的方式,分别为ObjectAnimator.ofInt、ValueAnimator.ofInt 、ValueAnimator.ofObject。

ObjectAnimator.ofInt 方式

该方式是处理这种没有对应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();

ValueAnimator.ofInt 方式

该方式是属性动画运用的通用方式,重要性较高,对一些效果复杂或多个动画关联调用的情况处理比较有优势,其核心是动画回调的运用。核心代码如下:

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

首先,如果动画存在且正在执行则返回。如果动画不存在则创建动画,并设置监听,在监听中动态改变布局参数并刷新布局。最后启动动画。

ValueAnimator.ofObject方式

该方式主要是为了演示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 <WidthObject>{
    @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的使用

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;效果如下:
Android动画详解(下)

自定义控件switcher

除了上述知识点之外,属性动画还有一个非常重要的知识点那就是Interpolator,下面通过一个常用的自定义控件switcher来演示Interpolator的用法。先上一个慢放的效果图:
Android动画详解(下)

先处理我们比较熟悉的绘制过程,很明显这个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.

  • 当进行程度为0时,AnimatedValue为0,为起始状态
  • 当进行程度为0.34f时,AnimatedValue为1,为移动过程1
  • 当进行程度为0.67f时,AnimatedValue为2,为移动过程2
  • 当进行程度为1.0f时,AnimatedValue为3,为结束状态

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的理解。这两篇文章对应的代码都放在了一个项目里,有需要的可以下载。项目地址