属性动画 ValueAnimator 在自定义 View 中的使用


功能强大的属性动画(property animation)

最近在学习有关自定义 View 的内容,在 Github 上看到好多开源的 View 控件,如果涉及动画,基本上都使用的是功能更加强大的属性动画,真心觉得属性动画比之前的补间动画强大太多饿了,也学到了使用属性动画自定义 View 的方便和强大。所以想记录一下在自定义 View 时,使用属性动画的几个方面。

属性动画的强大之处在于可以对任意对象的任意属性增加动画效果,并且可以自定义值的类型和变化过程(TypeEvaluator)和过渡速度(Interpolator)。

这篇文章先来看看 ValueAnimator 的使用方法。

ValueAnimator

ValueAnimator 是属性动画的核心类,最常用的 ObjectAnimator (下篇会讲到)就是它的子类。此类只是以特定的方式(可以自定义)对值进行不断的修改,已达到某种想要的过渡效果。此类提供设置播放次数、动画间隔、重复模式、开始动画以及设置动画监听器的方法。

看一个最简单的例子吧。

ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f);
animator.setDuration(3000);
animator.setInterpolator(new LinearInterpolator());
animator.start();

以上代码先使用 ofFloat() 方法接收 0 和 1 参数初始化了一个 ValueAnimator 对象,接着设置动画播放的时间,设置变化速率为系统提供的线性变化,最后启动动画。

达到的效果是,在3000毫秒内从0线性增加到1。我们试着打印出这些值。

animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        float value = (Float)animation.getAnimatedValue();
        Log.d(TAG, value);
    }
});
animator.start();

结果打印从0到1 线性 变化的值,并且耗时3000毫秒。如果我们不设置 Interpolator,会调用默认的 Interpolator,先加速增加后减速增加。

这些值对于动画有什么用呢?我们可以添加监听器获取每次改变的值,并且可以把此值用在改变 view 的某些属性上,从而达到动画效果。

怎样使用ValueAnimator自定义View动画?

当然,上面的代码只是对数字的变化的操作,并没有涉及到动画效果。接下来我们通过在动画开始前(start()方法)设置监听器来让自定义 View 做出相应的动画。

如果想要做出如下图所示的效果,使用 ValueAnimator 就特别简单。

valueanimator1 横向移动gif

由于效果是一个小球从左边移动一段距离后,重复执行。变化的值只有小球圆心的X轴坐标。所以可以利用 ValueAnimator 产生从开始位置到结束位置的一系列中间值,设置小球移动的动画。

直接上代码: onDraw() 方法:根据 XPoint (x轴坐标)绘制圆形。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawCircle(XPoint, heightSpecSize / 2, 30, mPaint);
}

对外提供开始动画的 start() 方法:创建 ValueAnimator 对象,以及各种属性,添加监听器把每次改变的值赋值给 xPoint,并且通知 view 重绘,最后开始动画。

public void start() {
    final ValueAnimator animator = ValueAnimator.ofFloat(60, 600);
    animator.setDuration(2000);
    animator.setRepeatCount(ValueAnimator.INFINITE);
    animator.setRepeatMode(ValueAnimator.RESTART);
    animator.setInterpolator(new LinearInterpolator());
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            XPoint = (Float)animation.getAnimatedValue();
            invalidate();
        }
    });
    animator.start();
}

以上代码首先创建一个从 60 变化到 600 的 ValueAnimator 对象,接着设置动画时间、重播方式、重播次数和速度变化情况,最后增加监听器,每次获取变化的值再赋值给 xPoint ,接着很重要的一点,调用 invalidate() 方法通知 View 重绘,即值每次改变都需要 View 重绘。

这样就很方便的根据属性动画控制 float 值的改变,给 view 增加了动画的效果。

TypeEvaluator

前面的例子,创建 ValueAnimator 的时候,都是使用的 ValueAnimator.ofFloat(float, float) 方法,这个方法传递的参数为可变参数。其实创建 ValueAnimator 也可以使用 ofInt() 等方法,得到 Int 值得改变动画。 在使用 ofInt() 或 ofFloat() 方法时,其实是使用了 FloatEvaluator、FloatArrayEvaluator、IntEvaluator、IntArrayEvaluator 这些系统已经实现好了的 TypeEvaluator。我们使用这些方法创建 ValueAnimator 时就不必自定义类来继承 TypeEvaluator。

