转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/43536355
大家好,在上一篇文章当中,我们学习了Android属性动画的基本用法,当然也是最常用的一些用法,这些用法足以覆盖我们平时大多情况下的动画需求了。但是,正如上篇文章当中所说到的,属性动画对补间动画进行了很大幅度的改进,之前补间动画可以做到的属性动画也能做到,补间动画做不到的现在属性动画也可以做到了。因此,今天我们就来学习一下属性动画的高级用法,看看如何实现一些补间动画所无法实现的功能。
阅读本篇文章需要你对属性动画有一定的了解,并且掌握属性动画的基本用法,如果你还对属性动画不够了解的话,建议先去阅读 Android属性动画完全解析(上),初识属性动画的基本用法 。
ValueAnimator的高级用法
在上篇文章中介绍补间动画缺点的时候有提到过,补间动画是只能对View对象进行动画操作的。而属性动画就不再受这个限制,它可以对任意对象进行动画操作。那么大家应该还记得在上篇文章当中我举的一个例子,比如说我们有一个自定义的View,在这个View当中有一个Point对象用于管理坐标,然后在onDraw()方法当中就是根据这个Point对象的坐标值来进行绘制的。也就是说,如果我们可以对Point对象进行动画操作,那么整个自定义View的动画效果就有了。OK,下面我们就来学习一下如何实现这样的效果。
在开始动手之前,我们还需要掌握另外一个知识点,就是TypeEvaluator的用法。可能在大多数情况下我们使用属性动画的时候都不会用到TypeEvaluator,但是大家还是应该了解一下它的用法,以防止当我们遇到一些解决不掉的问题时能够想起来还有这样的一种解决方案。
那么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是系统内置好的功能,并不需要我们自己去编写,但介绍它的实现方法是要为我们后面的功能铺路的。前面我们使用过了ValueAnimator的ofFloat()和ofInt()方法,分别用于对浮点型和整型的数据进行动画操作的,但实际上ValueAnimator中还有一个ofObject()方法,是用于对任意对象进行动画操作的。但是相比于浮点型或整型数据,对象的动画操作明显要更复杂一些,因为系统将完全无法知道如何从初始对象过度到结束对象,因此这个时候我们就需要实现一个自己的TypeEvaluator来告知系统如何进行过度。
下面来先定义一个Point类,如下所示:
- 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 float getY() {
- return y;
- }
- }
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编写完成了,接下来我们就可以非常轻松地对Point对象进行动画操作了,比如说我们有两个Point对象,现在需要将Point1通过动画平滑过度到Point2,就可以这样写:
- Point point1 = new Point(0, 0);
- Point point2 = new Point(300, 300);
- ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), point1, point2);
- anim.setDuration(5000);
- anim.start();
好的,这就是自定义TypeEvaluator的全部用法,掌握了这些知识之后,我们就可以来尝试一下如何通过对Point对象进行动画操作,从而实现整个自定义View的动画效果。
新建一个MyAnimView继承自View,代码如下所示:
- public class MyAnimView extends View {
- public static final float RADIUS = 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) {
- if (currentPoint == null) {
- currentPoint = new Point(RADIUS, RADIUS);
- drawCircle(canvas);
- startAnimation();
- } else {
- drawCircle(canvas);
- }
- }
- private void drawCircle(Canvas canvas) {
- float x = currentPoint.getX();
- float y = currentPoint.getY();
- canvas.drawCircle(x, y, RADIUS, mPaint);
- }
- private void startAnimation() {
- Point startPoint = new Point(RADIUS, RADIUS);
- Point endPoint = new Point(getWidth() - RADIUS, getHeight() - RADIUS);
- ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
- anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- currentPoint = (Point) animation.getAnimatedValue();
- invalidate();
- }
- });
- anim.setDuration(5000);
- anim.start();
- }
- }
那么我们来观察一下startAnimation()方法中的代码,其实大家应该很熟悉了,就是对Point对象进行了一个动画操作而已。这里我们定义了一个startPoint和一个endPoint,坐标分别是View的左上角和右下角,并将动画的时长设为5秒。然后有一点需要大家注意的,就是我们通过监听器对动画的过程进行了监听,每当Point值有改变的时候都会回调onAnimationUpdate()方法。在这个方法当中,我们对currentPoint对象进行了重新赋值,并调用了invalidate()方法,这样的话onDraw()方法就会重新调用,并且由于currentPoint对象的坐标已经改变了,那么绘制的位置也会改变,于是一个平移的动画效果也就实现了。
下面我们只需要在布局文件当中引入这个自定义控件:
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- >
- <com.example.tony.myapplication.MyAnimView
- android:layout_width="match_parent"
- android:layout_height="match_parent" />
- </RelativeLayout>
最后运行一下程序,效果如下图所示:
OK!这样我们就成功实现了通过对对象进行值操作来实现动画效果的功能,这就是ValueAnimator的高级用法。
ObjectAnimator的高级用法
ObjectAnimator的基本用法和工作原理在上一篇文章当中都已经讲解过了,相信大家都已经掌握。那么大家应该都还记得,我们在吐槽补间动画的时候有提到过,补间动画是只能实现移动、缩放、旋转和淡入淡出这四种动画操作的,功能限定死就是这些,基本上没有任何扩展性可言。比如我们想要实现对View的颜色进行动态改变,补间动画是没有办法做到的。
但是属性动画就不会受这些条条框框的限制,它的扩展性非常强,对于动态改变View的颜色这种功能是完全可是胜任的,那么下面我们就来学习一下如何实现这样的效果。
大家应该都还记得,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()方法得到调用了,毫无疑问,当然是要借助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 (mCurrentRed != endRed) {
- mCurrentRed = getCurrentColor(startRed, endRed, colorDiff, 0,
- fraction);
- } else if (mCurrentGreen != endGreen) {
- mCurrentGreen = getCurrentColor(startGreen, endGreen, colorDiff,
- redDiff, fraction);
- } else if (mCurrentBlue != endBlue) {
- mCurrentBlue = getCurrentColor(startBlue, endBlue, colorDiff,
- redDiff + greenDiff, fraction);
- }
- // 将计算出的当前颜色的值组装返回
- String currentColor = "#" + getHexString(mCurrentRed)
- + getHexString(mCurrentGreen) + getHexString(mCurrentBlue);
- return currentColor;
- }
- /**
- * 根据fraction值来计算当前的颜色。
- */
- private int getCurrentColor(int startColor, int endColor, int colorDiff,
- int offset, float fraction) {
- int currentColor;
- if (startColor > endColor) {
- currentColor = (int) (startColor - (fraction * colorDiff - offset));
- if (currentColor < endColor) {
- currentColor = endColor;
- }
- } else {
- currentColor = (int) (startColor + (fraction * colorDiff - offset));
- if (currentColor > endColor) {
- currentColor = endColor;
- }
- }
- return currentColor;
- }
- /**
- * 将10进制颜色值转换成16进制。
- */
- private String getHexString(int value) {
- String hexString = Integer.toHexString(value);
- if (hexString.length() == 1) {
- hexString = "0" + hexString;
- }
- return hexString;
- }
- }
首先在evaluate()方法当中获取到颜色的初始值和结束值,并通过字符串截取的方式将颜色分为RGB三个部分,并将RGB的值转换成十进制数字,那么每个颜色的取值范围就是0-255。接下来计算一下初始颜色值到结束颜色值之间的差值,这个差值很重要,决定着颜色变化的快慢,如果初始颜色值和结束颜色值很相近,那么颜色变化就会比较缓慢,而如果颜色值相差很大,比如说从黑到白,那么就要经历255*3这个幅度的颜色过度,变化就会非常快。
那么控制颜色变化的速度是通过getCurrentColor()这个方法来实现的,这个方法会根据当前的fraction值来计算目前应该过度到什么颜色,并且这里会根据初始和结束的颜色差值来控制变化速度,最终将计算出的颜色进行返回。
最后,由于我们计算出的颜色是十进制数字,这里还需要调用一下getHexString()方法把它们转换成十六进制字符串,再将RGB颜色拼装起来之后作为最终的结果返回。
好了,ColorEvaluator写完之后我们就把最复杂的工作完成了,剩下的就是一些简单调用的问题了,比如说我们想要实现从蓝色到红色的动画过度,历时5秒,就可以这样写:
- ObjectAnimator anim = ObjectAnimator.ofObject(myAnimView, "color", new ColorEvaluator(),
- "#0000FF", "#FF0000");
- anim.setDuration(5000);
- anim.start();
接下来我们需要将上面一段代码移到MyAnimView类当中,让它和刚才的Point移动动画可以结合到一起播放,这就要借助我们在上篇文章当中学到的组合动画的技术了。修改MyAnimView中的代码,如下所示:
- public class MyAnimView extends View {
- ...
- private void startAnimation() {
- Point startPoint = new Point(RADIUS, RADIUS);
- Point endPoint = new Point(getWidth() - RADIUS, getHeight() - RADIUS);
- ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
- anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- currentPoint = (Point) animation.getAnimatedValue();
- invalidate();
- }
- });
- ObjectAnimator anim2 = ObjectAnimator.ofObject(this, "color", new ColorEvaluator(),
- "#0000FF", "#FF0000");
- AnimatorSet animSet = new AnimatorSet();
- animSet.play(anim).with(anim2);
- animSet.setDuration(5000);
- animSet.start();
- }
- }
OK,位置动画和颜色动画非常融洽的结合到一起了,看上去效果还是相当不错的,这样我们就把ObjectAnimator的高级用法也掌握了。