Android 属性动画ObjectAnimator

做一个有信义的人胜似做一个有名气的人。 ——美国前总统罗斯福

上篇博客着重讲解了ValueAnimator,但ValueAnimator有一个缺点,只能对动画中的数值做计算,如果想要对哪个控件进行操作,我们要监听动画过程,相比于补间动画要繁琐的多了。

为了能让动画直接应用控件,ObjectAnimator继承自ValueAnimator,它也重写了几个函数,比如ofInt(),ofFloat()等。我们先看看利用ObjectAnimator重写的ofFloat()函数如何实现一个改变透明度的动画:
先看下我们的布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btnStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Start Anim" />

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:background="#FF5722"
        android:padding="10dp"
        android:text="Hello"
        android:textColor="@android:color/white" />

</LinearLayout>

非常简单,接着看看我们的代码实现

package com.as.propertyanimator;

import android.animation.ObjectAnimator;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity;

public class ObjectAnimatorActivity extends AppCompatActivity {

    private TextView tv;

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

        Button btnStart = findViewById(R.id.btnStart);
        tv = findViewById(R.id.tv);

        btnStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                objAnimatorAlpha();
            }
        });
    }


    private void objAnimatorAlpha() {
        ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "alpha", 1, 0, 1);
        alpha.setDuration(2000);
        alpha.start();
    }

}

当点击按钮时执行动画,将TextView 的透明度从1 变成 0 在变成1的过程。从上面的代码中可以看到,构造ObjectAnimator的方法简单。

public static ObjectAnimator ofFloat(Object target, String propertyName, float... values)
  • 第一个参数 用于指定动画操作的哪个控件
  • 第二个参数 用于指定动画要操作这个控件的哪个属性
  • 第三个参数 这个属性值如何变化,在上面的代码中,就是将TextView 的透明度从1 变成 0 在变成1的过程。

我们在来看一下如何实现旋转效果

private void objAnimatorRotation() {
        ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "rotation", 0, 270, 0);
        alpha.setDuration(10000);
        alpha.start();
    }

TextView 从0度旋转到270度,然后又旋转到0度。
从代码中我们可以看出,我们只要改变ofFloat()函数第二个参数的值,就可以实现对应的动画。那我们怎么知道第二个参数的值是什么?

set函数

ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "rotation", 0, 270, 0);

TextView 控件有rotation这个属性吗?没有,不光TextView没有,连它的父类View也没有。那怎么改变这个值呢?其实ObjectAnimator并不是根据控件的属性来改变,而是通过指定属性所对应的set函数来改变的。上面指定改变rotation属性值,他会到TextView 这个指定控件中寻找setRotation函数来改变控件中对用的值。

在View中,有关动画共有下面几组set函数

// 1 透明度
public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha)

// 2 旋转度数
public void setRotation(float rotation) 
public void setRotationX(float rotationX)
public void setRotationY(float rotationY)

// 3 平移
public void setTranslationX(float translationX)
public void setTranslationY(float translationY)


// 4 缩放
public void setScaleX(float scaleX)
public void setScaleY(float scaleY)

可以看到,在View中已经实现了与alpha ,rotation,translate,scale相关的set函数,所以我们在ObjectAnimator 时可以直接使用。
x
我们先做一个总结:

  • 要使用ObjectAnimator来构造动画,要操作的控件必须存在对应属性的set函数,而且参数类型必须与构造所使用的ofFloat()或者ofInt()函数一致。
  • set函数的命名必须采用骆驼拼写法,既set后每个单词首字母大写,其余字母大小,类似于setPropertyName所对应的属性为propertyName
// 表示围绕Z轴旋转
public void setRotation(float rotation) 
// 表示围绕X轴旋转
public void setRotationX(float rotationX)
// 表示围绕Y轴旋转
public void setRotationY(float rotationY)

先来看看setRotationX()函数的使用方法与效果

private void objAnimatorRotationX() {
        ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "rotationX", 0, 180, 0);
        alpha.setDuration(1000 * 8);
        alpha.start();  
    }

从效果图中可以明显看出是围绕X轴旋转,我们设定为从0度到180度,再返回0度。

