本篇博客转载自郭大神的Android属性动画解析(中),ValueAnimator和ObjectAnimator的高级用法 有兴趣的可以去瞅瞅。
前言:在上一篇博客中我们已经了解了Android属性动画的基本用法,也是一些最常用的方法,掌握了这些方法,一般的动画需求就像 洒洒水 似的简单了,而这篇博客呢是要介绍属性动画的高级用法,正如上篇文章中我们提到的,属性动画在补间动画的基础上进行了很大幅度的改进,之前补间动画可以做的的属性动画也可以做的,补间动画做不到的属性动画也可以做到了,因此今天我们就来学习一下补间动画的高级用法,看看如何实现一些补间动画所无法实现的功能。
阅读本章文章之前,如果你对属性动画的基本用法还不是很熟悉,建议先去阅读Android属性动画详解(上),初始属性动画的基本用法。
在商品文章中我们介绍补间动画的缺点的时候有说过,补间动画是只能针对view对象进行操作的,而属性动画就不在受这个限制,它是可以对任意对象的任意属性进行操作的,不知道大家还记不得在上篇文章中我们说会实现一个例子来证明属性动画对对象的操作,举个什么例子呢:比如说我们现在有一个自定义的view,在这个view中有一个Point对象用于管理坐标,然后在onDraw()方法中我们根据Point这个对象的坐标值进行绘制。那也就是说我们可以对Point对象进行动画操作,那么整个自定义view的动画效果我们就可以实现了。
不过在动手之前我们还需要掌握一个知识点,就是TypeEvaluator的用法,可能在大多数情况下我们使用属性动画的时候都不会使用到TypeEvaluator,但是大家应该了解他的用法,这对我们后面要实现Point动画很重要。
那么TypeEvaluator到底是做什么用的呢?简单来说,其实就是告知动画系统如何从初始值过渡到结束值,我们在上一篇文章中有提到其实ValueAnimator的 ofFloat其实就是实现了初始值到结束值的平滑过渡, 那么这个平滑过渡到底是怎么做的呢?想必大家应该有这个疑问吧?其实就是系统内置了一个FloatEvaluteor,通过它来计算并告知动画系统当前动画的执行的进度,也就是初始值到结束值之间的过渡值,我们先来看一下FloatEvaluator的代码实现:
public class FloatEvaluator implements TypeEvaluator<Number> {
public Float evaluate(float fraction, Number startValue, Number endValue) {
float startFloat = startValue.floatValue();
return startFloat + fraction * (endValue.floatValue() - startFloat);
}
}
可以看到其实FloatEvaluator中的代码很简单,其实就是实现了TypeEvalutor接口,重写了evalute()方法,而evaluate()这个方法中接受了三个参数,第一个参数fraction是非常重要的,它表示的是当前动画的完成度,我们就是根据这个值来计算当前我们动画的执行进度的,第二和第三个参数就相对比较简单了,分别表示动画的初始值和结束值,其实上述的代码已经很清晰的告知了我们动画进度是怎么计算的了,其实就是用 结束值 减去 初始值,算出他们之间的差值,然后在乘以完成的比例(fraction),然后在加上初始值,就是当前我们已经完成的动画的进度,是不是很好理解?
好了,FloatEvaluator是系统内置好的功能,不需要我们自己去写,用的时候我们直接拿来用就可以啦,但介绍他是因为我们后面会使用到TypeEvaluator,因为我们之前使用的都是ofInt、ofFloat分别用于对整型和浮点型的数据进行动画操作, 但是如果我们想要对一个对象进行操作,那么ofInt、ofFloat就帮不上什么忙了,但是别忘了我们还有一个ofObject()这个方法,这个方法就是用来对任意对象进行动画操作的,不过要想使用它来完成动画,就相对比较麻烦了,因为我们想要实现对象的动画操作,系统是无法知道如何从初始值过渡到结束值的,所以这时候就需要我们自己来编写一个TypeEvaluator类来告诉系统我们的动画要如何过渡。
下面先定义一个Point类,用于保存当前Point对象的X轴和Y轴坐标:
public class Point {
private float x;
private float y;
public Point(float x, float y){
this.x = x;
this.y = y;
};
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
}
Point对象很简单,只定义了两个属性x,y分别用于记录当前point对象的X轴和Y轴位置,并提供了构造方法来设置坐标,提供get方法获取坐标
接下来就是定义一个我们自己的TypeEvaluator:
public class PointEvalutor implements TypeEvaluator<Point> {
@Override
public Point evaluate(float fraction, Point startValue, Point endValue) {
float x = fraction * (endValue.getX() - startValue.getX()) + startValue.getX();
float y = fraction * (endValue.getY() - startValue.getY()) + startValue.getY();
return new Point(x,y);
}
}
代码也非常简单,就是根据初始值和结束值分别得到当前正在运行的point的x和y的坐标,并组成一个point返回。
这样我们就已经完成了对PointEvalutor的编写了,接下来我们就可以很轻松的对point进行动画操作了,比如说我们现在有两个point对象,现在需要将point1 过渡到 point2 就可以这样写:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.object_animator1);
//初始的Point
Point startPoint = new Point(0, 0);
//结束的Point
Point endPoint = new Point(3,3);
valueAnimator = ValueAnimator.ofObject(new PointEvalutor(), startPoint, endPoint);
Button start = (Button) findViewById(R.id.bt_start);
start.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
valueAnimator.setDuration(300).start();
}
});
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
Point animatedValue = (Point) animation.getAnimatedValue();
Log.w("TAG","animatedValue.getX() = "+animatedValue.getX()+"------animatedValue.getY() = "+animatedValue.getY());
}
});
}
这里呢我们设置的初始的Point为(0,0),结束的Point为(3,3),使用方法和之前是一样的,用的都还是ValueAnimator,只不过在这里我们将前面使用的ofInt、ofFloat给换成了ofObject,而使用ofIObject时,参数也发生了变化,这里需要我们多传入进去一个TypeEvaluator对象,其他的到是一样,后面的参数直接传入Point对象即可,来我们看一下打印的过渡值:
应该没什么疑问的吧,代码很简单,效果和之前咱们的第一个例子是一样的,只不过这里把类型给换了而已
好了上面我们已经实现了值的过渡,但是却没什么卵用,因为不是太直观,想要直观一点,当然是把动画效果给整出来啊
新建一个PointView 继承自View,代码如下:
public class PointView extends View {
//初始化画笔的半径
private static final float RADIUS = 50f;
//画笔
private Paint mPaint;
//当前动画的位置
private Point mPoint;
public PointView(Context context) {
this(context, null);
}
public PointView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PointView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//采用抗锯齿
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//设置画笔颜色
mPaint.setColor(Color.RED);
}
@Override
protected void onDraw(Canvas canvas) {
//初始化Point,初始坐标为 RADIUS,RADIUS
if (mPoint == null) {
mPoint = new Point(RADIUS, RADIUS);
//初始化一个圆出来
drawCircle(canvas);
//开始执行动画,进行不断绘制操作
startAnimation();
} else {
//初始化一个圆出来
drawCircle(canvas);
}
}
//画圆
private void drawCircle(Canvas canvas) {
//这里根据Point的坐标在指定位置上绘制一个圆,圆的半径为RADIUS
if (mPaint != null) {
canvas.drawCircle(mPoint.getX(), mPoint.getY(), RADIUS, mPaint);
}
}
private void startAnimation() {
//初始化初始位置的Point以及结束位置的Point
Point startPoint = new Point(RADIUS, RADIUS);
Point endPoint = new Point(getWidth() - RADIUS, getHeight() - RADIUS);
ValueAnimator valueAnimator = ValueAnimator.ofObject(new PointEvalutor(), startPoint, endPoint);
//添加监听,用于更新point坐标
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPoint = (Point) animation.getAnimatedValue();
//通知动画系统重新绘制
invalidate();
}
});
valueAnimator.setDuration(5000).start();
}
其实代码还是很简单的,没什么难的地方,下面我们就来分析一下整个代码的流程:
最后我们只需要在布局文件中引用我们自定义的PointView就可以了:
"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:orientation="vertical"
tools:context="demo.mk.com.valueanimatordemo.MainActivity">
.mk.com.valueanimatordemo.abvanced.PointView
android:layout_width="match_parent"
android:layout_height="match_parent" />
OK,这样我们就完成了对对象进行值操作来实现动画的效果的功能。
ObjectAnimator的基本用法我们在上篇文章中已经说完了,相信大家都已经掌握了,那么我们这里要介绍什么呢?不知道大家还记不记的我们在上篇博客中提到过说补间动画仅仅只能针对view进行操作,是不能对view的属性以及背景等进行操作的,但是属性动画可以啊,所以对于改变view的颜色这种功能真的就跟洒洒水似的
大家应该都还记得,ObjectAnimator的工作原理其实就是根据指定属性寻找内部的get、set方法,然后通过方法对其值不断的进行改变从而实现的,因此,我们需要在我们自定义的PointView中定义一个 color 属性,并提供get、set方法,这里我们将color作为字符串来使用(用一个十六进制的字符串表示一种颜色,也就是RGB的格式:#RRGGBB ) 代码如下所示:
public class PointView extends View {
...
//当前颜色
private String color;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
//在这里接受传入的color 并重新设置画笔的颜色,并重新请求绘制
mPaint.setColor(Color.parseColor(color));
invalidate();
}
...
}
这里没什么逻辑,需要注意的地方就是在setColor()方法中我们做的操作,就是当颜色改变时,调用画笔mPaint的setColor方法更新画笔的颜色,并重新刷新视图,那么接下来我们就需要考虑什么时候去调用setColor方法了,setColor方法需要我们去手动的调用么?毫无疑问,PointView的setColor方法是不需要我们去手动调用的,因为我们可以通过ObjectAnimator的ofObject 通过传递属性名,让动画系统去帮我们去调用setColor方法,但是在使用ObjectAnimator之前我们需要完成一个非常重要的操作,那就是编写一个颜色过渡的TypeEvaluator,创建实现TypeEvaluator接口的ColorEvaluator类,代码如下:
public class ColorEvaluator implements TypeEvaluator {
//这里我们只简单的实现由 蓝色 过渡到 红色的一个Evaluator
private int mCurrentBlue = -1;
private int mCurrentGreen = -1;
private int mCurrentRed = -1;
@Override
public String evaluate(float fraction, String startValue, String endValue) {
//获取初始值中单个颜色的int值
int startRed = Integer.parseInt(startValue.substring(1, 3), 16);
int startGreen = Integer.parseInt(startValue.substring(3, 5), 16);
int startBlue = Integer.parseInt(startValue.substring(5, 7), 16);
//获取结束值中单个颜色的int值
int endRed = Integer.parseInt(endValue.substring(1, 3), 16);
int endGreen = Integer.parseInt(endValue.substring(3, 5), 16);
int endBlue = Integer.parseInt(endValue.substring(5, 7), 16);
//分别获取单个颜色之间的差值
int redDiff = endRed - startRed;
int greenDiff = endGreen - startGreen;
int blueDiff = endBlue - startBlue;
//判断单个颜色的初始位置是否和结束位置相同,相同不在进行颜色的改变,默认取开始位置的颜色值
if (startRed != endRed) {
//开始获取当前单个颜色的颜色值
mCurrentRed = getCurrentColor(fraction, startRed, endRed, redDiff);
} else {
mCurrentRed = startRed;
}
if (startGreen != endGreen) {
//开始获取当前单个颜色的颜色值
mCurrentGreen = getCurrentColor(fraction, startGreen, endGreen, greenDiff);
} else {
mCurrentGreen = startGreen;
}
if (startBlue != endBlue) {
//开始获取当前单个颜色的颜色值
mCurrentBlue = getCurrentColor(fraction, startBlue, endBlue, blueDiff);
} else {
mCurrentBlue = startBlue;
}
//将获取的单个颜色拼成一个十六进制的字符串颜色
return "#" + getHexString(mCurrentRed) + getHexString(mCurrentGreen) + getHexString(mCurrentBlue);
}
private int getCurrentColor(float fraction, int startColor, int endColor, int diff) {
int currentColor = -1;
if (startColor > endColor) {
currentColor = (int) (startColor - (diff * fraction));
if (currentColor > endColor) {
currentColor = endColor;
}
} else {
currentColor = (int) (startColor + (diff * fraction));
if (currentColor > endColor) {
currentColor = endColor;
}
}
return currentColor;
}
private String getHexString(int color) {
String colorStr = Integer.toHexString(color);
if (colorStr.length() == 1) {
colorStr = "0" + colorStr;
}
return colorStr;
}
}
这段代码相对而言就有点难度了,但是也很好理解,显而易见属性动画的难点就是如何才能编写一个适合的TypeEvaluator了,只要你逻辑够深,那么属性动画就很简单
下面我们就一步步来分析下我们这段代码的流程吧:
好了,ColorEvaluator都写完了,也就表示咱们这个动画基本上已经完事了,下面就是简单的调用问题了,不如现在我们想实现从蓝色到红色的颜色渐变,我们就可以这样写:
ObjectAnimator anim = ObjectAnimator.ofObject(myAnimView, "color", new ColorEvaluator(),
02. "#0000FF", "#FF0000");
03.anim.setDuration(5000);
04.anim.start();
用法非常简单易懂,相信不需要我再进行解释了。
接下来我们需要将上面一段代码移到MyAnimView类当中,让它和刚才的Point移动动画可以结合到一起播放,这就要借助我们在上篇文章当中学到的组合动画的技术了。修改MyAnimView中的代码,如下所示:
为了大家看着方便,代码全部给出:
public class PointView extends View {
//初始化画笔的半径
private static final float RADIUS = 50f;
//画笔
private Paint mPaint;
//当前动画的位置
private Point mPoint;
//当前颜色
private String color;
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
//在这里接受传入的color 并重新设置画笔的颜色,并重新请求绘制
mPaint.setColor(Color.parseColor(color));
invalidate();
}
public PointView(Context context) {
this(context, null);
}
public PointView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PointView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//采用抗锯齿
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
//设置画笔颜色
mPaint.setColor(Color.BLUE);
}
@Override
protected void onDraw(Canvas canvas) {
//初始化Point,初始坐标为 RADIUS,RADIUS
if (mPoint == null) {
mPoint = new Point(RADIUS, RADIUS);
//初始化一个圆出来
drawCircle(canvas);
//开始执行动画,进行不断绘制操作
startAnimation();
} else {
//初始化一个圆出来
drawCircle(canvas);
}
}
//画圆
private void drawCircle(Canvas canvas) {
//这里根据Point的坐标在指定位置上绘制一个圆,圆的半径为RADIUS
if (mPaint != null) {
canvas.drawCircle(mPoint.getX(), mPoint.getY(), RADIUS, mPaint);
}
}
private void startAnimation() {
//初始化初始位置的Point以及结束位置的Point
Point startPoint = new Point(RADIUS, RADIUS);
Point endPoint = new Point(getWidth() - RADIUS, getHeight() - RADIUS);
ValueAnimator valueAnimator = ValueAnimator.ofObject(new PointEvalutor(), startPoint, endPoint);
//添加监听,用于更新point坐标
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPoint = (Point) animation.getAnimatedValue();
//通知动画系统重新绘制
invalidate();
}
});
ObjectAnimator anim = ObjectAnimator.ofObject(this, "color", new ColorEvaluator(), "#0000FF", "#FF0000");
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(valueAnimator).with(anim);
animatorSet.setDuration(5000).start();
}
}
OK,位置动画和颜色动画非常融洽的结合到一起了,看上去效果还是相当不错的,这样我们就把ObjectAnimator的高级用法也掌握了。
相关文章:
1、Android 属性动画(Property Animation) 完全解析 (上)
2、 Android 属性动画(Property Animation) 完全解析 (下)