Android 属性动画ValueAnimator

成功的事业离不开坚如磐石的意志。成功路上的坎坷,意志脆弱者是根本无法超越的。——富兰克林·罗斯福

老式电影胶片就是逐帧动画的工作原理,很简单,将一个完整的电影拆分成一张张单独的图片,然后再将它们连贯起来进行播放。所以我们总是看到一个轮子在不停的转圈,就是在拖动一张张图片。动画分为两类,视图动画 和 属性动画。视图动图包括补间动画和帧动画,属性动画包括ValueAnimator 和 ObjectAnimator。为什么在Android 3.0 引入属性动画?

补间动画有一个致命的缺陷,比如现在屏幕的左上角有一个按钮,我们通过补间动画将它移动到了屏幕的右下角,你可以去尝试点击一下这个按钮,点击事件是绝对不会触发,因为这个按钮还是停留在屏幕的左上角,只不过补间动画将这个按钮绘制到了屏幕的右下角而已。
下面简单看一下这个动画的代码实现

<?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="vertical">

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="Start Anim" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#03A9F4"
        android:padding="10dp"
        android:text="Hello World"
        android:textColor="@android:color/white" />

</LinearLayout>

接下来给Buttom 和 TextView 添加单击事件,当点击TextView时,弹出Toast提示,当点击Buttom,TextView 移动

public class MainActivity extends AppCompatActivity {

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

        Button btn = findViewById(R.id.btn);
        final TextView tv = findViewById(R.id.tv);

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                TranslateAnimation translateAnimation = new TranslateAnimation(Animation.ABSOLUTE, 0, Animation.ABSOLUTE, 400,
                        Animation.ABSOLUTE, 0, Animation.ABSOLUTE, 200);
                translateAnimation.setFillAfter(true);
                translateAnimation.setDuration(2000);
                tv.startAnimation(translateAnimation);
            }
        });

        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "go", Toast.LENGTH_SHORT).show();
            }
        });
    }
}

补间动画还有一个缺陷,它只能够实现移动、缩放、旋转和淡入淡出这四种动画操作,那如果我们希望可以对View的背景色进行动态地改变呢?它实现不了,属性动画因为对控件的属性进行修改,它可以轻易实现背景颜色的动态改变。

ValueAnimator

        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 400);
        valueAnimator.setDuration(1000);
        valueAnimator.start();

ValueAnimator的ofFloat()方法就可以构建出一个0到400的动画,从这段代码中可以出看,ValueAnimator没有跟任何控件想关联,动画时长是1秒,然后开始动画,这也正好说明ValueAnimator只对值进行动画运算,而不是针对控件的。我们需要监听动画过程来自己对控件进行操作。

        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 400);
        valueAnimator.setDuration(1000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float curValue = (float) animation.getAnimatedValue();
                Log.d("tag", "----curValue:" + curValue);
            }
        });

        valueAnimator.start();

我们通过addUpdateListener()方法来添加一个动画的监听器,在监听传回的结果中,animator表达当前ValueAnimation实例,通过animation.getAnimatedValue()函数得到当前值,运行结果:

06-10 20:20:34.855 23008-23008/com.as.propertyanimator D/tag: ----curValue:0.0
06-10 20:20:35.063 23008-23008/com.as.propertyanimator D/tag: ----curValue:0.0
06-10 20:20:35.198 23008-23008/com.as.propertyanimator D/tag: ----curValue:1.822114
06-10 20:20:35.254 23008-23008/com.as.propertyanimator D/tag: ----curValue:7.4239135
06-10 20:20:35.267 23008-23008/com.as.propertyanimator D/tag: ----curValue:11.401903
06-10 20:20:35.280 23008-23008/com.as.propertyanimator D/tag: ----curValue:15.465462
06-10 20:20:35.299 23008-23008/com.as.propertyanimator D/tag: ----curValue:20.118963
06-10 20:20:35.317 23008-23008/com.as.propertyanimator D/tag: ----curValue:25.347496
06-10 20:20:35.335 23008-23008/com.as.propertyanimator D/tag: ----curValue:31.134438

06-10 20:20:36.110 23008-23008/com.as.propertyanimator D/tag: ----curValue:398.00473
06-10 20:20:36.128 23008-23008/com.as.propertyanimator D/tag: ----curValue:399.3332
06-10 20:20:36.146 23008-23008/com.as.propertyanimator D/tag: ----curValue:399.93683
06-10 20:20:36.165 23008-23008/com.as.propertyanimator D/tag: ----curValue:400.0

对指定值区间进行动画运算,我们对运算过程进行监听来自己操作控件,总而言之

  • ValueAnimator 只负责对指定值区间进行动画运算
  • 我们需要对运算过程进行监听,然后自己对控件执行动画操作

ValueAnimator 使用实例

下面使用ValueAnimator 来实现补间动画的单机区域问题中的例子,看看是否仍存在单机区域问题
布局代码与上一个例子相同

<?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="vertical">

    <Button
        android:id="@+id/btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="Start Anim" />

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

</LinearLayout>

分别给Buttom 和 TextView 添加单击事件,当点击TextView时,弹出Toast提示,当点击Buttom,TextView 开始移动。

public class MainActivity extends AppCompatActivity {

    private TextView tv;

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

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

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

        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "go", Toast.LENGTH_SHORT).show();
            }
        });

    }

    //当点击Button 的时候调用translate 函数执行动画操作,在单击TextView 弹出Toast提示
    private void translate() {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 400);

        valueAnimator.setDuration(2000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int curValue = (int) animation.getAnimatedValue();
                //通过layout 函数改变TextView 的位置,而layout函数在改变控件位置时是永久的,即通过更改left ,top,right,bottom这四个点的坐标来更改坐标位置,而不仅仅是视觉上画在哪个位置上
                tv.layout(curValue, curValue, tv.getWidth() + curValue, tv.getHeight() + curValue);
            }
        });
        valueAnimator.start();
    }
...

TextView 的运动轨迹从屏幕左上角(0,0)点运行到(400,400)点

在上面的列子中,我们使用了ofFloat 和 ofInt 函数,下面看下它的具体声明

public static ValueAnimator ofInt(int... values)
public static ValueAnimator ofFloat(float... values)

它们的参数类型都是可变参数,所以我们可以传入任何数量的值,传进入的列表就表示动画的变化方位,比如ofInt(100,400,200)就表示从数字100 变化到400在变化到数字200,所以我们传进去的数字越多,动画变化就越复杂。从参数类型中可以看出ofInt() 和 ofFloat() 唯一的区别就是传入的数字类型不同。

在上面例子的基础上,我们使用ofFloat() 函数来举一个列子

private void translateFloat() {
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0f, 400f,100f,300f);

        valueAnimator.setDuration(4000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                Float curValueFloat = (Float) animation.getAnimatedValue();
                int curValue = curValueFloat.intValue();
                tv.layout(curValue, curValue, tv.getWidth() + curValue, tv.getHeight() + curValue);
            }
        });
        valueAnimator.start();
    }

在这个列子中,我们使用 ValueAnimator.ofFloat(0f, 400f,100f,300f) 构造了一个比较复杂的动画,值从0变到400,再回到100,最后变成300。
所以,在单机按钮之后,TextView 会从(0,0)点移动到(400,400)点,再运动到(100,100)点,最后运动到(300,300)点。



大家可能会疑问,为什么要转换成Float 类型,我们先来看看getAnimatorValue()函数声明

public Object getAnimatedValue()

它返回的是Object类型,那我们怎么知道要转换的类型呢?哎,我们在设置动画初始值使用的是ofFloat()函数,所以每个值的类型必定是Float类型,我们获取到的类型也必然是Float类型。在得到当前运动点后,通过layout() 函数将TextView 移动到指定位置即可。

常用函数

// 设置动画时长
public ValueAnimator setDuration(long duration)

// 获取当前运动点的值
public Object getAnimatedValue()

// 开始动画
void start()

