Android ObjectAnimator类:手把手带你自定义属性动画

3,271 阅读16分钟
原文链接: blog.csdn.net

前言

  • 属性动画的使用 是 Android 开发中常用的知识
  • 今天,我将讲解属性动画使用中最核心的一个方法类:ObjectAnimator,希望你们会喜欢。

目录

示意图


储备知识

阅读本文前,请先阅读文章:Android:这是一份全面 & 详细的动画入门学习指南


1. 简介

  • 实现属性动画中的一个核心方法类
  • 继承自ValueAnimator类,即底层的动画实现机制是基于ValueAnimator类

2. 实现动画的原理

直接对对象的属性值进行改变操作,从而实现动画效果

如直接改变 Viewalpha 属性 从而实现透明度的动画效果


3. 本质原理

通过不断控制 值 的变化,再不断 自动 赋给对象的属性,从而实现动画效果。如下图:

示意图

从上面的工作原理可以看出:ObjectAnimatorValueAnimator类的区别:

至于是如何自动赋值给对象的属性,下面会详细说明


4. 基础使用

由于是继承了ValueAnimator类,所以使用的方法十分类似:XML 设置 / Java设置

4.1 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();  
// 启动动画

4.2 在XML 代码中设置

  • 步骤1:在路径 res/animator 的文件夹里创建动画效果.xml文件

此处设置为res/animator/set_animation.xml

  • 步骤2:设置动画参数

set_animation.xml

// ObjectAnimator 采用<animator>  标签
<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代码中启动动画

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

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

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

4.3 使用实例

此处先展示四种基本变换:平移、旋转、缩放 & 透明度

a. 透明度

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

        ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "alpha", 1f, 0f, 1f);
        // 表示的是:
        // 动画作用对象是mButton
        // 动画作用的对象的属性是透明度alpha
        // 动画效果是:常规 - 全透明 - 常规
        animator.setDuration(5000);
        animator.start();

属性动画 - 透明度.gif

b. 旋转

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

  ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f);

        // 表示的是:
        // 动画作用对象是mButton
        // 动画作用的对象的属性是旋转alpha
        // 动画效果是:0 - 360
        animator.setDuration(5000);
        animator.start();

属性动画- 旋转.gif

c. 平移

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

  float curTranslationX = mButton.getTranslationX();
        // 获得当前按钮的位置
        ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "translationX", curTranslationX, 300,curTranslationX);
        

        // 表示的是:
        // 动画作用对象是mButton
        // 动画作用的对象的属性是X轴平移(在Y轴上平移同理,采用属性"translationY"
        // 动画效果是:从当前位置平移到 x=1500 再平移到初始位置
        animator.setDuration(5000);
        animator.start();

属性动画 - X轴平移.gif

d. 缩放

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

  ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "scaleX", 1f, 3f, 1f);
        // 表示的是:
        // 动画作用对象是mButton
        // 动画作用的对象的属性是X轴缩放
        // 动画效果是:放大到3倍,再缩小到初始大小
        animator.setDuration(5000);
        animator.start();

属性动画 - 缩放.gif


4. 通过自定义对象属性实现动画效果

  • 在上面的讲解,我们使用了属性动画最基本的四种动画效果:透明度、平移、旋转 & 缩放

即在ObjectAnimator.ofFloat()的第二个参数String property传入alpharotationtranslationXscaleY 等blabla

属性 作用 数值类型
Alpha 控制View的透明度 float
TranslationX 控制X方向的位移 float
TranslationY 控制Y方向的位移 float
ScaleX 控制X方向的缩放倍数 float
ScaleY 控制Y方向的缩放倍数 float
Rotation 控制以屏幕方向为轴的旋转度数 float
RotationX 控制以X轴为轴的旋转度数 float
RotationY 控制以Y轴为轴的旋转度数 float

