属性动画详解

今天跟着blog详细了解属性动画。简书学习

1、属性动画出现的原因

属性动画是Android3.0之后出现的,而之前常用的两种动画为view动画和帧动画,不过他们有很多缺陷。
(1)作用对象局限:view
view动画只能作用在view上,而无法对非view对象进行动画操作。有些情况下动画效果只是视图的某个属性而不是整个视图,比如需要实现一个视图的动态颜色变化,那么就需要操作视图的颜色属性而实现动画效果,而不是针对整个view设计
(2)动画效果单一
view动画只实现了一个view的透明度、旋转、缩放、平移这些简单的动画需求。
(3)没有改变view属性,只是改变视觉效果
view动画只是改变了view的视觉效果,而不是真正的去改变一个view属性。
比如通过一个view动画将一个view从左上角移到右下角,而我们去点击动画完后右下角的view时,没有反馈。实际上的view还在左上角。

而属性动画的出现能够解决上述全部问题。

2、属性动画的工作原理

  • 在一定时间间隔内,通过不断对值进行改变,并不断赋值给该对象的属性,从而实现该对象在该属性上的动画效果。
  • 具体的工作原理如下:
    在这里插入图片描述
  • 在赋值的时候有两个很重要的类:ValueAnimator类和ObjectAnimator类。属性动画就是靠这两个类实现动画效果。

3、具体的使用

3.1、ValueAnimator类:
属性动画中最核心的一个类,通过不断控制值的变化,在不断手动赋值给对象的属性,从而实现动画效果:
在这里插入图片描述
从上图的ValueAnimator可以看出有三个很重要的方法:

  1. ValueAnimator.ofInt(int values)
  2. ValueAnimator.ofFloat(float values)
  3. ValueAnimator.ofObject(int values)

下面将逐一介绍:

3.1.1、ValueAnimator.ofInt(int values):
其工作原理就是和第一张图一样。本质上是一种对值的操作。

下面可以举一个实例,如何将一个值从0到3平滑的过渡。操作方式为xml/java代码中实现。

(建议实际多用java代码实现,因为很多属性值是一开始不知道的,要动态的去获取)
通过java代码实现:

// 步骤1:设置动画属性的初始值 & 结束值
ValueAnimator anim = ValueAnimator.ofInt(0, 3);
        // ofInt()作用有两个
        // 1. 创建动画实例
        // 2. 将传入的多个Int参数进行平滑过渡:此处传入0和1,表示将值从0平滑过渡到1
        // 如果传入了3个Int参数 a,b,c ,则是先从a平滑过渡到b,再从b平滑过渡到C,以此类推
        // ValueAnimator.ofInt()内置了整型估值器,直接采用默认的.不需要设置,即默认设置了如何从初始值 过渡到 结束值
        // 关于自定义插值器我将在下节进行讲解
        // 下面看看ofInt()的源码分析 ->>关注1
        
// 步骤2:设置动画的播放各种属性
        anim.setDuration(500);
        // 设置动画运行的时长
        
        anim.setStartDelay(500);
        // 设置动画延迟播放时间

        anim.setRepeatCount(0);
        // 设置动画重复播放次数 = 重放次数+1
        // 动画播放次数 = infinite时,动画无限重复
        
        anim.setRepeatMode(ValueAnimator.RESTART);
        // 设置重复播放动画模式
        // ValueAnimator.RESTART(默认):正序重放
        // ValueAnimator.REVERSE:倒序回放
     
// 步骤3:将改变的值手动赋值给对象的属性值:通过动画的更新监听器
        // 设置 值的更新监听器
        // 即:值每次改变、变化一次,该方法就会被调用一次
        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {

                int currentValue = (Integer) animation.getAnimatedValue();
                // 获得改变后的值
                
                System.out.println(currentValue);
                // 输出改变后的值

        // 步骤4:将改变后的值赋给对象的属性值,下面会详细说明
                View.setproperty(currentValue);

       // 步骤5:刷新视图,即重新绘制,从而实现动画效果
                View.requestLayout();
                
                
            }
        });

        anim.start();
        // 启动动画
    }

// 关注1:ofInt()源码分析
    public static ValueAnimator ofInt(int... values) {
        // 允许传入一个或多个Int参数
        // 1. 输入一个的情况(如a):从0过渡到a;
        // 2. 输入多个的情况(如a,b,c):先从a平滑过渡到b,再从b平滑过渡到C
        
        ValueAnimator anim = new ValueAnimator();
        // 创建动画对象
        anim.setIntValues(values);
        // 将传入的值赋值给动画对象
        return anim;
    }