// 设置循环次数,INFINITE 表示无限循环,0表示不循环
public void setRepeatCount(int value)

/**设置循环模式  
*RESTART 表示正序重新开始
* REVERSE 表示倒序重新开始
*/
public void setRepeatMode(@RepeatMode int value)

//取消动画
void cancel()

下面举个例子
首先是布局代码,我们将两个按钮放到一列,将TextView 放到中间

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

    <Button
        android:id="@+id/btnStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="Start Anim" />

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:background="#03A9F4"
        android:padding="10dp"
        android:text="Hello World"
        android:textColor="@android:color/white" />

    <Button
        android:id="@+id/btnCancel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/btnStart"
        android:layout_marginTop="30dp"
        android:padding="10dp"
        android:text="Cancel Anim" />

</RelativeLayout>

下面看看两个按钮的操作

public class MainActivity extends AppCompatActivity {

    private TextView tv;
    private ValueAnimator valueAnimatorRepeat;

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

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

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

        btnCancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                valueAnimatorRepeat.cancel();
            }
        });

        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "go", Toast.LENGTH_SHORT).show();
            }
        });

    }

    private void doRepeatAnimator() {
        valueAnimatorRepeat = ValueAnimator.ofInt(0, 400);
        valueAnimatorRepeat.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int curValue = (int) animation.getAnimatedValue();
                tv.layout(tv.getLeft(), curValue, tv.getRight(), tv.getHeight() + curValue);
            }
        });
        valueAnimatorRepeat.setRepeatMode(ValueAnimator.REVERSE); //倒叙重新开始
        valueAnimatorRepeat.setRepeatCount(ValueAnimator.INFINITE); // 无限循环
        valueAnimatorRepeat.setDuration(2500);
        valueAnimatorRepeat.start();
    }
}

当点击btnStart时,调用doRepeatAnimator()函数,返回ValueAnimator对象,并将其赋给valueAnimatorRepeat,下面来看看这段代码

    private void doRepeatAnimator() {
        valueAnimatorRepeat = ValueAnimator.ofInt(0, 400);
        valueAnimatorRepeat.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int curValue = (int) animation.getAnimatedValue();
                tv.layout(tv.getLeft(), curValue, tv.getRight(), tv.getHeight() + curValue);
            }
        });
        valueAnimatorRepeat.setRepeatMode(ValueAnimator.REVERSE); //倒叙重新开始
        valueAnimatorRepeat.setRepeatCount(ValueAnimator.INFINITE); // 无限循环
        valueAnimatorRepeat.setDuration(2500);
        valueAnimatorRepeat.start();
    }

在这里,我们构造一个ValueAnimator,动画方位时0到400,设置重复次数无限循环,重复模式为倒序。当活动结束的时候,必须调用cancel()函数取消动画,否则动画将会无限循环,导致View无法释放,进一步导致Activity无法释放,最终引起内存泄漏。


自定义插值器

我们通过ofInt(0,400)定义了动画的区间值 0 到400,然后通过添加AnimatorUpdateListener来监听动画的实时变化。那么问题来了,0到400之间的值是怎么变化的呢?像我们跑步,还有得快,有的慢,这个值是匀速变化的吗?如果是,想让他一直加入该怎么办呢?这就是插值器的作用。

插值器就是控制动画区间值如何被计算出来,比如LinearInterpolator插值器表示匀速返回区间内的值,等等其它插值器。

在自定义插值器之前,先看看系统自带的插值器是如何实现的,比如LinearInterpolator

public class LinearInterpolator  implements Interpolator {

    public LinearInterpolator() {
    }

    public LinearInterpolator(Context context, AttributeSet attrs) {
    }

    public float getInterpolation(float input) {
        return input;
    }
}

LinearInterpolator 实现了Interpolator 接口,而Interpolator 接口直接继承TimeInterpolator,并且没有添加任何其它的方法。

package android.animation;

public interface TimeInterpolator {    
    float getInterpolation(float input);
}

