*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
声明:本篇文章部分内容来自《Android开发艺术探索》。
概述
我们都知道对于属性动画可以对某个属性做动画,而插值器(TimeInterpolator)和估值器(TypeEvaluator)在其中扮演了重要角色,下面先了解下TimeInterpolator和TypeEvaluator。
TimeInterpolator(时间插值器):
- 作用:根据时间流逝的百分比计算出当前属性值改变的百分比。
- 系统已有的插值器:
①LinearInterpolator(线性插值器):匀速动画。
②AccelerateDecelerateInterpolator(加速减速插值器):动画两头慢,中间快。
③DecelerateInterpolator(减速插值器):动画越来越慢。
TypeEvaluator(类型估值算法,即估值器):
- 作用:根据当前属性改变的百分比来计算改变后的属性值。
- 系统已有的估值器:
①IntEvaluator:针对整型属性
②FloatEvaluator:针对浮点型属性
③ArgbEvaluator:针对Color属性
那么TimeInterpolator和TypeEvaluator是怎么协同工作的呢?
答:它们是实现非匀速动画的重要手段。属性动画是对属性做动画,属性要实现动画,首先由TimeInterpolator(插值器)根据时间流逝的百分比计算出当前属性值改变的百分比,并且插值器将这个百分比返回,这个时候插值器的工作就完成了。比如插值器返回的值是0.5,很显然我们要的不是0.5,而是当前属性的值,即当前属性变成了什么值,这就需要估值器根据当前属性改变的百分比来计算改变后的属性值,根据这个属性值,我们就可以设置当前属性的值了。
源码分析
上面的理论知识可能比较抽象,下面根据一个实例结合系统源码来分析一下:
下图表示一个匀速动画,采用了线性插值器和整型估值算法,在40ms内,View的x属性实现从0到40的变换。
由于动画的默认刷新率为10ms/帧,所以该动画将分为5帧进行。当动画进行到第三帧的时候,(x=20,t=20ms)当时间t=20ms的时候,时间流逝的百分比是0.5(20/40=0.5),即时间过了一半。这个百分比不是我们最终想要的,我们关心的是x的变化,那么x应该变化多少呢?插值器和估值算法就起作用了。拿线性插值器来说,线性插值器是实现匀速动画的,先看一下线性插值器的源码:
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
public LinearInterpolator() {
}
public LinearInterpolator(Context context, AttributeSet attrs) {
}
public float getInterpolation(float input) {
return input;
}
}
很显然,线性插值器的返回值和输入值一样(看getInterpolation方法),因此t=20ms时插值器返回的值是0.5,这意味着x属性的改变是0.5。这个时候插值器的工作已经完成了,插值器的工作就是根据时间流逝的百分比计算出当前属性值改变的百分比。我们得到了当前属性改变的百分比是0.5,即50%。下一步就是要算出x具体变为了什么值,这个时候估值算法就起作用了,它的作用就是根据当前属性改变的百分比来计算改变后的属性值。我们先看看系统提供的整型估值算法的源码:
public class IntEvaluator implements TypeEvaluator<Integer> {
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
int startInt = startValue;
return (int)(startInt + fraction * (endValue - startInt));
}
}
上述代码中的evaluate方法的三个参数分别表示估值小数(fraction),开始值(startValue)和结束值(endValue)。对于我们的这个例子就是0.5,0和40。我们将这三个值代入求值,即:0+0.5*(40 - 0)=20。没错,这就是当t=20ms时,x=20的由来。
其实对于插值器和估值器来说,除了系统提供的外,我们还可以自定义。实现方式也很简单,因为插值器和估值器都是一个接口,且内部都只有一个方法,我们只要实现接口就可以了,就可以做出很多绚丽的动画了。其中,自定义插值器需要实现Interpolator或者TimeInterpolator,自定义估值器需要实现TypeEvaluator。但是一般来说,插值器一般使用系统的就足够了,估值器一般自定义的可能会多一些,另外就是如果要对其他类型(非Int丶float丶color)做动画,必须自定义类型估值算法。
代码演示(一)
上一篇文章有提过抛物线动画,在这里我们用两种方式实现抛物线轨迹,一种是固定时间的抛物线,一种是固定长度的抛物线。
(1)球按照抛物线轨迹运行1.5秒(参照鸿阳大神的抛物线效果):
首先分析一下实现过程:
要实现抛物线的效果,水平方向100px/s,垂直方向加速度200px/s*s,你会发现我们只需要知道时间的变化,就可以求出某刻小球的水平位移(即x)和数值位移(即y)。那么公式就是:x = 200t ; y=at²。但是还有一个问题就是,我们要同时记录两个值并进行估值算法,所以我们可以创建一个Point类来保存这两个值。前面我们使用过ValueAnimator的ofFloat()和ofInt()方法,分别用于对浮点型和整型的数据进行动画操作的,但实际上ValueAnimator中还有一个ofObject()方法,是用于对任意对象进行动画操作的。但是相比于浮点型或整型数据,对象的动画操作明显要更复杂一些,因为系统将完全无法知道如何从初始对象过度到结束对象,因此这个时候我们就需要实现一个自己的TypeEvaluator来告知系统如何进行过度。这里我们要过度的对象就是Point对象。
首先,定义一个Point类,如下所示:
package com.wangjian.propertyanim;
/**
* 保存小球坐标的类
* Created by Administrator on 2016/2/2.
*/
public class Point {
float x;
float y;
public Point(){
}
public Point(float x,float y){
this.x = x;
this.y = y;
}
}
Point 类非常简单,x和y两个变量用于记录坐标的位置,并且提供了两个构造方法。
接下来自定义估值算法,来告知系统如何实现初始对象到结束对象的过度(对于插值器来说,我们就使用系统的线性插值器即可,无须自定义):
package com.wangjian.propertyanim;
import android.animation.TypeEvaluator;
/**
* 自定义估值算法
* Created by Administrator on 2016/2/2.
*/
public class MyTypeEvaluator implements TypeEvaluator<Point> {
@Override
public Point evaluate(float fraction, Point startValue, Point endValue) {
Point point = new Point();
point.x = 200 * (fraction * 1.5f);//fraction * 1.5f就是时间,fraction代表时间流逝的百分比,1.5代表1500ms即1.5s
point.y = 200 * (fraction * 1.5f) * (fraction * 1.5f);
return point;
}
}
对于自定义估值算法,你需要指定一个泛型,这里我们操作的是Point 对象,当我们指定Point 为泛型时,evaluate的返回值也是Point ,并且startValue和endValue也是Point 类型,这样就对了,在自定义估值算法中,我们就可以直接使用Point 对象了。但是你会发现一个问题就是,我们并没有用到startValue和endValue,而是使用了一个写死的1.5f,因为没必要,我们的抛物线是与时间挂钩的,不用他们也是完全可以的,这样写的话,出来的效果就是:球运行1.5s的轨迹图。(下面我们还会有一个例子,它的自定义估值算法就是以startValue和endValue来计算的)。
下面是布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/id_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/id_ball"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@mipmap/bol_blue" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:onClick="paowuxian"
android:layout_centerHorizontal="true"
android:text="抛物线" />
</RelativeLayout>
下面看MainActivity的代码:
package com.wangjian.propertyanim;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
public class MainActivity extends Activity {
private ImageView mBlueBall;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBlueBall = (ImageView) findViewById(R.id.id_ball);
}
/**
* 抛物线
* @param view
*/
public void paowuxian(View view){
//设置自定义的TypeEvaluator,起始属性
ValueAnimator valueAnimator = ValueAnimator.ofObject(new MyTypeEvaluator(), new Point(0, 0));
//设置持续时间
valueAnimator.setDuration(1500);
//设置线性时间插值器
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Point point = (Point) animation.getAnimatedValue();
mBlueBall.setX(point.x);
mBlueBall.setY(point.y);
Log.e("TAG",point.x+":"+point.y);
}
});
valueAnimator.start();
}
}
通过代码发现,我们调用ValueAnimator的ofObject()方法来构建ValueAnimator的实例,这里需要注意的是,ofObject()方法要求多传入一个TypeEvaluator参数,这里我们只需要传入刚才定义好的PointEvaluator的实例就可以了。还有我们设置了1500ms的持续时间,这里的1500ms和自定义估值器里面的1.5f不是一回事,也就是说我们就算设置的是2000ms的持续时间,得到的小球运行轨迹是一样的,因为自定义估值器里面的1.5f是写死的。
运行效果:
我们发现小球确实是抛物线运行,但是仔细看代码你会发现,我们并没有指定小球的落点位置,即Point对象的终止属性,那么小球落点的位置是哪里呢?其实我们可以算出来,即x=200*1.5=300;y=200*1.5*1.5=450。但是现在我们更换一下需求,要求小球刚好落在屏幕的底部,而不是求具体时间小球的轨迹。
(2)小球呈抛物线轨迹运行到屏幕的右边部(自己尝试的抛物线效果):
我们知道y=ax^2也是抛物线的一个公式,但是仔细看这个公式你会发现,它不是以t为基准了,而是y与x的关系,也就是说y的变化直接与x挂钩。并且对于y=ax^2,我们知道a越小,开口越大,所以为了抛物线效果好看,我们此次使用的a=0.001。
要实现这个效果,其实可以不用ofObject()方法,也就是说不用自定义估值算法,直接使用ofFloat()也是可以实现的,即直接使用系统的估值器。我们可以为x属性设置一个匀速动画,然后设置监听器来实现为x和y设置属性值。
布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/id_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/id_ball"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@mipmap/bol_blue" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:onClick="paowuxian"
android:layout_centerHorizontal="true"
android:text="抛物线" />
</RelativeLayout>
MainActivity:
package com.wangjian.propertyanim;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
public class MainActivity extends Activity {
private ImageView mBlueBall;
private int mScreenWidth;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBlueBall = (ImageView) findViewById(R.id.id_ball);
WindowManager wm = (WindowManager)getSystemService(Context.WINDOW_SERVICE);
mScreenWidth = wm.getDefaultDisplay().getWidth();
}
/**
* 抛物线
* @param view
*/
public void paowuxian(View view){
//为x的属性值设置动画
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0,mScreenWidth-mBlueBall.getWidth());
//设置持续时间
valueAnimator.setDuration(3000);
//设置线性时间插值器
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float x = (float) animation.getAnimatedValue();//获取x的值
float y = 0.001f*x*x;//y=ax^2
mBlueBall.setX(x);
mBlueBall.setY(y);
Log.e("TAG",x+":"+y);
}
});
valueAnimator.start();
}
}
代码不多,主要的逻辑上面也已经说了。
下面是运行效果:
确实,小球在碰到右边部就停止了,是我们想要的效果,当然也可以使用自定义估值算法,但是自定义估值算法你需要求出球碰到右边部的时候的y坐标作为终止属性,其实也不难求,x=200t;y=200t^2。所以y=xt;其中x就是我们屏幕的宽度,t就是x/200。有兴趣可以试下,有错误还望指出,其实要实现抛物线效果方法应该还有很多。结合数学公式还可以做出很多效果,比如正选曲线运动等。
代码演示(二)
上面我们演示了抛物线效果,下面我们再来一个效果,就是水平方向速度100px/s,竖直方向150px/s。这次我们要自定义估值算法并且使用startValue和endValue参数来做算法。
布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/id_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/id_ball"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@mipmap/bol_blue" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:onClick="paowuxian"
android:layout_centerHorizontal="true"
android:text="抛物线" />
</RelativeLayout>
和上面一样,先创建一个Point类:
package com.wangjian.propertyanim;
/**
* 保存小球坐标的类
* Created by Administrator on 2016/2/2.
*/
public class Point {
float x;
float y;
public Point(){
}
public Point(float x,float y){
this.x = x;
this.y = y;
}
}
自定义估值算法:
package com.wangjian.propertyanim;
import android.animation.TypeEvaluator;
/**
* 自定义估值算法
* Created by Administrator on 2016/2/2.
*/
public class MyTypeEvaluator implements TypeEvaluator<Point> {
@Override
public Point evaluate(float fraction, Point startValue, Point endValue) {
Point point = new Point();
point.x = startValue.x+fraction * (endValue.x - startValue.x);//fraction * 3就是时间,fraction代表时间流逝的百分比,3代表3000ms即3s
point.y = startValue.y+fraction*(endValue.y - startValue.y);
return point;
}
}
在evaluate
MainActivity:
package com.wangjian.propertyanim;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
public class MainActivity extends Activity {
private ImageView mBlueBall;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBlueBall = (ImageView) findViewById(R.id.id_ball);
}
/**
* 抛物线
* @param view
*/
public void paowuxian(View view){
//为x的属性值设置动画
ValueAnimator valueAnimator = ValueAnimator.ofObject(new MyTypeEvaluator(),new Point(0,0),new Point(200,300));
//设置持续时间
valueAnimator.setDuration(3000);
//设置线性时间插值器
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Point point = (Point) animation.getAnimatedValue();
mBlueBall.setX(point.x);
mBlueBall.setY(point.y);
}
});
valueAnimator.start();
}
}
理一下思路:
①首先创建Point类用来保存坐标信息,Point对象即是我们要操作的对象
②自定义估值算法MyTypeEvaluator,并制定泛型为Point类型,在evaluate方法中进行估值算法,为point对象的x和y赋值并将该对象返回。
③在MainActivity中调用ValueAnimator.ofObject()方法获得ValueAnimator对象,并传入自定义估值器对象和Point的初始对象与终止对象。
④为ValueAnimator对象设置AnimatorUpdateListener监听,在onAnimationUpdate方法中通过Point point = (Point) animation.getAnimatedValue();即可获得在估值算法中返回的Point对象,并为小球设置新的x和y值。
下面是运行图:
没错,小球从(0,0)坐标运行到了(200,300)坐标,并按我们预想的轨迹运行。