值从初始值过渡到结束值如下:
在这里插入图片描述
通过XML实现:
现在res/animator的文件创建相应的动画.xml文件,再在这个文件设置参数:

// ValueAnimator采用<animator>  标签,xml名为set_animation
<animator xmlns:android="http://schemas.android.com/apk/res/android"  
    android:valueFrom="0"   // 初始值
    android:valueTo="100"  // 结束值
    android:valueType="intType" // 变化值类型 :floatType & intType

    android:duration="3000" // 动画持续时间(ms),必须设置,动画才有效果
    android:startOffset ="1000" // 动画延迟开始时间(ms)
    android:fillBefore =true// 动画播放完后,视图是否会停留在动画开始的状态,默认为true
    android:fillAfter =false// 动画播放完后,视图是否会停留在动画结束的状态,优先于fillBefore值,默认为false
    android:fillEnabled=true// 是否应用fillBefore值,对fillAfter值无影响,默认为true
    android:repeatMode= “restart” // 选择重复播放动画模式,restart代表正序重放,reverse代表倒序回放,默认为restart|
    android:repeatCount =0// 重放次数(所以动画的播放次数=重放次数+1),为infinite时无限重复
    android:interpolator = @[package:]anim/interpolator_resource // 插值器,即影响动画的播放速度,下面会详细讲

/>  

最后在java中启动这个动画:

Animator animator = AnimatorInflater.loadAnimator(context, R.animator.set_animation);  
// 载入XML动画

animator.setTarget(view);  
// 设置动画对象

animator.start();  
// 启动动画

效果和java代码实现的那个一样。

实例:对一个宽度为150px的botton做一个宽度边长到500的动画效果:

Button mButton = (Button) findViewById(R.id.Button);
        // 创建动画作用对象:此处以Button为例

// 步骤1:设置属性数值的初始值 & 结束值
        ValueAnimator valueAnimator = ValueAnimator.ofInt(mButton.getLayoutParams().width, 500);
        // 初始值 = 当前按钮的宽度,此处在xml文件中设置为150
        // 结束值 = 500
        // ValueAnimator.ofInt()内置了整型估值器,直接采用默认的.不需要设置
        // 即默认设置了如何从初始值150 过渡到 结束值500

// 步骤2:设置动画的播放各种属性
        valueAnimator.setDuration(2000);
        // 设置动画运行时长:1s

// 步骤3:将属性数值手动赋值给对象的属性:此处是将 值 赋给 按钮的宽度
        // 设置更新监听器:即数值每次变化更新都会调用该方法
        valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animator) {

                int currentValue = (Integer) animator.getAnimatedValue();
                // 获得每次变化后的属性值
                System.out.println(currentValue);
                // 输出每次变化后的属性值进行查看

                mButton.getLayoutParams().width = currentValue;
                // 每次值变化时,将值手动赋值给对象的属性
                // 即将每次变化后的值 赋 给按钮的宽度,这样就实现了按钮宽度属性的动态变化

// 步骤4:刷新视图,即重新绘制,从而实现动画效果
                mButton.requestLayout();
                
            }
        });

        valueAnimator.start();
        // 启动动画

    }

效果如下:
在这里插入图片描述

3.1.2、ValueAnimator.ofFloat(float values)
和ofInt不同的是,它是将初始值以浮点数的形式过渡到结束值。工作流程和ofInt一样。同样也可以通过xml设置和java代码完成动画。 java代码就不做阐述,下面看一下xml文件中的设置:

// ValueAnimator采用<animator>  标签
<animator xmlns:android="http://schemas.android.com/apk/res/android"  
    // 设置属性同上
    android:valueFrom="0"  
    android:valueTo="100"  
    android:valueType="intType"/>  

接着在java启动,最后效果图如下:
在这里插入图片描述
可以看出ofInt和ofFloat的差别仅仅是在估值器上的区别,ofInt采用默认的整型估值器(IntEvaluator),ofFloat采用默认的浮点型估值器(FloatEvaluator)

3.1.3、VlueAnimator.ofObject()
作用是将初始值以对象的形式过渡到结束值,流程上面图有说到,有默认的插值器,没有默认的估值器,只能自己自定义实现。

具体使用为:

// 创建初始动画时的对象  & 结束动画时的对象
myObject object1 = new myObject();  
myObject object2 = new myObject();  

ValueAnimator anim = ValueAnimator.ofObject(new myObjectEvaluator(), object1, object2);  
// 创建动画对象 & 设置参数
// 参数说明
// 参数1:自定义的估值器对象(TypeEvaluator 类型参数) - 下面会详细介绍
// 参数2:初始动画的对象
// 参数3:结束动画的对象
anim.setDuration(5000);  
anim.start(); 

在ofObject的使用中,我们要去手写一个估值器对象。下面介绍一下估值器:

估值器的介绍
作用是设置动画 初始值 到 结束值 时的逻辑。
插值器(Interpolator)可以决定值的变化模式(匀速、变速)
估值器(TypeEvaluator)可以决定值的具体变化竖直。

之前可以看到ofFloat中,实现了将初始值以浮点数的形式过渡到结束值的逻辑,而这个逻辑就是由FloatEvaluator这个默认估值器实现的,我们来看下它的代码:

public class FloatEvaluator implements TypeEvaluator {  
// FloatEvaluator实现了TypeEvaluator接口

// 重写evaluate()
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
// 参数说明
// fraction:表示动画完成度(根据它来计算当前动画的值)
// startValue、endValue:动画的初始值和结束值
        float startFloat = ((Number) startValue).floatValue();  
        
        return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);  
        // 初始值 过渡 到结束值 的算法是:
        // 1. 用结束值减去初始值,算出它们之间的差值
        // 2. 用上述差值乘以fraction系数
        // 3. 再加上初始值,就得到当前动画的值
    }  
}  

上述就是系统默认的估值器,而对于ofObject,系统没法默认实现,因为其中的值是对象,而对象复杂多样,所以要求我们自己去实现。
自定义一个估值器去实现TypeEvaluator接口的实现如下:

// 实现TypeEvaluator接口
public class ObjectEvaluator implements TypeEvaluator{  

// 复写evaluate()
// 在evaluate()里写入对象动画过渡的逻辑
    @Override  
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
        // 参数说明
        // fraction:表示动画完成度(根据它来计算当前动画的值)
        // startValue、endValue:动画的初始值和结束值

        ... // 写入对象动画过渡的逻辑
        
        return value;  
        // 返回对象动画过渡的逻辑计算后的值
    }  
  

下面来实现一个实例:使一个圆从一个点移动到另一个点:
在这里插入图片描述
工程目录如图:
在这里插入图片描述
因为ofObject是面向对象操作的,所以我们要自己去自定义对象类。
本例需要操作的是对象的圆的坐标。

步骤1:Point.java 代码:

public class Point {

    // 设置两个变量用于记录坐标的位置
    private float x;
    private float y;

    // 构造方法用于设置坐标
    public Point(float x, float y) {
        this.x = x;
        this.y = y;
    }

    // get方法用于获取坐标
    public float getX() {
        return x;
    }

    public float getY() {
        return y;
    }
}

步骤2:根据需求实现TypeEvaluator接口:
本例是实现一个从左上角到右下角移动的逻辑,编写PointEvaluator.java:

// 实现TypeEvaluator接口
public class PointEvaluator implements TypeEvaluator {

    // 复写evaluate()
    // 在evaluate()里写入对象动画过渡的逻辑
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {

        // 将动画初始值startValue 和 动画结束值endValue 强制类型转换成Point对象
        Point startPoint = (Point) startValue;
        Point endPoint = (Point) endValue;

        // 根据fraction来计算当前动画的x和y的值
        float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
        float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());
        
        // 将计算后的坐标封装到一个新的Point对象中并返回
        Point point = new Point(x, y);
        return point;
    }

}

步骤3:将属性动画作用到自定义view中:
MyView.java:

public class MyView extends View {
    // 设置需要用到的变量
    public static final float RADIUS = 70f;// 圆的半径 = 70
    private Point currentPoint;// 当前点坐标
    private Paint mPaint;// 绘图画笔
    

    // 构造方法(初始化画笔)
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 初始化画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
    }

    // 复写onDraw()从而实现绘制逻辑
    // 绘制逻辑:先在初始点画圆,通过监听当前坐标值(currentPoint)的变化,每次变化都调用onDraw()重新绘制圆,从而实现圆的平移动画效果
    @Override
    protected void onDraw(Canvas canvas) {
        // 如果当前点坐标为空(即第一次)
        if (currentPoint == null) {
            currentPoint = new Point(RADIUS, RADIUS);
            // 创建一个点对象(坐标是(70,70))

            // 在该点画一个圆:圆心 = (70,70),半径 = 70
            float x = currentPoint.getX();
            float y = currentPoint.getY();
            canvas.drawCircle(x, y, RADIUS, mPaint);


 // (重点关注)将属性动画作用到View中
            // 步骤1:创建初始动画时的对象点  & 结束动画时的对象点
            Point startPoint = new Point(RADIUS, RADIUS);// 初始点为圆心(70,70)
            Point endPoint = new Point(700, 1000);// 结束点为(700,1000)

            // 步骤2:创建动画对象 & 设置初始值 和 结束值
            ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
            // 参数说明
            // 参数1:TypeEvaluator 类型参数 - 使用自定义的PointEvaluator(实现了TypeEvaluator接口)
            // 参数2:初始动画的对象点
            // 参数3:结束动画的对象点

            // 步骤3:设置动画参数
            anim.setDuration(5000);
            // 设置动画时长

// 步骤3:通过 值 的更新监听器,将改变的对象手动赋值给当前对象
// 此处是将 改变后的坐标值对象 赋给 当前的坐标值对象
            // 设置 值的更新监听器
            // 即每当坐标值(Point对象)更新一次,该方法就会被调用一次
            anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    currentPoint = (Point) animation.getAnimatedValue();
                    // 将每次变化后的坐标值(估值器PointEvaluator中evaluate()返回的Piont对象值)到当前坐标值对象(currentPoint)
                    // 从而更新当前坐标值(currentPoint)

// 步骤4:每次赋值后就重新绘制,从而实现动画效果
                    invalidate();
                    // 调用invalidate()后,就会刷新View,即才能看到重新绘制的界面,即onDraw()会被重新调用一次
                    // 所以坐标值每改变一次,就会调用onDraw()一次
                }
            });

            anim.start();
            // 启动动画


        } else {
            // 如果坐标值不为0,则画圆
            // 所以坐标值每改变一次,就会调用onDraw()一次,就会画一次圆,从而实现动画效果

            // 在该点画一个圆:圆心 = (30,30),半径 = 30
            float x = currentPoint.getX();
            float y = currentPoint.getY();
            canvas.drawCircle(x, y, RADIUS, mPaint);
        }
    }
}

步骤4:在布局文件中加入自定义view

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="rikka.MainActivity">

    <scut.carson_ho.valueanimator_ofobject.MyView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
         />
</RelativeLayout>	

最后启动Activity动画效果出来了。

至此,属性动画最核心的ValueAnimator类已经讲解完毕。接下来讲解另一个重要的类。

3.2、ObjectAnimator类

直接对对象的属性值进行改变,从而实现动画效果。
比如直接改变一个view的alpha属性从而实现透明的动画效果
ObjectAnimator是继承ValueAnimator类,所以其底层实现还是基于ValueAnimator类。

本质是 通过不断的变化值, 自动的 赋值给对象属性。
工作流程如图:
在这里插入图片描述
从图中可以看到ObjectAnimator和ValueAnimator的区别:

  1. ValueAnimator是先通过估值器改变值,然后手动赋值给对象属性从而实现动画;是间接对对象属性进行操作。
  2. ObjectAnimator是先改变值,然后自动赋值给对象的属性从而实现动画。是直接对对象属性进行操作。

具体使用:(可以用xml和java)
java设置:

ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String property, float ....values);  

// ofFloat()作用有两个
// 1. 创建动画实例
// 2. 参数设置:参数说明如下
// Object object:需要操作的对象
// String property:需要操作的对象的属性
// float ....values:动画初始值 & 结束值(不固定长度)
// 若是两个参数a,b,则动画效果则是从属性的a值到b值
// 若是三个参数a,b,c,则则动画效果则是从属性的a值到b值再到c值
// 以此类推
// 至于如何从初始值 过渡到 结束值,同样是由估值器决定,此处ObjectAnimator.ofFloat()是有系统内置的浮点型估值器FloatEvaluator,同ValueAnimator讲解

anim.setDuration(500);
        // 设置动画运行的时长

        anim.setStartDelay(500);
        // 设置动画延迟播放时间

        anim.setRepeatCount(0);
        // 设置动画重复播放次数 = 重放次数+1
        // 动画播放次数 = infinite时,动画无限重复

        anim.setRepeatMode(ValueAnimator.RESTART);
        // 设置重复播放动画模式
        // ValueAnimator.RESTART(默认):正序重放
        // ValueAnimator.REVERSE:倒序回放

animator.start();  
// 启动动画

在xml中设置:

// ObjectAnimator 采用<objectAnimator>  标签
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"  
    android:valueFrom="1"   // 初始值
    android:valueTo="0"  // 结束值
    android:valueType="floatType"  // 变化值类型 :floatType & intType
    android:propertyName="alpha" // 对象变化的属性名称
/>  

然后在java中设置target等然后通过start开启一个动画。