参数input :它的取值方位0到1,表示当前动画的进度,0表示动画开始,1表示动画结束,0.5表示动画中间的位置。表示当前的动画参数是匀速增加的,动画进度就是动画在时间上的进度,随着时间推移,动画自然从0到1逐渐增加。input参数相当于时间的概念,我们通过setDuration()指定了动画时长。
返回值:表示当前实际想要显示的进度,取值可以超过1,表示超过目标值,小于0 表示小于开始值。

        ValueAnimator valueAnimator = ValueAnimator.ofInt(100, 500);
        valueAnimator.setDuration(1000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int curValue = (int) animation.getAnimatedValue();
                tv.layout(curValue, curValue, tv.getWidth() + curValue, tv.getHeight() + curValue);
            }
        });
        valueAnimator.start();

animation.getAnimatedValue() 得到当前值是怎么来的?看下面的计算公式,目前可以这么理解

当前的值 = 100 + (400 - 100 ) * 显示进度

其中,100和400就是我们设置ofFloat (100,400)中的值

input 参数就表示当前动画进度,而返回值则表示当前动画的数值进度。

我们自定义插值器

package com.as.propertyanimator;

import android.animation.TimeInterpolator;

public class MyInterpolator implements TimeInterpolator {
    @Override
    public float getInterpolation(float input) {
        return 1 - input;
    }
}

在这个自定义插值器中我们将进度反转过来,当传入0的时候,让它的数值进度在完成的位置,当完成的时候,让它的数值进度在开始位置。

private void myInterpolator() {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 300);

        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int curValue = (int) animation.getAnimatedValue();
                tv.layout(tv.getLeft(), curValue, tv.getRight(), tv.getHeight() + curValue);
            }
        });
        valueAnimator.setDuration(1000);
        valueAnimator.setInterpolator(new MyInterpolator());
        valueAnimator.start();
    }

Evaluator(求值器)

上图讲述了从定义动画的数值区间到在AnimatorUpdateListener中得到当前动画所对应数值的整个过程。
这4个步骤的具体含义如下
(1)ofInt(0,400) 表示指定动画的数值区间,从0运动到400
(2)插值器:在动画开始后,返回当前动画进度所对应的数值进度,但这个数值进度是以小数表示的,如0.2。
(3)Evaluator:我们通过监听器拿到的是当前动画所对应的具体数值,而不是用小数表示的数值。那么必须有一个地方根据档期那数值进度转化为对应数值,这个地方就是Evaluator。Evaluator用于将插值器返回的数值进度转化为对应数值。
(4)监听器返回:我们通过AnimatorUpdateListener 监听器中使用animation.getAnimatedValue()函数拿到Evaluator中返回的数值。

讲了这么多,Evaluator其实就是一个转换器,能把小数进度转换成对应的数值。

在设置Evaluator时,是通过animator.setEvaluator()函数来实现的,比如:

       ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 300);

        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int curValue = (int) animation.getAnimatedValue();
                tv.layout(tv.getLeft(), curValue, tv.getRight(), tv.getHeight() + curValue);
            }
        });
        valueAnimator.setDuration(1000);
        valueAnimator.setEvaluator(new IntEvaluator());
        valueAnimator.setInterpolator(new BounceInterpolator());
        valueAnimator.start();

我们设置了IntEvaluator,用来计算数值进度所对应的数值。但在此之前,我们在使用ofInt()函数时,从来没有定义过使用IntEvaluator来转换值,能正常运行是因为ofInt()和ofFloat()都是系统直接提供的函数,所以会有默认的插值器和Evaluator可供使用。下面看下IntEvaluator内部是怎么实现的。

package android.animation;

/**
 * This evaluator can be used to perform type interpolation between <code>int</code> values.
 */
public class IntEvaluator implements TypeEvaluator<Integer> {
    public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
        int startInt = startValue;
        return (int)(startInt + fraction * (endValue - startInt));
    }
}
  • fraction 参数就是插值器中返回的值,表示当前动画的百分比进度。
  • startValue 和 endValue 分别对应ofInt(int start,int end)函数中start 和 end值
  • 返回值就是当前数值进度所对应的具体数值
    假设当我们定义的动画ofInt(200,500)进行到数值进度10%的时候,我们来算下具体值
 return (int)(startInt + fraction * (endValue - startInt));