问题:那么ofFloat()的第二个参数还能传入什么属性值呢?
答案:任意属性值。因为:

  • ObjectAnimator 类 对 对象属性值 进行改变从而实现动画效果的本质是:通过不断控制 值 的变化,再不断 自动 赋给对象的属性,从而实现动画效果

工作原理

  • 自动赋给对象的属性的本质是调用该对象属性的set() & get()方法进行赋值
  • 所以,ObjectAnimator.ofFloat(Object object, String property, float ....values)的第二个参数传入值的作用是:让ObjectAnimator类根据传入的属性名 去寻找 该对象对应属性名的 set() & get()方法,从而进行对象属性值的赋值,如上面的例子:
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "rotation", 0f, 360f);
// 其实Button对象中并没有rotation这个属性值
// ObjectAnimator并不是直接对我们传入的属性名进行操作
// 而是根据传入的属性值"rotation" 去寻找对象对应属性名对应的get和set方法,从而通过set() &  get()对属性进行赋值

// 因为Button对象中有rotation属性所对应的get & set方法
// 所以传入的rotation属性是有效的
// 所以才能对rotation这个属性进行操作赋值
public void setRotation(float value);  
public float getRotation();  

// 实际上,这两个方法是由View对象提供的,所以任何继承自View的对象都具备这个属性


4.1 原理解析

至于是如何进行自动赋值的,我们直接来看源码分析:

// 使用方法
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的star()
   // 经过层层调用,最终会调用到 自动赋值给对象属性值的方法
   // 下面就直接看该部分的方法
}  



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

// 步骤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()方法,从而从而将新的属性值设置给对象属性。

所以:

  • ObjectAnimator 类针对的是任意对象 & 任意属性值,并不是单单针对于View对象

  • 如果需要采用ObjectAnimator 类实现动画效果,那么需要操作的对象就必须有该属性的set() & get()

  • 同理,针对上述另外的三种基本动画效果,View 也存在着setRotation()getRotation()setTranslationX()getTranslationX()setScaleY()getScaleY()set() & get()


4.2 具体使用

对于属性动画,其拓展性在于:不局限于系统限定的动画,可以自定义动画,即自定义对象的属性,并通过操作自定义的属性从而实现动画。

那么,该如何自定义属性呢?本质上,就是:

  • 为对象设置需要操作属性的set() & get()方法
  • 通过实现TypeEvaluator类从而定义属性变化的逻辑

类似于ValueAnimator的过程

4.3 实例讲解

下面,我将用一个实例来说明如何通过自定义属性实现动画效果

  • 实现的动画效果:一个圆的颜色渐变
    属性动画 - 颜色变化

  • 自定义属性的逻辑如下:(需要自定义属性为圆的背景颜色)

自定义属性的逻辑

步骤1:设置对象类属性的set() & get()方法

设置对象类属性的set() & get()有两种方法:

  1. 通过继承原始类,直接给类加上该属性的 get()& set(),从而实现给对象加上该属性的 get()& set()

  2. 通过包装原始动画对象,间接给对象加上该属性的 get()&
    set()。即 用一个类来包装原始对象

此处主要使用第一种方式进行展示。

关于第二种方式的使用,会在下一节进行详细介绍。

MyView2.java

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()从而实现绘制逻辑
    // 绘制逻辑:先在初始点画圆,通过监听当前坐标值(currentPoint)的变化,每次变化都调用onDraw()重新绘制圆,从而实现圆的平移动画效果
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawCircle(500, 500, RADIUS, mPaint);
    }
}

步骤2:在布局文件加入自定义View控件

activity_main.xml

<?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.java

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()方法

MainActivity.java

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();
    }
}

效果图

属性动画 - 颜色变化

源码地址

Carson_Ho的Github地址


5. 特别注意:如何手动设置对象类属性的 set() & get()

5.1 背景说明

  • ObjectAnimator 类 自动赋给对象的属性 的本质是调用该对象属性的set() & get()方法进行赋值
  • 所以,ObjectAnimator.ofFloat(Object object, String property, float ....values)的第二个参数传入值的作用是:让ObjectAnimator类根据传入的属性名 去寻找 该对象对应属性名的 set() & get()方法,从而进行对象属性值的赋值