再来看看setRotationY()

private void objAnimatorRotationY() {
        ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "rotationY", 0, 180, 0);
        alpha.setDuration(1000 * 8);
        alpha.start();
    }

从效果图中可以明显看出是围绕Y轴旋转。最后来看看setRotation()函数

private void objAnimatorRotation() {
        ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "rotation", 0, 270, 0);
        alpha.setDuration(1000 * 8);
        alpha.start();
    }

平移

  • setTranslationX(float translationX):表示在X轴上的平移距离,以当前控件为原点,向又为正方向,参数translationX表示移动的距离
  • setTranslationY(float translationY):表示在Y轴上的平移距离,以当前控件为原点,向下为正方向,参数translationY表示移动的距离

先来看看setTranslationX(float translationX)函数的使用方法与效果

 private void objAnimatorTranslationX() {
        ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "translationX", 0, 200, -200, 0);
        alpha.setDuration(1000 * 4);
        alpha.start();
    }

我们在构造动画时,指定的移动距离时(0,200,-200,0),所以控件会先从自身位置向右移动200个像素,然后移动到距离远点-200px,最后回到原点。

再来看看setTranslationY() 函数的使用方法和效果

private void objAnimatorTranslationY() {
        ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "translationY", 0, 200, -50, 0);
        alpha.setDuration(1000 * 4);
        alpha.start();
    }

可以看出,每次移动距离的计算都是以原点为中心点,在上述代码中,初始动画为ObjectAnimator.ofFloat(tv, "translationY", 0, 200, -50, 0);表示首先从0移动到正方向200px的位置,然后在移动到负方向50px的位置,最后移动到原点。

缩放

  • setScaleX(float scaleX) 在X轴上进行缩放,scaleX表示缩放倍数。
  • setScaleY(float scaleY) 在Y轴上进行缩放,scaleY表示缩放倍数。

先来看看 setScaleX()函数的使用方法与效果

private void objAnimatorScaleX() {
        ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "scaleX", 0, 4, 1);
        alpha.setDuration(1000 * 4);
        alpha.start();
    }

将TextView在X轴上进行缩放,从初始宽度的0倍放大到4倍,然后在缩小1倍。

再来看看setScaleY()

private void objAnimatorScaleY() {
        ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "scaleY", 0, 4, 1);
        alpha.setDuration(1000 * 4);
        alpha.start();
    }

为了更好的看到效果,为TextView 距离顶部100dp



将TextView在Y轴上进行缩放,从初始宽度的0倍放大到4倍,然后在缩小1倍。

ObjectAnimator动画原理

第一 在ObjectAnimator流程中,先通过插值器产生当前进度的百分比,然后通过Evaluator计算百分比所对应的数值。这两步与ValueAnimator的动画流程时完全一样的,唯一不同的是最后一步,在ValueAnimator中,需要通过添加监听器来监听当前的数字,而ObjectAnimator,则先根据属性拼接曾对应的set函数,比如scaleY就是将第一个首字母大写,后面的不变,与set拼接后得到setScaleY,然后通过反射找到对应控件的setScaleY(float scaleY)函数,并将当前的数值作为setScaleY(float scaleY)函数的参数传入。

第二 如何确定函数的参数类型?我们知道如何找到对应的函数名,那么对应方法中的参数类型如何确定?是在我们调用ofInt()函数或者ofFloat()函数时确定的。

比如我们的构造方法ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "scaleX", 0, 4, 1);由于构造时使用的时ofFloat()函数,所以中间值的类型应该是Float类型,最后一步拼装出来的是setScaleY(float xxx)的样式,这是,系统利用反射找到它,并把当前的动画数值作为参数传入。

如果没有setScaleY(float xxx)的函数,只实现了setScaleY(int xxx)函数怎么办呢?在这里,虽然函数名一样,但参数类型不一样,系统就会报错,如下所示:

Method setScaleY() with type int not found on target class class androidx.appcompat.widget.AppCompatTextView

没有找到指定函数对应的参数类型

