属性动画对补间动画进行了很大幅度的改进,之前补间动画可以做到的属性动画也能做到,补间动画做不到的现在属性动画也可以做到了。因此,今天我们就来学习一下属性动画的高级用法,看看如何实现一些补间动画所无法实现的功能。
补间动画是只能对View对象进行动画操作的。而属性动画就不再受这个限制,它可以对任意对象进行动画操作。那么大家应该还记得在上篇文章当中我举的一个例子,比如说我们有一个自定义的View,在这个View当中有一个Point对象用于管理坐标,然后在onDraw()方法当中就是根据这个Point对象的坐标值来进行绘制的。也就是说,如果我们可以对Point对象进行动画操作,那么整个自定义View的动画效果就有了。OK,下面我们就来学习一下如何实现这样的效果。
那么TypeEvaluator的作用到底是什么呢?简单来说,就是告诉动画系统如何从初始值过度到结束值。我们在上一篇文章中学到的ValueAnimator.ofFloat()方法就是实现了初始值与结束值之间的平滑过度,那么这个平滑过度是怎么做到的呢?其实就是系统内置了一个FloatEvaluator,它通过计算告知动画系统如何从初始值过度到结束值,我们来看一下FloatEvaluator的代码实现:
public class FloatEvaluator implements TypeEvaluator {
public Object evaluate(float fraction, Object startValue, Object endValue) {
float startFloat = ((Number) startValue).floatValue();
return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);
}
}
可以看到,FloatEvaluator实现了TypeEvaluator接口,然后重写evaluate()方法。evaluate()方法当中传入了三个参数,第一个参数fraction非常重要,这个参数用于表示动画的完成度的,我们应该根据它来计算当前动画的值应该是多少,第二第三个参数分别表示动画的初始值和结束值。那么上述代码的逻辑就比较清晰了,用结束值减去初始值,算出它们之间的差值,然后乘以fraction这个系数,再加上初始值,那么就得到当前动画的值了。
但是相比于浮点型或整型数据,对象的动画操作明显要更复杂一些,因为系统将完全无法知道如何从初始对象过度到结束对象,因此这个时候我们就需要实现一个自己的TypeEvaluator来告知系统如何进行过度。
public class Point{
private float x;
private float y;
public Point(float x, float y){
this.x = x;
this.y = y;
}
public float getY() {
return y;
}
public float getX() {
return x;
}
}
Point类非常简单,只有x和y两个变量用于记录坐标的位置,并提供了构造方法来设置坐标,以及get方法来获取坐标。接下来定义PointEvaluator,如下所示:
public class PointEvaluator implements TypeEvaluator{
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
Point startPoint = (Point) startValue;
Point endPoint = (Point) endValue;
float x = startPoint.getX() + fraction*(endPoint.getX()-startPoint.getX());
float y = startPoint.getY() + fraction*(endPoint.getY()-startPoint.getY());
Point point = new Point(x,y);
return point;
}
}
可以看到,PointEvaluator同样实现了TypeEvaluator接口并重写了evaluate()方法。其实evaluate()方法中的逻辑还是非常简单的,先是将startValue和endValue强转成Point对象,然后同样根据fraction来计算当前动画的x和y的值,最后组装到一个新的Point对象当中并返回。
这样我们就将PointEvaluator编写完成了,接下来我们就可以非常轻松地对Point对象进行动画操作了,以这样写:
Point point1 = new Point(0,0);
Point point2 = new Point(300,300);
ValueAnimator animator = ValueAnimator.ofFloat(new PointEvaluator,point1,point2);
animator.setDuration(4000);
animator.start();
好的,这就是自定义TypeEvaluator的全部用法,掌握了这些知识之后,我们就可以来尝试一下如何通过对Point对象进行动画操作,从而实现整个自定义View的动画效果。
public class MyView extends View{
public static final float REDIUS = 50f;
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);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (currentPoint==null){
currentPoint = new Point(REDIUS,REDIUS);
drawCircle(canvas);
startAnimation();
}else {
drawCircle(canvas);
}
}
private void startAnimation() {
Point startPoint = new Point(REDIUS,REDIUS);
Point endPoint = new Point(getWidth()-REDIUS,getHeight()-REDIUS);
ValueAnimator anim = ValueAnimator.ofFloat(new PointEvaluator,startPoint,endPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
anim.setDuration(4000);
anim.start();
}
private void drawCircle(Canvas canvas) {
float x = currentPoint.getX();
float y = currentPoint.getY();
canvas.drawCircle(x,y,REDIUS,mPaint);
}
}
首先在自定义View的构造方法当中初始化了一个Paint对象作为画笔,并将画笔颜色设置为蓝色,接着在onDraw()方法当中进行绘制。这里我们绘制的逻辑是由currentPoint这个对象控制的,如果currentPoint对象不等于空,那么就调用drawCircle()方法在currentPoint的坐标位置画出一个半径为50的圆,如果currentPoint对象是空,那么就调用startAnimation()方法来启动动画。
下面我们只需要在布局文件当中引入这个自定义控件:
<com.example.demo.MyView
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
ObjectAnimator内部的工作机制是通过寻找特定属性的get和set方法,然后通过方法不断地对值进行改变,从而实现动画效果的。因此我们就需要在MyAnimView中定义一个color属性,并提供它的get和set方法。这里我们可以将color属性设置为字符串类型,使用#RRGGBB这种格式来表示颜色值,代码如下所示:
public class MyAnimView extends View{
private String color;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
mPaint.setColor(Color.parseColor(color));
invalidate();
}
}
注意在setColor()方法当中,我们编写了一个非常简单的逻辑,就是将画笔的颜色设置成方法参数传入的颜色,然后调用了invalidate()方法。这段代码虽然只有三行,但是却执行了一个非常核心的功能,就是在改变了画笔颜色之后立即刷新视图,然后onDraw()方法就会调用。在onDraw()方法当中会根据当前画笔的颜色来进行绘制,这样颜色也就会动态进行改变了。
那么接下来的问题就是怎样让setColor()方法得到调用了,毫无疑问,当然是要借助ObjectAnimator类,但是在使用ObjectAnimator之前我们还要完成一个非常重要的工作,就是编写一个用于告知系统如何进行颜色过度的TypeEvaluator。创建ColorEvaluator并实现TypeEvaluator接口,代码如下所示:
public class ColorEvaluator implements TypeEvaluator{
private int mCurrentRed = -1;
private int mCurrentGreen = -1;
private int mCurrentBlue = -1;
@Override
public Object evaluate(float fraction, Object startValue, Object endValue) {
String startColor = (String) startValue;
String endColor = (String) endValue;
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);
//初始化颜色的值
if ( mCurrentRed == -1){
mCurrentRed = startRed;
}
if ( mCurrentGreen == -1){
mCurrentGreen = startGreen;
}
if ( mCurrentBlue == -1){
mCurrentBlue = startBlue;
}
//计算初始颜色和借宿颜色的差值
int redDiff = Math.abs(startRed - endRed);
int greenDiff = Math.abs(startGreen - endGreen);
int blueDiff = Math.abs(startBlue - endBlue);
int colorDiff = redDiff+greenDiff+blueDiff;
if (mCurrentGreen!=endGreen){
mCurrentGreen = getCurrentColor(startGreen,endGreen,colorDiff,redDiff,fraction);
}else if (mCurrentRed!=endRed){
mCurrentRed = getCurrentColor(startRed,endRed,colorDiff,0,fraction);
}else if (mCurrentBlue != endBlue){
mCurrentBlue = getCurrentColor(startBlue,endBlue,colorDiff,redDiff+greenDiff,fraction);
}
//将当前颜色的值组装返回
String currentColor = "#"+getHexString(mCurrentRed)+getHexString(mCurrentBlue)+getHexString(mCurrentGreen);
return currentColor;
}
private String getHexString(int value) {
String hexString = Integer.toHexString(value);
if (hexString.length() == 1){
hexString = "0"+hexString;
}
return hexString;
}
//根据fraction值计算当前颜色
public int getCurrentColor(int startColor, int endColor, int colorDiff, int offset, float fraction) {
int currentColor;
if (startColor>endColor){
currentColor = (int) (startColor - (fraction*colorDiff-offset));
if (currentColorelse {
currentColor = (int) (startColor + (fraction*colorDiff-offset));
if (currentColor>endColor){
currentColor = endColor;
}
}
return currentColor;
}
}
这大概是我们整个动画操作当中最复杂的一个类了。没错,属性动画的高级用法中最有技术含量的也就是如何编写出一个合适的TypeEvaluator。好在刚才我们已经编写了一个PointEvaluator,对它的基本工作原理已经有了了解,那么这里我们主要学习一下ColorEvaluator的逻辑流程吧。
首先在evaluate()方法当中获取到颜色的初始值和结束值,并通过字符串截取的方式将颜色分为RGB三个部分,并将RGB的值转换成十进制数字,那么每个颜色的取值范围就是0-255。接下来计算一下初始颜色值到结束颜色值之间的差值,这个差值很重要,决定着颜色变化的快慢,如果初始颜色值和结束颜色值很相近,那么颜色变化就会比较缓慢,而如果颜色值相差很大,比如说从黑到白,那么就要经历255*3这个幅度的颜色过度,变化就会非常快。
那么控制颜色变化的速度是通过getCurrentColor()这个方法来实现的,这个方法会根据当前的fraction值来计算目前应该过度到什么颜色,并且这里会根据初始和结束的颜色差值来控制变化速度,最终将计算出的颜色进行返回。
最后,由于我们计算出的颜色是十进制数字,这里还需要调用一下getHexString()方法把它们转换成十六进制字符串,再将RGB颜色拼装起来之后作为最终的结果返回。
好了,ColorEvaluator写完之后我们就把最复杂的工作完成了,剩下的就是一些简单调用的问题了,比如说我们想要实现从蓝色到红色的动画过度,历时5秒,就可以这样写:
ObjectAnimator anim = ObjectAnimator.ofFloat(myAnimView,"color",new ColorEvaluator(),"#0000ff","#ff0000");
anim.setDuration(5000);
anim.start();
用法非常简单易懂,相信不需要我再进行解释了。
接下来我们需要将上面一段代码移到MyAnimView类当中,让它和刚才的Point移动动画可以结合到一起播放,这就要借助我们在上篇文章当中学到的组合动画的技术了。修改MyAnimView中的代码,如下所示:
public class MyAnimView extends View{
public static final float REDIUS = 50f;
private Point currentPoint;
private Paint mPaint;
public MyAnimView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.BLUE);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (currentPoint==null){
currentPoint = new Point(REDIUS,REDIUS);
drawCircle(canvas);
startAnimation();
}else {
drawCircle(canvas);
}
}
private void startAnimation() {
Point startPoint = new Point(REDIUS,REDIUS);
Point endPoint = new Point(getWidth()-REDIUS,getHeight()-REDIUS);
ValueAnimator anim = ValueAnimator.ofFloat(new PointEvaluator,startPoint,endPoint);
anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
currentPoint = (Point) animation.getAnimatedValue();
invalidate();
}
});
ObjectAnimator anim2 = ObjectAnimator.ofFloat(myAnimView,"color",new ColorEvaluator(),"#0000ff","#ff0000");
AnimatorSet animSet = new AnimatorSet();
animSet.play(anim).with(anim2);
anim.setDuration(5000);
anim.start();
}
可以看到,我们并没有改动太多的代码,重点只是修改了startAnimation()方法中的部分内容。这里先是将颜色过度的代码逻辑移动到了startAnimation()方法当中,注意由于这段代码本身就是在MyAnimView当中执行的,因此ObjectAnimator.ofObject()的第一个参数直接传this就可以了。接着我们又创建了一个AnimatorSet,并把两个动画设置成同时播放,动画时长为五秒,最后启动动画。现在重新运行一下代码,效果如下图所示: