换个思路来看Android属性动画

一句话总结属性动画:

根据属性值变化区间和持续时间,在每个时间点确定一个值,调用动画作用对象的某条属性的set方法,修改该属性的值,具体想要这个这个属性值改变之后,动画效果怎么表现,需要在作用对象的作用属性的set方法或者动画的UpdateListener数据更新监听中自定义。

也就是说一个属性动画的并不能确定具体的动画效果是什么样子的,还需要配合动画作用的对象才能确定最终的动画表现
属性动画所定义的只是 作用对象作用属性作用持续时间变化范围内的变化

以下会有两个例子帮助理解这句话

一.从简单的例子开始

这里直接给出使用属性动画实现的简单例子,可以帮助建立起属性动画的使用方法主线,后续在这条主线的基础上补充分支
例子一:抖动的Hello World

抖动的Hello World

首先在Activity的布局文件中添加一个TextView

    

然后在Activity中设置属性动画:

    public void initTextAnimator(){
        textView=findViewById(R.id.tv_text);
        //ofFloat(Object target, String propertyName, float... values)
//第一个参数为object类型的target,这里是textview
//第二个参数是要做动画的属性值
//最后一个可以传入任意个数的参数,动画在他们数值之间过渡
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(textView, "rotation", 0f, 20f, -20f, 20f, -20f, 0f);
        //动画时长
        objectAnimator.setDuration(3000);
        //动画重复次数
        objectAnimator.setRepeatCount(2);
        //动画重复模式
        objectAnimator.setRepeatMode(ValueAnimator.RESTART);
        //设置动画插值器
        objectAnimator.setInterpolator(new AccelerateInterpolator());
        //启动动画
        objectAnimator.start();
    }

onCreate中调用initTextAnimator()就可以实现以上动画效果
现在简单分析:
这个属性动画的作用对象:TextView
作用属性:rotation
作用变化范围:0 → 20 → -20 → 20 → -20 → 0
作用时间:3000毫秒
重复次数:2
重复模式:从头开始
插值器:先慢后快

也就是说在3秒的时间内,rotation旋转属性要从0到20再到-20...最后到0,所以我们就可以看到Hello World开始摇摆

但其实rotation属性并不在TextView中,而是在它的父类View中,我们可以在View中找到rotation的set方法

    public void setRotation(float rotation) {
        if (rotation != getRotation()) {
            // Double-invalidation is necessary to capture view's old and new areas
            invalidateViewProperty(true, false);
            mRenderNode.setRotationZ(rotation);
            invalidateViewProperty(false, true);

            invalidateParentIfNeededAndWasQuickRejected();
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
    }

例子二:画个圈(需要一点点自定义View的前置知识,花20分钟即可了解,推荐文章https://www.jianshu.com/p/705a6cb6bfee)

这是我们想要的效果:

一个圈

怎么通过属性动画来实现这个画个圈的效果呢?
这里我自定义了一个MyCircleView
首先我们分析以下画这个圈是通过画弧来实现的,每一帧这个弧度都会增长一点,从0到360,最终完成一个完整的圈
所以在MyCircleView中增加一个属性sweepAngle 并为它添加get和set方法

public class MyCircleView extends View {

    //画笔
    private Paint paint;
    //绘制弧度
    int sweepAngle;
    public int getSweepAngle() {
        return sweepAngle;
    }

    public void setSweepAngle(int sweepAngle) {
        this.sweepAngle = sweepAngle;
    }
...其他代码省略...
}

这里的弧度sweepAngle 就是我们接下来定义的属性动画要作用的属性,当然MyCircleView就是属性动画要作用的对象了,作用的属性一定要有set方法,否则会报错。

接下来我们在onDraw(Canvas canvas)根据弧度画出圆弧:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //获取绘制的View的宽度
        int width = getWidth();
        //获取绘制的View的高度
        int height = getHeight();
        paint.setAntiAlias(true);//设置抗锯齿
        paint.setStyle(Paint.Style.STROKE);  // 画弧线,画笔样式设置为空心
        paint.setStrokeWidth(5);//设置线宽
        paint.setColor(Color.BLUE);//画笔颜色为blue
        // 设置矩形区域 drawArc会画出于次矩形区域内切的弧(正方形是正圆弧,长方形是椭圆弧)
        RectF rectF = new RectF(0, 0, width, width);
        //画弧(矩形区域,起始角,弧度,是否经过圆心,画笔)
        canvas.drawArc(rectF, 150, sweepAngle, false, paint);  // 第四个参数 userCenter为true,表示轨迹经过圆心
    }

以上代码最主要的其实只是最后一句:
canvas.drawArc(rectF, 150, sweepAngle, false, paint);
根据sweepAngle初始值画出弧度(目前为0)

接下来才开始用到属性动画,定义一个播放动画的方法showAnim

    //传入参数 想要变化到的弧度toSweepAngle
    public void showAnim(int toSweepAngle){
        //定义属性动画ObjectAnimator.ofInt(作用对象, 作用属性(int型), 属性值变化区间);
        ObjectAnimator mSweepAnimator = ObjectAnimator.ofInt(this, "sweepAngle", new int[]{0, toSweepAngle});
        //设置动画时间为5000ms 也就是5秒
        mSweepAnimator.setDuration(3000);
        //开始播放
        mSweepAnimator.start();
    }

然后我们知道,属性动画就是调用的set方法改变某属性的值,所以我们需要在sweepAngle的set方法中,重新绘制View,使这个值的变化体现出来

    public void setSweepAngle(int sweepAngle) {
        this.sweepAngle = sweepAngle;
        //重绘View
        postInvalidate();
    }

现在简单分析:
这个属性动画的作用对象:MyCircleView
作用属性:sweepAngle
作用变化范围:0 → 360
作用时间:3000毫秒

也就是说sweepAngle在3秒内从0连续变化到了360,并且每变化一次,MyCircleView都会进行重绘,体现出变化的效果

接下来我们在MainActivity中调用showAnim方法就可以看到动画了

   public void initProgressBar(){
       myCircleView =findViewById(R.id.my_circle_view);
       //设置点击监听 点击时播放动画
       myCircleView.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               myCircleView.showAnim(360);
           }
       });
   }

