美文网首页
换个思路来看Android属性动画

换个思路来看Android属性动画

作者: 番茄tomato | 来源:发表于2020-04-15 19:01 被阅读0次

    一句话总结属性动画:

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

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

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

    一.从简单的例子开始

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

    抖动的Hello World
    首先在Activity的布局文件中添加一个TextView
        <TextView
            android:id="@+id/tv_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:text="Hello World"
            android:textSize="25sp"
            />
    

    然后在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<ObInfo> {
    
       @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 version="1.0" encoding="utf-8"?>
    <animator xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="1000"
        android:repeatCount="1"
        android:repeatMode="reverse"
        android:valueFrom="0.0"
        android:valueTo="1.0"
        android:valueType="floatType" />
    

    在代码中加载XML文件:

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

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

    <animator xmlns:android="http://schemas.android.com/apk/res/android"
              android:duration="1000"
              android:repeatCount="1"
              android:repeatMode="reverse">
        <propertyValuesHolder>
            <keyframe android:fraction="0f" android:value="0f"/>
            <keyframe android:fraction="0.5f" android:value="5f"/>
            <keyframe android:fraction="1f" android:value="0f"/>
        </propertyValuesHolder>
    </animator>
    

    三.优秀的动画例子学习

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

    https://github.com/ChengangFeng/TickView

    3.2 ...待续...

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

    相关文章

      网友评论

          本文标题:换个思路来看Android属性动画

          本文链接:https://www.haomeiwen.com/subject/eeycvhtx.html