当前值 = 200 + 0.1 * (500 - 200)

简单实现Evaluator

下面仿照一个IntEvaluator 的实现方法,自定义一个MyEvalutor

package com.as.propertyanimator;

import android.animation.TypeEvaluator;

public class MyEvaluator implements TypeEvaluator<Integer> {

    @Override
    public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
        return (int) (200 + startValue + fraction * (endValue - startValue));
    }
}

我们在IntEvaluator的基础上修改了一下,让它返回时增加200。当我们定义一个ofInt(0,500)时,它的实际返回值区间时(200,700)。



很明显,TextView 的动画位置都向下移动了200px。

自定义插值器实现倒序输出

package com.as.propertyanimator;

import android.animation.TypeEvaluator;

/**
 * 倒叙输出
 */
public class ReverseEvaluator implements TypeEvaluator<Integer> {

    @Override
    public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
        return (int) (endValue - fraction * (endValue - startValue));
    }
}

fraction * (endValue - startValue) 表示动画的实际运动距离,我们用endValue 减去实际运动距离表示距离终点越来越远,也就实现了从终点出发,到起点的效果

private void myReverseEvaluator() {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 300);

        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int curValue = (int) animation.getAnimatedValue();
                tv.layout(tv.getLeft(), curValue, tv.getRight(), tv.getHeight() + curValue);
            }
        });
        valueAnimator.setDuration(1000);
        valueAnimator.setEvaluator(new ReverseEvaluator());
        valueAnimator.start();
    }

我们来实现颜色过渡转换

private void colorEvaluator() {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(0xffffff00, 0xff0000ff);

        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                int curValue = (int) animation.getAnimatedValue();
                tv.setBackgroundColor(curValue);
            }
        });
        valueAnimator.setDuration(5000);
        valueAnimator.setEvaluator(new ArgbEvaluator());
        valueAnimator.start();
    }

我们将动画的定义为(0xffffff00, 0xff0000ff),即从黄色变为蓝色。在监听事件中,我们根据当前传回的颜色值,将其设置为TextView 的背景色。


这里需要注意的是,必须使用ofInt()函数来定义颜色的取值范围,并且颜色必须包含A,R,G,B 4个值,我们来简单看一下ArgbEvaluator的源码

/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.animation;

import android.annotation.UnsupportedAppUsage;

/**
 * This evaluator can be used to perform type interpolation between integer
 * values that represent ARGB colors.
 */
public class ArgbEvaluator implements TypeEvaluator {
   
    public Object evaluate(float fraction, Object startValue, Object endValue) {
        int startInt = (Integer) startValue;
        float startA = ((startInt >> 24) & 0xff) / 255.0f;
        float startR = ((startInt >> 16) & 0xff) / 255.0f;
        float startG = ((startInt >>  8) & 0xff) / 255.0f;
        float startB = ( startInt        & 0xff) / 255.0f;

        int endInt = (Integer) endValue;
        float endA = ((endInt >> 24) & 0xff) / 255.0f;
        float endR = ((endInt >> 16) & 0xff) / 255.0f;
        float endG = ((endInt >>  8) & 0xff) / 255.0f;
        float endB = ( endInt        & 0xff) / 255.0f;

        // compute the interpolated color in linear space
        float a = startA + fraction * (endA - startA); //当前进度的透明度
        float r = startR + fraction * (endR - startR); //当前进度下的红色值
        float g = startG + fraction * (endG - startG); //当前进度下的绿色值
        float b = startB + fraction * (endB - startB); //当前进度下的蓝色值

        return Math.round(a) << 24 | Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b);
    }
}

这段代码分为三部分,第一部分根据startValue 起初 A,R,G,B中各个色彩的初始值,第二部分根据endValue 求出 A,R,G,B 各个色彩的结束值,第三部分根据当前动画的百分比进度求出对应的数值,最后通过或运算把结果拼接到一个整型 4个字节中。