通过在ofFloat中传入property,可以实现4个动画:透明度变化、缩放、平移、旋转,为:

  • “alpha”:((1,0,1)从不透明到透明再到不透明)
  • “translationX/Y” :平移到指定的x或者y坐标
  • “scaleX/Y”:(scale是倍数,可以缩放横向和纵向长度为原来的几倍)
  • “rotation”:((0,360)转一圈)

当然了,除了这一些属性,还可以传入别的属性!当然这些属性在对应的类要有对应的get和set方法。

比如对一个button的alpha进行操作,而button中就有 getAlpha方法和setAlpha(float values)。就是对这两个方法进行操作。而这些属性view都有,继承了view属性都能得到这些get和set方法。

而至于如何进行赋值的,我们来看下源码:

// 使用方法
ObjectAnimator animator = ObjectAnimator.ofFloat(Object object, String property, float ....values);  
anim.setDuration(500);
animator.start();  
// 启动动画,源码分析就直接从start()开始

<--  start()  -->
@Override  
public void start() {  
    AnimationHandler handler = sAnimationHandler.get();  

    if (handler != null) {  
        // 判断等待动画(Pending)中是否有和当前动画相同的动画,如果有就把相同的动画给取消掉 
        numAnims = handler.mPendingAnimations.size();  
        for (int i = numAnims - 1; i >= 0; i--) {  
            if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {  
                ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);  
                if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {  
                    anim.cancel();  
                }  
            }  
        }  
      // 判断延迟动画(Delay)中是否有和当前动画相同的动画,如果有就把相同的动画给取消掉 
        numAnims = handler.mDelayedAnims.size();  
        for (int i = numAnims - 1; i >= 0; i--) {  
            if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {  
                ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);  
                if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {  
                    anim.cancel();  
                }  
            }  
        }  
    }  
    
    super.start();  
   // 调用父类的start()
   // 因为ObjectAnimator类继承ValueAnimator类,所以调用的是ValueAnimator的start()
   // 经过层层调用,最终会调用到 自动赋值给对象属性值的方法
   // 下面就直接看该部分的方法
}  



<-- 自动赋值给对象属性值的逻辑方法 ->>

// 步骤1:初始化动画值
private void setupValue(Object target, Keyframe kf) {  
    if (mProperty != null) {  
        kf.setValue(mProperty.get(target));  
        // 初始化时,如果属性的初始值没有提供,则调用属性的get()进行取值
    }  
        kf.setValue(mGetter.invoke(target));   
    }  
}  

// 步骤2:更新动画值
// 当动画下一帧来时(即动画更新的时候),setAnimatedValue()都会被调用
void setAnimatedValue(Object target) {  
    if (mProperty != null) {  
        mProperty.set(target, getAnimatedValue());  
        // 内部调用对象该属性的set()方法,从而从而将新的属性值设置给对象属性
    }  
    
}  

可以看出自动化赋值的逻辑:

  1. 初始化时,如果属性的初始值没有提供,则通过 调用属性的get()方法进行取值。
  2. 当值变化时,用该对象的属性 set()方法,从而将新值设置给对象属性。

总结:

  1. ObjectAnimator针对的是任意对象/任意属性值,并不单单是针对整个view
  2. 如果要采用ObjectAnimator实现某个对象某个属性的动画效果,则该属性必须实现get和set方法。

3.3、通过自定义对象属性实现动画效果
我们通过为对象的属性设置get和set方法,还有实现TypeEvaluator类定义逻辑上的变化来做一个动画。
该动画实现颜色渐变。

自定义的动画工作逻辑如下:
在这里插入图片描述
步骤1:设置对象属性的get和set方法
在这里,有两种可以设置get/set方法的方法:

  1. 通过继承原始类,直接给类属性加上get和set。
  2. 通过包装原始动画对象,间接给对象加上该属性的get和set,即用一个类来包装原始对象

第二种方法之后再讲,先用第一个方法实现一个 MyView2:

public class MyView2 extends View {
    // 设置需要用到的变量
    public static final float RADIUS = 100f;// 圆的半径 = 100
    private Paint mPaint;// 绘图画笔

    private String color;
    // 设置背景颜色属性

    // 设置背景颜色的get() & set()方法
    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
        mPaint.setColor(Color.parseColor(color));
        // 将画笔的颜色设置成方法参数传入的颜色
        invalidate();
        // 调用了invalidate()方法,即画笔颜色每次改变都会刷新视图,然后调用onDraw()方法重新绘制圆
        // 而因为每次调用onDraw()方法时画笔的颜色都会改变,所以圆的颜色也会改变
    }


    // 构造方法(初始化画笔)
    public MyView2(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 初始化画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
    }

    // 复写onDraw()从而实现绘制逻辑
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawCircle(500, 500, RADIUS, mPaint);
    }
}