还有一个很重要的方法: ValueAnimator.ofObject(TypeEvaluator, Object…),此方法和其他方法不同之处在于参数 TypeEvaluator 和 Object,需要我们自己去实现,此处需要使用系统已经实现好的或自定义子类,用于设定自定义类型。

假如现在需要这个圆形像下图这样斜着移动,使用 ValueAnimator 该怎样实现?当然很很多简单的实现方法,比如 Path 路径的使用,这里为了演示自定义 TypeEvaluator,使用自定义 TypeEvaluator 的方式来实现。

valueanimator2

斜着移动gif

自定义类 PointEvaluator 实现 TypeEvaluator 接口。

class PointEvaluator implements TypeEvaluator{

    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        Point startPoint = (Point) startValue;
        Point endPoint = (Point) endValue;

        int x = (int) (startPoint.x + fraction * (endPoint.x - startPoint.x));
        int y = (int) (startPoint.y + fraction * (endPoint.y - startPoint.y));
            
        return new Point(x, y);
    }
}

这里需要实现 evaluate() 方法,根据动画完成的百分比返回对应的值。其中 fraction 参数和动画时间有关,一般代表动画执行的完成程度,比如动画总时间为 2000 毫秒,现在执行了1000 毫秒,那么此刻传递进来的 fraction 参数值为二分之一。

在 onDraw() 方法中依然还是简单的绘制一个圆形,此圆的圆心坐标是成员变量 mPoint 的 x , y值。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawCircle(mPoint.x, mPoint.y, 30, mPaint);
}

最后提供 start() 方法开始动画。

public void start() {
    final ValueAnimator animator = ValueAnimator.ofObject(new PointEvaluator(), 
        new Point(30, 30), new Point(600, 600));
    animator.setDuration(2000);
    animator.setRepeatCount(ValueAnimator.INFINITE);
    animator.setRepeatMode(ValueAnimator.REVERSE);
    animator.setInterpolator(new LinearInterpolator());
    animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mPoint = (Point)animation.getAnimatedValue();
            invalidate();
        }
    });
    animator.start();
}

和上边的代码类似,只是 ValueAnimotor 操作的值从 float 改变成了 Point (可以自定义类型),不再做过多的解释了。

这个例子,我们可以知道 ValueAnimotor 操作的值的类型是任意的,可以由我们来自定义,只要自定义类实现 TypeEvaluatorb,并且实现此接口的唯一一个方法 evaluate() 即可。

TimeInterpolator

TimeInterpolator 表示动画的速率,上边代码中我们就设置了动画速率,只不过使用的是API中已经实现好了的 LinearInterpolator。

查询API知道 TimeInterpolator 接口有很多已知的实现类,比如 AccelerateDecelerateInterpolator 表示先加速后减速, AccelerateInterpolator 表示一直加速, DecelerateInterpolator 表示一直加速等。 BounceInterpolator 可以模拟物理规律,实现反弹的效果

如果不设置 setInterpolator(),那么默认使用 AccelerateDecelerateInterpolator。

在自定义 TimeInterpolator 之前,我们先看看API中提供的实现的例子:LinearInterpolator,AccelerateInterpolator。

TimeInterpolator 接口,只有一个方法:getInterpolation(float input),此方法接收一个 float 类型的 input 值,此值的变化范围为0~1,并且根据动画运行的时间,均匀增加,和 TypeEvaluator 接口方法中的参数 fraction 很像, fraction 也是根据动画运行的时间,均匀增加。

注意:getInterpolation() 方法的返回值传递给了 TypeEvaluator 的 fraction 参数

LinearInterpolator 源码:

public float getInterpolation(float input) {
    return input;
}

从源码中,可以看到 getInterpolation 的逻辑简单到不能再简单,直接返回 input,因为 input 本身表示的就是均匀增加的。

AccelerateInterpolator 源码:

public float getInterpolation(float input) {
    if (mFactor == 1.0f) {
        return input * input;
    } else {
        return (float)Math.pow(input, mDoubleFactor);
    }
}