第三 调用set函数以后怎么办?从ObjectAnimator流程中可以看到,ObjectAnimator只负责把动画过程中的数值当作参数传递到set函数中就结束了。set函数对控件的操作还是由我们自己编写,这里以setScaleY()函数为例。

public void setScaleY(float scaleY) {
        if (scaleY != getScaleY()) {
            scaleY = sanitizeFloatPropertyValue(scaleY, "scaleY");
            invalidateViewProperty(true, false);
            mRenderNode.setScaleY(scaleY);
            invalidateViewProperty(false, true);

            invalidateParentIfNeededAndWasQuickRejected();
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
    }

大家不必仔细看这段代码,因为这些代码需要读懂View的整体流程以后才能看得懂。重新设置当前控件参数,调用invalidate()函数强制重绘。

set 函数的调用频率是多少?由于动画在进行时每隔十几毫秒会刷新一次,所以set函数也会每隔十几毫秒被调用一次。

自定义ObjectAnimator属性

public static ObjectAnimator ofFloat(Object target, String propertyName, float... values)
public static ObjectAnimator ofInt(Object target, String propertyName, int... values)
public static ObjectAnimator ofObject(Object target, String propertyName,
            TypeEvaluator evaluator, Object... values)

相比于ValueAnimator,ObjectAnimator的每个构造函数多了一个propertyName属性,用于指定要做的属性。

接下来我们通过ofObject()函数来举例。我们使用ObjectAnimator实现ValueAnimator中的抛物动画例子,当单机按钮,圆球开始向下做抛物运动

我们使用ObjectAnimator,则需要操作某个控件的某个属性,所以我们从ImageView 派生一个类来表示圆球

package com.as.propertyanimator;

import android.content.Context;
import android.graphics.Point;
import android.util.AttributeSet;

import androidx.appcompat.widget.AppCompatImageView;

public class FallingBallImageView extends AppCompatImageView {

    private int left = 0;
    private int top = 0;
    private int bottom;
    private int right;

    public FallingBallImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FallingBallImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        //left top 只赋值一次
        if (this.left != 0) {
            return;
        }
        this.left = left;
        this.top = top;
        this.right = right;
        this.bottom = bottom;
    }

    public void setFallingPos(Point point) {
        //从起始位置开始
        layout(left + point.x, top + point.y, point.x + right, point.y + bottom);

        //这两行代码运行结果一样
//        layout(left + point.x, top + point.y, left + point.x + getWidth(), top + point.y + getHeight());
    }
}

在使用ObjectAnimator时,布局代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <androidx.appcompat.widget.AppCompatButton
        android:id="@+id/btnStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Start Anim" />

    <com.as.propertyanimator.FallingBallImageView
        android:id="@+id/tv"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_marginLeft="10dp"
        android:src="@drawable/circle" />

</LinearLayout>

drawable/circle.xml与上篇博客相同,是一个圆形。

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <solid android:color="#03A9F4" />
</shape>

在代码中,点击按钮开始动画

private FallingBallImageView ballIv;

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

        AppCompatButton btnStart = findViewById(R.id.btnStart);
        ballIv = findViewById(R.id.ballIv);

        btnStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                objAnimatorFallingBall();
            }
        });

    }


    private void objAnimatorFallingBall() {
        ObjectAnimator alpha = ObjectAnimator.ofObject(ballIv, "FallingPos",
                new FallingBallEvaluator(),
                new Point(0, 0), new Point(400, 400));
        alpha.setDuration(1000 * 2);
        alpha.start();
    }

着重看一下ObjectAnimator的构造方法,他要控制的对象是ballIv,对应的属性FallingPos,值从点(0,0) 运动到点(400,400)。其中FallingBallEvaluator的实现如下

package com.as.propertyanimator;

import android.animation.TypeEvaluator;
import android.graphics.Point;

/**
 * 蹦蹦求 从空中落到地面上
 */
public class FallingBallEvaluator implements TypeEvaluator<Point> {

    //蹦蹦求返回值
    private Point mPoint = new Point();