步骤2:在布局文件中加入这个view:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="scut.carson_ho.valueanimator_ofobject.MainActivity">

    <scut.carson_ho.valueanimator_ofobject.MyView2
        android:id="@+id/MyView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
         />
</RelativeLayout>

步骤3:根据需求实现TypeEvaluator接口:
ColorEvaluator(实现颜色变化)

public class ColorEvaluator implements TypeEvaluator {
    // 实现TypeEvaluator接口

    private int mCurrentRed;

    private int mCurrentGreen ;

    private int mCurrentBlue ;

    // 复写evaluate()
    // 在evaluate()里写入对象动画过渡的逻辑:此处是写颜色过渡的逻辑
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {

        // 获取到颜色的初始值和结束值
        String startColor = (String) startValue;
        String endColor = (String) endValue;

        // 通过字符串截取的方式将初始化颜色分为RGB三个部分,并将RGB的值转换成十进制数字
        // 那么每个颜色的取值范围就是0-255
        int startRed = Integer.parseInt(startColor.substring(1, 3), 16);
        int startGreen = Integer.parseInt(startColor.substring(3, 5), 16);
        int startBlue = Integer.parseInt(startColor.substring(5, 7), 16);

        int endRed = Integer.parseInt(endColor.substring(1, 3), 16);
        int endGreen = Integer.parseInt(endColor.substring(3, 5), 16);
        int endBlue = Integer.parseInt(endColor.substring(5, 7), 16);

        // 将初始化颜色的值定义为当前需要操作的颜色值
            mCurrentRed = startRed;
            mCurrentGreen = startGreen;
            mCurrentBlue = startBlue;


        // 计算初始颜色和结束颜色之间的差值
        // 该差值决定着颜色变化的快慢:初始颜色值和结束颜色值很相近,那么颜色变化就会比较缓慢;否则,变化则很快
        // 具体如何根据差值来决定颜色变化快慢的逻辑写在getCurrentColor()里.
        int redDiff = Math.abs(startRed - endRed);
        int greenDiff = Math.abs(startGreen - endGreen);
        int blueDiff = Math.abs(startBlue - endBlue);
        int colorDiff = redDiff + greenDiff + blueDiff;
        if (mCurrentRed != endRed) {
            mCurrentRed = getCurrentColor(startRed, endRed, colorDiff, 0,
                    fraction);
                    // getCurrentColor()决定如何根据差值来决定颜色变化的快慢 ->>关注1
        } else if (mCurrentGreen != endGreen) {
            mCurrentGreen = getCurrentColor(startGreen, endGreen, colorDiff,
                    redDiff, fraction);
        } else if (mCurrentBlue != endBlue) {
            mCurrentBlue = getCurrentColor(startBlue, endBlue, colorDiff,
                    redDiff + greenDiff, fraction);
        }
        // 将计算出的当前颜色的值组装返回
        String currentColor = "#" + getHexString(mCurrentRed)
                + getHexString(mCurrentGreen) + getHexString(mCurrentBlue);

        // 由于我们计算出的颜色是十进制数字,所以需要转换成十六进制字符串:调用getHexString()->>关注2
        // 最终将RGB颜色拼装起来,并作为最终的结果返回
        return currentColor;
    }


    // 关注1:getCurrentColor()
    // 具体是根据fraction值来计算当前的颜色。

    private int getCurrentColor(int startColor, int endColor, int colorDiff,
                                int offset, float fraction) {
        int currentColor;
        if (startColor > endColor) {
            currentColor = (int) (startColor - (fraction * colorDiff - offset));
            if (currentColor < endColor) {
                currentColor = endColor;
            }
        } else {
            currentColor = (int) (startColor + (fraction * colorDiff - offset));
            if (currentColor > endColor) {
                currentColor = endColor;
            }
        }
        return currentColor;
    }

    // 关注2:将10进制颜色值转换成16进制。
    private String getHexString(int value) {
        String hexString = Integer.toHexString(value);
        if (hexString.length() == 1) {
            hexString = "0" + hexString;
        }
        return hexString;
    }

}

步骤4:调用ObjectAnimator.ofObject方法:

public class MainActivity extends AppCompatActivity {

    MyView2 myView2;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        myView2 = (MyView2) findViewById(R.id.MyView2);
        ObjectAnimator anim = ObjectAnimator.ofObject(myView2, "color", new ColorEvaluator(),
                "#0000FF", "#FF0000");
        // 设置自定义View对象、背景颜色属性值 & 颜色估值器
        // 本质逻辑:
        // 步骤1:根据颜色估值器不断 改变 值 
        // 步骤2:调用set()设置背景颜色的属性值(实际上是通过画笔进行颜色设置)
        // 步骤3:调用invalidate()刷新视图,即调用onDraw()重新绘制,从而实现动画效果

