属性动画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毫秒内动画float值从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,先加速增加后减速增加。

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

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

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

这是效果图:

横向移动

简单分析得知,由于效果是一个小球从左边移动一段距离后,变化的值只有小球圆心的X轴坐标。所以可以利用ValueAnimator产生从开始位置到结束位置的一系列中间值,在监听器中把每次改变的值设置给代表小球圆心X轴坐标的变量,再通知view重新绘制。我们看看这样会不会产出想要的动画效果。

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) {
            // 获取到动画每次该变得float值,赋值给xpoint
            XPoint = (Float)animation.getAnimatedValue();
            // 通知view重绘
            invalidate();
        }
    });
    animator.start();
}

以上代码首先创建一个从60变化到600的ValueAnimator对象,接着设置动画时间为2000毫秒、重播方式为从头开始播放、重播次数为无限和速度变化情况为线性变化,最后增加监听器,获取每次变化的值赋值给xPoint,接着很重要的一点,调用invalidate()方法通知View重绘,即float值每次改变都需要View重绘。
这样就很方便的根据float值的改变,给view增加了动画的效果。

三.TypeEvaluator

前面的例子,创建ValueAnimator的时候,都是使用的ValueAnimator.ofFloat(float, float)方法,这个方法传递的参数为可变参数,可以传递多个float值。其实创建ValueAnimator也可以使用ofInt等方法,这里有一个非常重要的方法:ValueAnimator.ofObject(TypeEvaluator, Object...),此方法和其他方法不同之处在于第一个参数TypeEvaluator,此处需要使用系统已经实现好的或自定义子类,用于设定自定义类型。

在使用ofInt或ofFloat方法时,内部其实是使用了FloatEvaluator、FloatArrayEvaluator`、IntEvaluator、IntArrayEvaluator这些系统已经实现好了的TypeEvaluator。我们使用这些方法创建ValueAnimator时就不必自定义类来继承TypeEvaluator。

下面我们试试自定义一个TypeEvaluator,实现我们想要的类型。

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

这是效果图:

斜着移动

分析得知,和上面的横向移动不同,斜着移动改变的是圆心的坐标,包括x轴和y轴坐标,我们可以自定义或使用系统提供的Point来表示一个二维坐标下的点,实现TypeEvaluator接口,根据动画完成的比例,返回一个具体代表点坐标的Point对象。

自定义类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参数和下文的TimeInterpolator接口方法返回的值有关,可以理解为动画执行的完成程度,比如动画总时间为3000毫秒,现在执行了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(可以自定义类型)。我们使用ValueAnimator.ofObject方法创建了一个ValueAnimator对象,并且传递给了两个Point对象,表示需要改变的Point值得范围,接着设置动画的执时间为2000毫秒、重播方式为从头开始播放、重播次数为无限和速度变化情况为线性变化,最后增加监听器,获取每次变化的值赋值给mPoint ,接着通知view重绘。

这个例子,我们可以知道ValueAnimotor操作的值的类型是任意的,可以由我们来自定义,只要自定义类实现TypeEvaluator,并且实现此接口的唯一一个方法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函数的返回值传递给了fraction,最好让此函数返回从0~1的值,并且递增,这样意义比较明确:代表动画完成的程度。自定义TypeEvaluator中也会比较好处理。

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的步骤。

下面我们实现一个以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实现。

这是效果图:

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

这是完整的代码:

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使颜色值变化的动画属性,都添加监听器以实现对成员变量的修改,重绘View,最后创建AnimatorSet对两个动画进行叠加,在播放移动动画的同时播放颜色渐变的动画。

下篇文章为属性动画ObjectAnimator在自定义View中的使用(计划),是更为常用的ObjectAnimator的使用。

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

推荐阅读更多精彩内容