构造函数接收一个 mFactor 表示加速的倍数,接收1.0f以上的数,mDoubleFactor = 2 * mFactor。 在 getInterpolation() 方法中,判断 mFactor 如果等于1.0f,直接返回 input * input(默认,二次函数增长),否则返回 input 的 mDoubleFactor 次方(mDoubleFactor 次函数增长)。

看来要想实现一个自定义的 TimeInterpolator,得要有一些必要的数学修养了。没办法,数学没有那么好,只能实现一个简单的 TimeInterpolator,演示自定义 TimeInterpolator 的步骤。

注意:最好让 getInterpolation() 方法返回的值在0~1之间,并且递增。以便在使用fraction参数时意思明确

下面我们实现一个以10给底数的负指数函数减速的例子:

    class LgDecelerateInterpolator implements TimeInterpolator {

        private float background;

        public LgDecelerateInterpolator() {
            background = 10;
        }

        @Override
        public float getInterpolation(float input) {
            return (1 - (float) Math.pow(background, -input));
        }
    }

然后在设置 animator.setInterpolator(new LgDecelerateInterpolator());,就可以使用了。

成员变量 background 表示底数,在构造方法中初始化为 10,因为是减速,所以用到了负指数,得到的值从 1 变化到 0 ,所以再用1减去这个结果值,就得到了最终的结果。

AnimatorSet

AnimatorSet 表示动画的集合,可以把几个动画一起播放,或按次序播放。提供 paly、with、after 等方法。

接下来,把以上用到的全部结合起来,播放一个动画的效果:圆形从 View 的左上角移动到右下角,伴随着颜色的变化,移动速度和颜色变化的速率都由上面自定义的 LgDecelerateInterpolator 实现。

效果图:

valueanimator3

斜着指数减速移动伴随颜色指数渐变 gif

完整的代码:

public class MyView extends View {

    private Paint mPaint;
    private Point mPoint;
    private int mColor;

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    public MyView(Context context) {
        super(context);
        initPaint();
    }

    private void initPaint() {
        mPaint = new Paint();
        mPaint.setColor(0xFFF00000);
        mPaint.setAntiAlias(true); // 抗锯齿
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawCircle(mPoint.x, mPoint.y, 60, mPaint);
    }

    public void start() {
        final ValueAnimator animator = ValueAnimator.ofObject(new PointEvaluator(),
                new Point(60, 60), new Point(990, 1050));
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mPoint = (Point) animation.getAnimatedValue();
                invalidate();
            }
        });

        final ValueAnimator animator1 = ValueAnimator.ofArgb(0xFFF00000,0xFFFFFF00);
        animator1.setRepeatCount(ValueAnimator.INFINITE);
        animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mColor = (int) animation.getAnimatedValue();
                mPaint.setColor(mColor);
            }
        });

        AnimatorSet animationSet = new AnimatorSet();
        animationSet.setDuration(3000);
        animationSet.setInterpolator(new LgDecelerateInterpolator());

        animationSet.play(animator).with(animator1);
        animationSet.start();
    }

    class PointEvaluator implements TypeEvaluator {

        @Override
        public Object evaluate(float fraction, Object startValue, Object endValue) {
            Point startPoint = (Point) startValue;
            Point endPoint = (Point) endValue;

            int x = (int) (startPoint.x + fraction * (endPoint.x - startPoint.x));
            int y = (int) (startPoint.y + fraction * (endPoint.y - startPoint.y));

            return new Point(x, y);
        }
    }

    class LgDecelerateInterpolator implements TimeInterpolator {

        private float background;
        public LgDecelerateInterpolator() {
            background = 10;
        }

        @Override
        public float getInterpolation(float input) {
            return (1 - (float) Math.pow(background, -input));
        }
    }

}

start() 方法中创建了两个 ValueAnimator,第一个使用 ofObject() 方法,使用自定义的 PointEvaluator,第二个使用API已经实现的ofArgb 使颜色值变化的动画属性(API21以上支持),都添加监听器以实现对成员变量的修改,重绘 View,最后创建 AnimatorSet 对两个动画进行叠加,在播放移动动画的同时播放颜色渐变的动画。

评论