以下是完整的MyCircleView代码,可以直接使用

public class MyCircleView extends View {

    //画笔
    private Paint paint;
    //绘制弧度
    int sweepAngle;

    public MyCircleView(Context context) {
        super(context);
        initView();
    }

    public MyCircleView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public MyCircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public MyCircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initView();
    }


    private void initView() {
        paint = new Paint();
        sweepAngle=0;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //这里只处理了wrap_content 时 默认为大小300
        if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, 300);
        } else if (widthMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(300, heightSize);
        } else if (heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize, 300);
        }
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //获取各个编剧的padding值
        int paddingLeft = getPaddingLeft();
        int paddingRight = getPaddingRight();
        int paddingTop = getPaddingTop();
        int paddingBottom = getPaddingBottom();
        float strokeWidth=5;//圆圈线宽
        //获取绘制的View的宽度
        int width = getWidth();
        //获取绘制的View的高度
        int height = getHeight();
        paint.setAntiAlias(true);//设置抗锯齿
        paint.setStyle(Paint.Style.STROKE);  // 画弧线,画笔样式设置为空心
        paint.setStrokeWidth(5);//设置线宽
        paint.setColor(Color.BLUE);//画笔颜色为blue
        // 设置矩形区域 drawArc会画出于次矩形区域内切的弧(正方形是正圆弧,长方形是椭圆弧)
        RectF rectF = new RectF(paddingLeft+strokeWidth/2, paddingTop+strokeWidth/2, width-paddingRight-strokeWidth/2, width-paddingBottom-strokeWidth/2);
        //画弧(矩形区域,起始角,弧度,是否经过圆心,画笔)
        canvas.drawArc(rectF, 150, sweepAngle, false, paint);  // 第四个参数 userCenter为true,表示轨迹经过圆心
    }




    //展示进度条动画
    public void showAnim(int toSweepAngle){
        //重置弧度为0
        sweepAngle=0;
        //定义属性动画ObjectAnimator.ofInt(作用对象, 作用属性(int型), 属性值变化区间);
        ObjectAnimator mSweepAnimator = ObjectAnimator.ofInt(this, "sweepAngle", new int[]{0, toSweepAngle});
        //设置动画时间为5000ms 也就是5秒
        mSweepAnimator.setDuration(3000);
        //开始播放
        mSweepAnimator.start();
    }

    public int getSweepAngle() {
        return sweepAngle;
    }

    public void setSweepAngle(int sweepAngle) {
        this.sweepAngle = sweepAngle;
        postInvalidate();
    }
}