这段代码根据位移和与运算求出颜色中A,R,G,B 各个部分对应的值,


如果大家对位移和与运算及如何得到指定为不太了解的画,可以看我这篇博客算法之美,位运算。

ValueAnimator 进阶 ofObject

ofInt()函数只能传入Integer类型,ofFloat只能传入Float 类型,如果我们需要操作其它类型的变量该怎么办呢?其实,ValueAnimator还有一个函数ofObject(),可以传入任何类型的变量,该函数的定义如下:

 public static ValueAnimator ofObject(TypeEvaluator evaluator, Object... values)

它有两个参数,第一个参数是自定义Evaluator;第二个参数是可变参数,属于Object类型。既然Object对象是我们自定义的,那么进度值的转换过程也由我们来做,否则系统不可能知道转换出来的具体值是什么。

下面我们尝试使用ofObject函数实现下面的效果,将TextView 中的字母从A变化到Z。

package com.as.propertyanimator;

import android.animation.TypeEvaluator;

/**
 * 动画求值器从字母A到字母Z
 */
public class CharEvaluator implements TypeEvaluator<Character> {

    @Override
    public Character evaluate(float fraction, Character startValue, Character endValue) {
        int startInt = startValue; //A = 65
        int endInt = endValue;     //Z = 90
        return (char) (startInt + fraction * (endInt - startInt)); //当前字符
    }

}

private void objectEvaluator() {
        //我们要实现的动画效果是从字母A到字母Z
        ValueAnimator valueAnimator = ValueAnimator.ofObject(new CharEvaluator(), 'A', 'Z');

        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //获取当前动画的值==>获取到当前字母
                char curValue = (char) animation.getAnimatedValue();
                tv.setText(String.valueOf(curValue));
            }
        });
        //动画匀速==>线性变化
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.setDuration(5000);
        valueAnimator.start();
    }

我们自定义了一个CharEvaluator;在初始动画时,传入的是char对象,一个是字母A,另一个是字母Z。我们先来了解下ASCIi 码表中数字与字符的转换方法,每个字符都有一个对应的数字,字母A到Z对应的数字区间65到90,在程序中,我们可以将数字转换为字符,也可以将字符转化为数字。
数字转字符

char c = (char) 65; //得到的c 就是大写字母A
char temp = 'A';
int num = (int) temp; // 65

我们来实现一个小球落地的动画

先用shape 标签实现一个圆形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>

然后实现布局文件

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

    <Button
        android:id="@+id/btnStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:text="Start Anim" />


    <Button
        android:id="@+id/btnCancel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/btnStart"
        android:layout_marginTop="30dp"
        android:padding="10dp"
        android:text="Cancel Anim" />

    <TextView
        android:id="@+id/tv"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_below="@+id/btnCancel"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="10dp"
        android:background="@drawable/circle"
        android:padding="10dp"
        android:textColor="@android:color/white" />

</RelativeLayout>

将上面的圆形shape 作为ImageView 的源文件显示出来,接着实现动画

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;
        int y = (int) (startValue.y + (fraction * (endValue.y - startValue.y)));
        mPoint.y = y;
        return mPoint;
    }

}


private void ballEvaluator() {
        //定义了球的位置
        ValueAnimator valueAnimator = ValueAnimator.ofObject(new FallingBallEvaluator(), new Point(0, 0), new Point(500, 500));
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //获取当前动画的值==>获取到当前x 和 y 位置
                Point curValue = (Point) animation.getAnimatedValue();
                //将球移动到指定位置
                tv.layout(curValue.x, curValue.y, curValue.x + tv.getWidth(), curValue.y + tv.getHeight());
            }
        });
        // 弹跳插值器,模拟了控件自由落地后回弹的效果
        valueAnimator.setInterpolator(new BounceInterpolator());
        valueAnimator.setDuration(3000);
        valueAnimator.start();
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342