从上面的原理可知,如果想让对象的属性a的动画生效,属性a需要同时满足下面两个条件:

  1. 对象必须要提供属性a的set()方法

a. 如果没传递初始值,那么需要提供get()方法,因为系统要去拿属性a的初始值 b. 若该条件不满足,程序直接Crash

  1. 对象提供的 属性a的set()方法 对 属性a的改变 必须通过某种方法反映出来

a. 如带来ui上的变化 b. 若这条不满足,动画无效,但不会Crash)

上述条件,一般第二条都会满足,主要是在第一条

  1. 比如说:由于ViewsetWidth()并不是设置View的宽度,而是设置View的最大宽度和最小宽度的;所以通过setWidth()无法改变控件的宽度;所以对View视图的width做属性动画没有效果
  2. 具体请看下面Button按钮的例子
       Button  mButton = (Button) findViewById(R.id.Button);
        // 创建动画作用对象:此处以Button为例
        // 此Button的宽高设置具体为具体宽度200px

               ObjectAnimator.ofInt(mButton, "width", 500).setDuration(5000).start();
                 // 设置动画的对象

效果图

效果图:不会有动画效果

为什么没有动画效果呢?我们来看ViewsetWidth方法

public void setWidth(int pixels) {  
    mMaxWidth = mMinWidth = pixels;  
    mMaxWidthMode = mMinWidthMode = PIXELS;  
    // 因为setWidth()并不是设置View的宽度,而是设置Button的最大宽度和最小宽度的
    // 所以通过setWidth()无法改变控件的宽度
   // 所以对width属性做属性动画没有效果
  
    requestLayout();  
    invalidate();  
}  
  

@ViewDebug.ExportedProperty(category = "layout")  
public final int getWidth() {  
    return mRight - mLeft;  
    // getWidth的确是获取View的宽度
}  

5.2 问题

那么,针对上述对象属性的set()不是设置属性 或 根本没有set() / get ()的情况应该如何处理?

5.3 解决方案

手动设置对象类属性的set() & get()。共有两种方法:

  1. 通过继承原始类,直接给类加上该属性的 get()& set(),从而实现给对象加上该属性的 get()& set()

  2. 通过包装原始动画对象,间接给对象加上该属性的 get()&
    set()。即 用一个类来包装原始对象

对于第一种方法,在上面的例子已经说明;下面主要讲解第二种方法:通过包装原始动画对象,间接给对象加上该属性的get()& set()

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

还是采用上述 Button 按钮的例子

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();
        }

    }

}

效果图

效果图 - 动画有效


6. 与ValueAnimator类对比

对比于属性动画中另外一个比较核心的使用类:ValueAnimator类:

6.1 相同点

二者都属于属性动画,本质上都是一致的:先改变值,然后 赋值 给对象的属性从而实现动画效果。

6.2 不同点

二者的区别在于:赋值的操作是手动 or 自动

  • ValueAnimator 类是先改变值,然后 手动赋值 给对象的属性从而实现动画;是 间接 对对象属性进行操作;

ValueAnimator 类本质上是一种 改变 值 的操作机制

  • ObjectAnimator类是先改变值,然后 自动赋值 给对象的属性从而实现动画;是 直接 对对象属性进行操作;

可以理解为:ObjectAnimator更加智能、自动化程度更高

至此,关于属性动画中的核心使用类ObjectAnimator讲解完毕


7. 总结

  • 本文对Android 属性动画中的最核心的 ObjectAnimator类进行全面 & 详细介绍
  • 接下来,我将继续对Android的相关知识进行分析,感兴趣的同学可以继续关注本人的技术博客Carson_Ho的技术博客

请点赞!因为你们的赞同/鼓励是我写作的最大动力!