二.主线上的一些扩展分支

通过以上两个例子,我们可以掌握属性动画的主线工作原理 即:

根据某条属性值变化区间和持续时间,该属性在持续时间内连续变化,调用动画作用对象的该属性的set方法,修改该属性的值

但是目前我们只是掌握了主干上的实现原理,就像一颗光秃秃的树干,接下来要学习更多的分支内容,使这个树干长出树枝,树叶,才能把属性动画做的漂亮

2.1 ValueAnimator和ObjectAnimator

Animator类提供了创建动画的基本组成,通常不直接使用这个类而是用ValueAnimator和ObjectAnimator来创建属性动画。
这二位有什么区别?
ValueAnimator
是整个属性动画机制当中最核心的一个类。它使用一种时间循环的机制来计算值与值之间的动画过渡,负责管理动画的播放次数、播放模式、设置动画设置监听器、设置自定义类型等。有两块动画属性:计算动画值和设置这些对象或属性的动画。ValueAnimator不执行第二个,所以你必须设置ValueAnimator更新值和修改对象的监听。ValueAnimator不会自动调用set方法,甚至在创建动画实例的时候可以不定义ValueAnimator的作用属性,但是必须在UpdateListener动画状态的监听中调用作用对象的作用属性的set方法

因为ValueAnimator并不直接作用于对象或属性,通常会通过这些计算的值来不断改变动画的对象,可以通过设置监听器并调用getAnimatedValue()来不断获得帧刷新的计算值

  animator.setDuration(200);
  animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float) animation.getAnimatedValue();
                Log.e("TTT", "value is " + value);
            }
        });
  animator.start();

ObjectAnimator
是ValueAnimator的一个子类,ObjectAnimator可以对任意对象及对象属性设置动画。通常情况下,使用ObjectAnimator更多一些因为它使目标对象动画过程更简单,然而因为ObjectAnimator有更多的限制,有时候使用ValueAnimator更合理一些,比如需要特定的acessor方法出现在目标对象。

2.2 ValueAnimator/ObjectAnimator常用方法

/**
 * 设置动画时长,单位是毫秒
 */  
setDuration(long duration)  
/**
 * 获取ValueAnimator在运动时,当前运动点的值
 */  
Object getAnimatedValue();  
/**
 * 开始动画
 */  
void start()  
/**
 * 设置循环次数,设置为ValueAnimator.INFINITE表示无限循环
 */  
void setRepeatCount(int value)  
/**
 * 设置循环模式
 * value取值有RESTART,REVERSE,
 */  
void setRepeatMode(int value)  
/**
 * 取消动画
 */  
void cancel()  

2.3 插值器Interpolator

什么是插值器?

Interpolator是时间插值器,用来修饰动画效果,它可以指定属性值如何随时间变化的,反应了动画的运动速率,运动速率可以是线性变化的(如匀速)也可以是非线性变化的(如加速、减速)

比如例子二中,弧度在3秒内从0到360是一个匀速的线性变化,那我现在想要画圆先快后慢,需要怎么做?这个时候就需要用到插值器了
使用public void setInterpolator(TimeInterpolator value)方法,为动画添加一个插值器DecelerateInterpolator即可

    //展示进度条动画
    public void showAnim(int toSweepAngle){
        //重置弧度为0
        sweepAngle=0;
        //定义属性动画ObjectAnimator.ofInt(作用对象, 作用属性(int型), 属性值变化区间);
        ObjectAnimator mSweepAnimator = ObjectAnimator.ofInt(this, "sweepAngle", new int[]{0, toSweepAngle});
        //设置动画时间为5000ms 也就是5秒
        mSweepAnimator.setDuration(3000);
        //设置插值器
        mSweepAnimator.setInterpolator(new DecelerateInterpolator());
        //开始播放
        mSweepAnimator.start();
    }

当然,DecelerateInterpolator只是先快后慢的插值器,系统还提供了其他各种各样的插值器:

插值器(接口/类) 对应效果
AccelerateDecelerateInterpolator(默认) 插入器的变化速度在开始和结束的地方慢,在中间的时候加速
AccelerateInterpolator 变化速度开始缓慢,然后加速
AnticipateInterpolator 开始后退,然后前进
AnticipateOvershootInterpolator 开始后退,然后前进超过终点,最后返回终点
BounceInterpolator 动画结束的时候弹跳至终点
CycleInterpolator 动画循环播放指定的次数
DecelerateInterpolator 变化速度加速开始,然后减慢
LinearInterpolator 变化速度是固定的线性变化
OvershootInterpolator 前进超过终点,最后返回终点
TimeInterpolator(接口) TimeInterpolator是一个接口,如果以上插值器都不符合你的需求,可以实现TimeInterpolator接口来自定义插值器

2.4 动画的监听

有时候我们需要对动画进行监听,根据动画播放的状态进行某些动作

1.监听动画播放的四个状态

        objectAnimator.addListeoner(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                //动画开始
            }

            @Override
            public void onAnimationEnd(Animator animation) {
              //动画结束
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                //动画取消
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
                //动画重播
            }
        });

//如果只想用其中的一个,只需改成适配器类AnimatorListenerAdapter即可:

 objectAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationCancel(Animator animation) {
                super.onAnimationCancel(animation);
            }
        });


2.监听动画播放过程中的值变化 这里也很关键 通常我们会在这调用作用对象的相关方法来表现出动画效果(不是所有对象的set方法都可以编辑)

       objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
           @Override
           public void onAnimationUpdate(ValueAnimator animation) {
               /**
           * 获取ValueAnimator在运动时,当前运动点的值
           */  
            animation.getAnimatedValue();
//float value = (float) animation.getAnimatedValue();
           }
       });

2.5 估值器Evaluators

Evaluators来告诉系统对于一个给定的属性如何来计算它的值,它们获得Animator提供的数据(动画起始值和结束值),并根据这些数据来计算动画值。

我们,知道创建动画实例的常用方法
ofFloat:创建属性为浮点型的动画 默认估值器FloatEvaluator
ofInt :创建属性为整型的动画 默认估值器IntEvaluator
ofArgb:创建属性为颜色值的动画 默认估值器ArgbEvaluator
ofObject:创建自定义属性的动画 没有估值器 需要自定义
ofPropertyValuesHolder:包含PropertyValuesHolder的animator实例

其中ofPropertyValuesHolder的用法会补充在后边 这里暂时跳过:

我们重点看ofObject,我们知道系统为ofFloatofIntofArgb都提供了估值器
但是对ofObject 系统没办法提供默认的估值器,因为不知道该怎么计算,这个时候就需要我们针对自定义属性来实现TypeEvaluator接口完成自定义估值器

 public class MyEvalutor implements TypeEvaluator {
        @Override
        public Object evaluate(float fraction, Object startValue, Object endValue) {
            return null;
        }
    }

2.5.1 估值器使用例子
原链接:https://www.jianshu.com/p/0389e2c8e6a8
先在activity中加入一个Textview
然后创建一个自定义object的ObjectAnimator例子,先定义一个ObInfo类:

 public class ObInfo implements Serializable {
     public int color;//用来定义颜色
     public float x; //用来定义X轴
     public float y; //用来定义Y轴

     public ObInfo(int color, float x, float y) {
         this.color = color;
         this.x = x;
         this.y = y;
        }
    }

接着自定义TypeEvaluator :

 public class MyEvalutor implements TypeEvaluator {

   @Override
   public ObInfo evaluate(float fraction, ObInfo startValue, ObInfo endValue) {
       float x = startValue.x + fraction * (endValue.x - startValue.x);
       float y = startValue.y + fraction * (endValue.y - startValue.y);
       int color = (int) (startValue.color + fraction * (endValue.color - startValue.color));
       return new ObInfo(color, x, y);
        }
    }

最后实现动画:

ObInfo info1 = new ObInfo(0xffffff00, 500, 200);
ObInfo info2 = new ObInfo(0xff0000ff, 500, 1000);
ValueAnimator animator = ValueAnimator.ofObject(new MyEvalutor(), info1, info2, info1);
animator.setDuration(4000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        ObInfo info = (ObInfo) animation.getAnimatedValue();
        tv_text.layout(tv_text.getLeft(), (int) info.y, tv_text.getRight(), (int) (info.y + tv_text.getHeight()));
        tv_text.setTextColor(info.color);
    }
  });
animator.start();
效果

2.6 PropertyValuesHolder和Keyframe

https://blog.csdn.net/harvic880925/article/details/50752838
原文链接https://www.jianshu.com/p/f32ef6b6e3a5

PropertyValuesHolder:顾名思义,就是属性值持有者,它保存了动画过程中所需要操作的属性和对应的值,我们通过ofFloat(Object target, String propertyName, float… values)构造的动画,ofFloat()的内部实现其实就是将传进来的参数封装成PropertyValuesHolder实例来保存动画状态。在封装成PropertyValuesHolder实例以后,后面的操作也是以PropertyValuesHolder为主的。
Keyframe:意为关键帧,设置了关键帧后,动画就可以在各个关键帧之间平滑过渡的,一个关键帧必须包含两个原素,第一时间点,第二位置,即这个关键帧是表示的是某个物体在哪个时间点应该在哪个位置上。fraction表示当前进度,value表示当前位置。
使用方法:先创建关键帧Keyframe ,然后通过多个关键帧创建一个PropertyValuesHolder ,最后通过ObjectAnimator.ofPropertyValuesHolder实例化

//创建关键帧
//这里总共三个关键帧
//Keyframe.ofFloat(0.5f, 5f); 表示在动画进度50%的时候 属性要到5
Keyframe keyframe1 = Keyframe.ofFloat(0f, 0f);
Keyframe keyframe2 = Keyframe.ofFloat(0.5f, 5f);
Keyframe keyframe3 = Keyframe.ofFloat(1.0f, 0f);
//创建PropertyValuesHolder 
PropertyValuesHolder propertyValuesHolder = PropertyValuesHolder.ofKeyframe(propertyName, keyframe1, keyframe2, keyframe3);
//创建ObjectAnimator 
ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(target, propertyValuesHolder);
//设置动画时长
objectAnimator.setDuration(1000);
//设置动画重复次数
objectAnimator.setRepeatCount(1);
//设置动画重复模式
objectAnimator.setRepeatMode(ValueAnimator.REVERSE);
//启动动画
objectAnimator.start();

2.7 AnimatorSet

如果想使用组合动画,可以使用AnimatorSet将多个动画组合到一起:

AnimatorSet bouncer = new AnimatorSet();
bouncer.play(bounceAnim).before(squashAnim1);
bouncer.play(squashAnim1).with(squashAnim2);
bouncer.play(squashAnim1).with(stretchAnim1);
bouncer.play(squashAnim1).with(stretchAnim2);
bouncer.play(bounceBackAnim).after(stretchAnim2);
ValueAnimator fadeAnim = ObjectAnimator.ofFloat(newBall, "alpha", 1f, 0f);
fadeAnim.setDuration(250);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(bouncer).before(fadeAnim);
animatorSet.start();

执行顺序:
1.执行bounceAnim动画
2.同时执行squashAnim1, squashAnim2, stretchAnim1, stretchAnim2动画
3.执行bounceBackAnim动画
4.最后fadeAnim

2.5 xml中定义动画

ValueAnimator 还可以用XML文件来写,这样写的好处是更容易被复用,为了和API 11之前的动画做区分,请将属性动画的XML文件放在res/animator/目录下,如新建一个value_animator.xml文件,示例:



在代码中加载XML文件:

  //加载XML文件
  ValueAnimator animator = (ValueAnimator) AnimatorInflater.loadAnimator(this, R.animator.value_animator);
 //设置要执行动画的目标
 animator.setTarget(myObject);
 //动画执行
  animator.start();

还可以使用 PropertyValuesHolder和Keyframe标签创建一个多步的动画,如:


    
        
        
        
    

三.优秀的动画例子学习

3.1 一个精致的打勾小动画View

https://github.com/ChengangFeng/TickView

3.2 ...待续...

参考文章:https://www.jianshu.com/p/2412d00a0ce4

你可能感兴趣的:(换个思路来看Android属性动画)