        anim.setDuration(8000);
        anim.start();
    }
}

在这里插入图片描述

3.4 特别注意:如何手动设置对象类属性的get和set

  1. 对象必须要提供属性a的set()方法
    (1)如果没有传递初始值,那么需要提供get方法,因为系统要去拿属性a的初始值
    (2)若该条件不满足,程序直接Crash
  2. 对象提供的 属性a的set()方法 对 属性a的改变 必须通过某种方法反映出来
    (1)比如带来ui上的变化
    (2)若该条件不满足,动画无效,但不会Crash

上述条件中条件2容易满足,但是1有时候会出现,比如说:
由View的setWidth()并不是设置View的宽高,而是设置View的最大宽度和最小宽度,所以通过setWidth()无法改变空间的宽度,所以不能直接view的width属性做动画,没有效果。

那么这个有了set和get都不能实现动画的问题应该这么解决呢?那就是用到刚刚讲的:
用一个类来包装原始对象。

解决方案:

  1. 手动设置set和get,比如textview的setWidth()不能实现直接改变宽度,那我们可以写一个 MyTextView来继承TextView然后重写set和get
  2. 通过类包装

类包装的本质是Java设计模式中的装饰模式,即通过包装类从而扩展对象的功能

public class MainActivity extends AppCompatActivity {
    Button mButton;
    ViewWrapper wrapper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = (Button) findViewById(R.id.Button);
        // 创建动画作用对象:此处以Button为例

        wrapper = new ViewWrapper(mButton);
        // 创建包装类,并传入动画作用的对象
        
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                ObjectAnimator.ofInt(wrapper, "width", 500).setDuration(3000).start();
                // 设置动画的对象是包装类的对象
            }
        });

    }
    // 提供ViewWrapper类,用于包装View对象
    // 本例:包装Button对象
    private static class ViewWrapper {
        private View mTarget;

        // 构造方法:传入需要包装的对象
        public ViewWrapper(View target) {
            mTarget = target;
        }

        // 为宽度设置get() & set()
        public int getWidth() {
            return mTarget.getLayoutParams().width;
        }
        public void setWidth(int width) {
            mTarget.getLayoutParams().width = width;
            mTarget.requestLayout();
        }
    }
}

综上,我们就能完成通过setWidth()方法对一个Button宽度进行更改。

总结:

  • ValueAnimator类和ObjectAnimator类都属于属性动画,本质上的流程都是:先改变值,然后赋值,最后刷新view
  • 区别在于,ValueAnimator是手动赋值间接操作属性,而ObjectAnimator自动赋值直接操作属性,后者相对更加智能和广泛。

4、额外的使用方法

4.1、组合动画
通过AnimatorSet实现组合动画的功能
具体使用:

AnimatorSet.play(Animator anim)   :播放当前动画
AnimatorSet.after(long delay)   :将现有动画延迟x毫秒后执行
AnimatorSet.with(Animator anim)   :将现有动画和传入的动画同时执行
AnimatorSet.after(Animator anim)   :将现有动画插入到传入的动画之后执行
AnimatorSet.before(Animator anim) :  将现有动画插入到传入的动画之前执行

实例:主要实现平移,平移过程中伴随着旋转动画,平移完后进行透明度变化:

通过java代码实现:

// 步骤1:设置需要组合的动画效果
ObjectAnimator translation = ObjectAnimator.ofFloat(mButton, "translationX", curTranslationX, 300,curTranslationX);  
// 平移动画
ObjectAnimator rotate = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f);  
// 旋转动画
ObjectAnimator alpha = ObjectAnimator.ofFloat(mButton, "alpha", 1f, 0f, 1f);  
// 透明度动画

// 步骤2:创建组合动画的对象
AnimatorSet animSet = new AnimatorSet();  

// 步骤3:根据需求组合动画
animSet.play(translation).with(rotate).before(alpha);  
animSet.setDuration(5000);  

// 步骤4:启动动画
animSet.start();  

动画就不展示了…