    @Override
    public Point evaluate(float fraction, Point startValue, Point endValue) {
        int x = (int) (startValue.x + (fraction * (endValue.x - startValue.x)));
        mPoint.x = x;
        if (fraction * 2 <= 1) {
            int y = (int) (startValue.y + (fraction * 2 * (endValue.y - startValue.y)));
            mPoint.y = y;
        } else {
            mPoint.y = endValue.y;
        }
        return mPoint;
    }

}


过程是:当单机按钮是开始动画,ObjectAnimator.ofObject()函数会根据FallingBallEvaluator实时得到当前Point值,然后到ballIv控件中去找FallingPos(Point point)函数,它的参数就是FallingBallEvaluator返回的Point对象,在找到setFallingPos(Point point)函数后,通过反射调用它,到这里,ObjectAnimator的任务就结束了,而在setFallingPos(Point point)函数中,我们会根据参数值实时改变圆形的位置。

何时需要实现属性对应的get函数

ObjectAnimator 有三个构造函数:ofFloat(),ofInt(),ofObject(),它们的最后一个参数都是可变参数,用于指定动画值的变化区间。

前面我们定义的都是多个值,既至少两个值之间的变化。如果我们只定义一个值呢?
先从TextView 继承一个子列,新建一个原来TextView 不存在的set函数,很明显,这是一个自定义属性值

package com.as.propertyanimator;

import android.content.Context;
import android.util.AttributeSet;

import androidx.appcompat.widget.AppCompatImageView;

public class CustomerTextView extends AppCompatImageView {

    public CustomerTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    public void setScaleSize(float num) {
        setScaleX(num);
    }

}

内部实现仍然使用的是setScaleX()函数,但我么在这里创建了一个TextView原来不存在的属性。
这么做自由道理,后面会讲述。

private CustomerTextView customerTv;

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

        AppCompatButton btnStart = findViewById(R.id.btnStart);
        customerTv = findViewById(R.id.customerTv);

        btnStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                objAnimatorScale();
            }
        });

    }

    private void objAnimatorScale() {
        ObjectAnimator alpha = ObjectAnimator.ofFloat(customerTv, "ScaleSize", 5);
        alpha.setDuration(1000 * 2);
        alpha.start();
    }

我们在这里只传递了一个变化值5,那它是从哪里开始变化呢?我们来看一下效果。


从效果图中可以看出,它是从0开始变化的,但在日志中已经发出了警告!

Method getScaleSize() with type null not found on target class class com.as.propertyanimator.CustomerTextView

意思是没有找到scaleSize属性所对应的getScaleSize()函数。

当且仅当我们只给动画设置一个值时,程序才会调用对应的get函数来得到动画初始值。如果动画没有初始值,则会取动画参数类型默认值为初始值。比如ofFloat()函数中使用的参数类型时Float类型,而Float类型的默认值是0,动画就会从0倍放到5倍大小;也就是系统虽然在找不到属性对应的get函数时会给出警告,但同时会使用系统的默认值作为初始值。

如果给自定义控件CustomerTextView 设置get函数,那么将get函数的返回值作为初始值。

public class CustomerTextView extends AppCompatImageView {

    public CustomerTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    public void setScaleSize(float num) {
        setScaleX(num);
    }

    public float getScaleSize() {
        return 0.5f;
    }

}

我们在getScaleSize()函数中返回0.5f,所以,当指定一个动画时,动画就会通过get函数来获取初始值。这里的动画缩放是从0.5倍开始的


从效果图中可以看出,动画的确是从0.5倍宽度开始缩放的。前面之所以不能直接使用setScaleX(float scaleX)函数来演示初始值,是因为在View 类中是存在getScaleX()函数的,我们必须找到一个不存在get函数的属性讲解才行。

对于ofInt(),ofFloat()函数而言,默认值都是0,而对于ofObject()函数而言,由于允许我们指定动画类型,所以不一定存在初始值,如果不传入起始值,程序会崩溃。

总结:当动画只有一个过渡值时,系统才会调用对应属性的get函数来得到动画初始值。当不存在get函数,则会取动画参数类型默认值为初始值;当无法取到动画类型默认值,则会直接崩溃。

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

推荐阅读更多精彩内容