XML设置:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:ordering="sequentially" >
    // 表示Set集合内的动画按顺序进行
    // ordering的属性值:sequentially & together
    // sequentially:表示set中的动画,按照先后顺序逐步进行(a 完成之后进行 b )
    // together:表示set中的动画,在同一时间同时进行,为默认值

    <set android:ordering="together" >
        // 下面的动画同时进行
        <objectAnimator
            android:duration="2000"
            android:propertyName="translationX"
            android:valueFrom="0"
            android:valueTo="300"
            android:valueType="floatType" >
        </objectAnimator>
        
        <objectAnimator
            android:duration="3000"
            android:propertyName="rotation"
            android:valueFrom="0"
            android:valueTo="360"
            android:valueType="floatType" >
        </objectAnimator>
    </set>

        <set android:ordering="sequentially" >
            // 下面的动画按序进行
            <objectAnimator
                android:duration="1500"
                android:propertyName="alpha"
                android:valueFrom="1"
                android:valueTo="0"
                android:valueType="floatType" >
            </objectAnimator>
            <objectAnimator
                android:duration="1500"
                android:propertyName="alpha"
                android:valueFrom="0"
                android:valueTo="1"
                android:valueType="floatType" >
            </objectAnimator>
        </set>
</set>

然后在java中启动:

mButton = (Button) findViewById(R.id.Button);
        // 创建动画作用对象:此处以Button为例

        AnimatorSet animator = (AnimatorSet) AnimatorInflater.loadAnimator(this, R.animator.set_animation);
// 创建组合动画对象  &  加载XML动画
        animator.setTarget(mButton);
        // 设置动画作用对象
        animator.start();
        // 启动动画

4.2、ViewPropertyAnimator用法
从上可以看出本质上是对属性的值进行修改。而Java是面向对象的,所以Google团队添加了面向对象操作的ViewPropertyAnimator类。
可以认为是属性动画的简写形式。
具体使用:

// 使用解析
        View.animate().xxx().xxx();
        // ViewPropertyAnimator的功能建立在animate()上
        // 调用animate()方法返回值是一个ViewPropertyAnimator对象,之后的调用的所有方法都是通过该实例完成
        // 调用该实例的各种方法来实现动画效果
        // ViewPropertyAnimator所有接口方法都使用连缀语法来设计,每个方法的返回值都是它自身的实例
        // 因此调用完一个方法后可直接连缀调用另一方法,即可通过一行代码就完成所有动画效果
        
// 以下是例子
        mButton = (Button) findViewById(R.id.Button);
        // 创建动画作用对象:此处以Button为例

        mButton.animate().alpha(0f);
        // 单个动画设置:将按钮变成透明状态 
        mButton.animate().alpha(0f).setDuration(5000).setInterpolator(new BounceInterpolator());
        // 单个动画效果设置 & 参数设置 
        mButton.animate().alpha(0f).x(500).y(500);
        // 组合动画:将按钮变成透明状态再移动到(500,500)处
        
        // 特别注意:
        // 动画自动启动,无需调用start()方法.因为新的接口中使用了隐式启动动画的功能,只要我们将动画定义完成后,动画就会自动启动
        // 该机制对于组合动画也同样有效,只要不断地连缀新的方法,那么动画就不会立刻执行,等到所有在ViewPropertyAnimator上设置的方法都执行完毕后,动画就会自动启动
        // 如果不想使用这一默认机制,也可以显式地调用start()方法来启动动画

4.3监听动画
Animation类通过监听动画的开始、结束、重复、取消来进行一系列操作。
可以在Java代码里的addListener()里设置:

   Animation.addListener(new AnimatorListener() {
          @Override
          public void onAnimationStart(Animation animation) {
              //动画开始时执行
          }
      
           @Override
          public void onAnimationRepeat(Animation animation) {
              //动画重复时执行
          }

         @Override
          public void onAnimationCancel()(Animation animation) {
              //动画取消时执行
          }
    
          @Override
          public void onAnimationEnd(Animation animation) {
              //动画结束时执行
          }
      });

// 特别注意:每次监听必须4个方法都重写。

所以可以得出Animator、ValueAnimator、ObjectAnimator、AnimatorSet有如下关系:
在这里插入图片描述

动画适配器
有时候我们不用监听动画的所有时刻,但是addListener必须重写那4个方法,那么为了解决代码累赘我们可以采用动画适配器,在addListener方法中传入一个AnimatorListenerAdapter。

anim.addListener(new AnimatorListenerAdapter() {  
// 向addListener()方法中传入适配器对象AnimatorListenerAdapter()
// 由于AnimatorListenerAdapter中已经实现好每个接口
// 所以这里不实现全部方法也不会报错
    @Override  
    public void onAnimationStart(Animator animation) {  
    // 如想只想监听动画开始时刻,就只需要单独重写该方法就可以
    }  
});  

综上,关于属性动画已经总结完成!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值