浅析Android动画(三),自定义Interpolator与TypeEvaluator

转载请注明出处! http://www.cnblogs.com/wondertwo/p/5327586.html


自定义Interpolator

本篇博客是浅析Android动画系列博客的第三篇,也是收尾工作!会在前两篇的基础上继续深入一步,介绍自定义Interpolator和自定义TypeEvaluator这两部分内容,如果对Android动画的基本用法还不是很熟悉,建议先去阅读我的前两篇博客:

如果对Android动画了解的比较深刻,应该都有同感,只有熟练的掌握自定义InterpolatorTypeEvaluator的技巧,才能做出一些酷炫的动画,那么,本篇博客就会带大家去揭开自定义InterpolatorTypeEvaluator的面纱一探究竟!先来看InterpolatorTypeEvaluator的继承关系如下图:

Interpolator直译过来就是插补器,也译作插值器,直接控制动画的变化速率,这涉及到变化率概念,形象点说就是加速度,可以简单理解为变化的快慢。从上面的继承关系可以清晰的看出来,Interpolator是一个接口,并未提供插值逻辑的具体实现,它的非直接子类有很多,比较常用的我都用红色下划线标出,有下面四个:

  • 加减速插值器AccelerateDecelerateInterpolator
  • 线性插值器LinearInterpolator
  • 加速插值器AccelerateInterpolator
  • 减速插值器DecelerateInterpolator

当你没有为动画设置插值器时,系统默认会帮你设置加减速插值器AccelerateDecelerateInterpolator,我们不妨来看一个实例效果图如下,小球从静止开始先做加速运动再做减速运动最后停止,效果还是很明显的:

那么这个先加速后减速的过程是怎样实现的呢?来看AccelerateDecelerateInterpolator源代码如下:

package android.view.animation;

import android.content.Context;
import android.util.AttributeSet;

/**
 * An interpolator where the rate of change starts and ends slowly but
 * accelerates through the middle.
 */
public class AccelerateDecelerateInterpolator extends BaseInterpolator
        implements NativeInterpolatorFactory {
    public AccelerateDecelerateInterpolator() {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) {
    }

    public float getInterpolation(float input) {
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
    }

    /** @hide */
    @Override
    public long createNativeInterpolator() {
        return NativeInterpolatorFactoryHelper.createAccelerateDecelerateInterpolator();
    }
}

我们只关注getInterpolation(float input)这个方法,getInterpolation()方法接收一个input参数,input参数是系统根据设置的动画持续时间计算出来的,取值范围是[0,1],从0匀速增加到1,那怎么根据这个匀速增加的参数计算出一个先加速后减速效果的返回值呢?getInterpolation()方法中的代码也很简单,只是返回了一个计算式:(float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f,翻译成数学表达式就是:

{0.5*cos[(input + 1)π] + 0.5}

很容易看懂,一个最基本的余弦函数变换,它的图像就对应了余弦函数π到2π范围内的曲线,如下所示,曲线的斜率先增大后减小,相应的,小球运动先加速后减速:

在第二篇博客中出现过一个参数fraction,同学们还记得吗?它表示动画时间流逝的百分比。其实fraction参数就是根据上面的input参数计算出来的,很容易联想到,最简单的情况就是对input参数不作任何计算处理,直接把input作为返回值返回,那么对应的插值器应该是线性插值器,为了验证一下我们的推理是否正确,来看看线性插值器LinearInterpolator的源码如下:

/**
 * An interpolator where the rate of change is constant
 */
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {

    public LinearInterpolator() {
    }

    public LinearInterpolator(Context context, AttributeSet attrs) {
    }

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

    /** @hide */
    @Override
    public long createNativeInterpolator() {
        return NativeInterpolatorFactoryHelper.createLinearInterpolator();
    }
}

LinearInterpolatorgetInterpolation(float input)方法果然是直接把input参数作为返回值返回了!由于input参数是从0匀速增加到1的,所以自然就是线性插值器啦。好了,现在已经清楚插值器的计算逻辑是在getInterpolation(float input)方法中完成的,那我们来看一个稍微复杂一点的插值器BounceInterpolatorBounceInterpolator可以实现弹跳效果,一般用来实现小球落地后的弹跳效果还是挺形象的哈,BounceInterpolator源码如下:

/**
 * An interpolator where the change bounces at the end.
 */
public class BounceInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
    public BounceInterpolator() {
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public BounceInterpolator(Context context, AttributeSet attrs) {
    }

    private static float bounce(float t) {
        return t * t * 8.0f;
    }

    public float getInterpolation(float t) {
        t *= 1.1226f;
        if (t < 0.3535f) return bounce(t);
        else if (t < 0.7408f) return bounce(t - 0.54719f) + 0.7f;
        else if (t < 0.9644f) return bounce(t - 0.8526f) + 0.9f;
        else return bounce(t - 1.0435f) + 0.95f;
    }

    /** @hide */
    @Override
    public long createNativeInterpolator() {
        return NativeInterpolatorFactoryHelper.createBounceInterpolator();
    }
}

getInterpolation()方法干的第一件事就是重新为变量t赋值,然后根据t取值范围的不同,调用bounce(float t)方法来计算返回值,而bounce(float t)方法很明显是一个二次函数,再稍微观察一下你会发现它其实就是一个分段二次函数,看完下图的详细推导过程你就会明白为什么啦:

分析到这里相信同学们很快就能举一反三,瞬间明白其他几种常见插值器的实现原理了,这里我就不继续解释源码了,而是要放出大招——自定义先减速后加速插值器DeceAcceInterpolatorDeceAcceInterpolator需要实现Interpolator接口,代码如下:

/**
 * DeceAcceInterpolator自定义减速加速插值器
 * Created by wondertwo on 2016/3/25.
 */
public class DeceAcceInterpolator implements Interpolator {
    @Override
    public float getInterpolation(float input) {
        return ((4*input-2)*(4*input-2)*(4*input-2))/16f + 0.5f;
    }
}

getInterpolation(float input)方法中返回值只有一行代码,很简单吧!把返回值((4*input-2)*(4*input-2)*(4*input-2))/16f + 0.5f翻译成数学表达式如下:

[(4*input-2)^3]/16 + 0.5

涉及到了三次函数变换,呃呃我假装你们都听得懂哈,其实也蛮简单的就是三次函数的简单变换啊,先来看三次函数的函数曲线如下:

还不明白?详细的数学推导过程请看下图:

可以看出来返回值的范围依然是[0,1],或许这样还不够直观,那我们就看一下函数对应的曲线的变化率,如下图所示,明显可以看到函数图像的变化率先减小后增大,这也是我们先减速后加速的动画效果的数学反映:

接下来把我们自定义的插值器设置给属性动画,代码如下:

// 设置自定义的减速加速插值器DeceAcceInterpolator()
animator.setInterpolator(new DeceAcceInterpolator());

那么我们自定义减速加速插值器的效果怎样呢?请看下图,可以看到小球确实是先做减速运动,速度减为0后又继续加速运动:

很明显可以看出来,小球的运动是先减速直到停下来,再加速到达终点。下图展示了动画执行过程中fraction参数的变化情况,由于数据太多我只展示了后半段数据,可以看到[0.5~0.6]区段打印了81个数据,而[0.9~1.0]区段只打印了10个数据,这也可以看出来后半段是一个加速的过程:


自定义TypeEvaluator

同学们应该记得在我,介绍了这样一个场景:在6秒内把一个按钮控件的背景颜色从蓝色渐变到红色,记得当时是怎样实现颜色渐变的吗?效果图如下:

当时提出了两种方式可以实现这种效果,第一种实现方式已经介绍过了,那我们现在就来看看第二种方式,通过自定义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;  
    }  
  
}  

关于颜色计算的逻辑和第二篇博客中evaluateForColor()方法的逻辑一模一样!evaluate(float fraction, Object startValue, Object endValue)方法返回一个经过计算的十六进制字符串颜色值。在创建ObjectAnimator对象的时候,传入的第三个参数就是我们自定义的颜色估值器ColorEvaluator的匿名对象,代码如下:

ObjectAnimator anim = ObjectAnimator.ofObject(
        targetView, 
        "color", 
        new ColorEvaluator(),   
        "#0000FF", 
        "#FF0000");  
anim.setDuration(5000);  
anim.start(); 

需要注意,不同点是这次我们的目标对象targetView需要有“color”这个属性,并且要有“color”属性的settergetter方法。经过上面的学习你应该对TypeEvaluator的工作原理比较熟悉了!看完接下来的这个实例,才算真正学会了自定义估值器。红色小圆点从屏幕的中央最高点向下做曲线运动,如果把轨迹记录下来,就是一条正弦曲线,效果图如下:

系统提供的view控件是不能直接通过设置坐标来改变位置的,因此我们首先需要自定义一个view控件PositionViewPositionView类中定义了一个内部类PositionPoint代表小圆点对象,这个小圆点对象有两个成员变量xy表示其坐标,可以通过getter方法来获取坐标值,并可以通过构造方法创建PositionPoint对象,另外在PositionView类中定义了createPoint(float x, float y)方法对其进行了封装;绘制小圆点drawCircle(Canvas canvas)和启动动画startPropertyAni()的逻辑在onDraw()方法中执行,代码如下:

/**
 * Created by wondertwo on 2016/3/27.
 */
public class PositionView extends View {

    public static final float RADIUS = 20f;
    private PositionPoint currentPoint;
    private Paint mPaint;

    public PositionView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.RED);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (currentPoint == null) {
            currentPoint = new PositionPoint(RADIUS, RADIUS);
            drawCircle(canvas);
            startPropertyAni();
        } else {
            drawCircle(canvas);
        }
    }

    private void drawCircle(Canvas canvas) {
        float x = currentPoint.getX();
        float y = currentPoint.getY();
        canvas.drawCircle(x, y, RADIUS, mPaint);
    }

    /**
     * 启动动画
     */
    private void startPropertyAni() {
        ValueAnimator animator = ValueAnimator.ofObject(
                new PositionEvaluator(),
                createPoint(RADIUS, RADIUS),
                createPoint(getWidth() - RADIUS, getHeight() - RADIUS));
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                currentPoint = (PositionPoint) animation.getAnimatedValue();
                invalidate();
            }
        });
        // 设置自定义的减速加速插值器DeceAcceInterpolator()
        animator.setInterpolator(new DeceAcceInterpolator());
        animator.setDuration(10 * 1000).start();
    }

    /**
     * createPoint()创建PositionPointView对象
     */
    public PositionPoint createPoint(float x, float y) {
        return new PositionPoint(x, y);
    }

    /**
     * 小圆点内部类
     */
    class PositionPoint {
        private float x;
        private float y;

        public PositionPoint(float x, float y) {
            this.x = x;
            this.y = y;
        }

        public float getX() {
            return x;
        }

        public float getY() {
            return y;
        }
    }
}

有了上面的准备工作,我们来看自定义坐标位置估值器PositionEvaluator.java的源码如下:

/**
 * PositionEvaluator位置估值器
 * Created by wondertwo on 2016/3/23.
 */
public class PositionEvaluator implements TypeEvaluator {

    // 创建PositionView对象,用来调用createPoint()方法创建当前PositionPoint对象
    PositionView positionView = new PositionView(PositionDeAcActivity.mainActivity, null);

    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {

        // 将startValue,endValue强转成PositionView.PositionPoint对象
        PositionView.PositionPoint point_1 = (PositionView.PositionPoint) startValue;

        // 获取起始点Y坐标
        float currentY = point_1.getY();
        /*// 计算起始点到结束点Y坐标差值
        float diffY = Math.abs(point_1.getY() - point_2.getY());*/

        // 调用forCurrentX()方法计算X坐标
        float x = forCurrentX(fraction);
        // 调用forCurrentY()方法计算Y坐标
        float y = forCurrentY(fraction, currentY);

        return positionView.createPoint(x, y);
    }

    /**
     * 计算Y坐标
     */
    private float forCurrentY(float fraction, float currentY) {
        float resultY = currentY;
        if (fraction != 0f) {
            resultY = fraction * 400f + 20f;
        }
        return resultY;
    }

    /**
     * 计算X坐标
     */
    private float forCurrentX(float fraction) {
        float range = 120f;// 振幅
        float resultX = 160f + (float) Math.sin((6 * fraction) * Math.PI) * range;// 周期为3,故为6fraction
        return resultX;
    }
}

位置估值器PositionEvaluator需要实现TypeEvaluator接口,重写evaluate(float fraction, Object startValue, Object endValue)方法,首先将startValueendValue强转成PositionView.PositionPoint对象,也就是我们的小圆点对象,再分别调用forCurrentX()forCurrentY()方法计算新的xy坐标,根据xy坐标创建新的小圆点对象并将其返回!需要注意,实现上面介绍的正弦曲线效果的逻辑就是在这两个方法中实现的,我们让Y坐标匀速增加,而X坐标做正弦振动,它们的合成运动就是正弦曲线的效果。最后在PositionView类的startAnimation()方法中,创建ValueAnimator对象时,传入的第一个参数就是我们自定义估值器PositionEvaluator匿名对象,代码如下:

ValueAnimator animator = ValueAnimator.ofObject(
        new PositionEvaluator(),
        createPoint(RADIUS, RADIUS),
        createPoint(getWidth() - RADIUS, getHeight() - RADIUS));

最后在Activity的布局文件中添加我们的自定义控件PositionView即可看到效果,代码如下:

<com.wondertwo.propertyanime.PositionView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerHorizontal="true" />

总结:

  1. 自定义Interpolator需要重写getInterpolation(float input),控制时间参数的变化;
  2. 自定义TypeEvaluator需要重写evaluate()方法,计算对象的属性值并将其封装成一个新对象返回;

在最后附上浅析Android动画系列的三篇文章:

  1. 浅析Android动画(一),View动画高级实例探究 http://www.cnblogs.com/wondertwo/p/5295976.html
  2. 浅析Android动画(二),属性动画与高级实例探究 http://www.cnblogs.com/wondertwo/p/5312482.html
  3. 浅析Android动画(三),自定义Interpolator与TypeEvaluator http://www.cnblogs.com/wondertwo/p/5327586.html

如果觉得不错请继续关注哦!

posted @ 2016-03-28 01:21 布鲁克林一棵树 阅读(...) 评论(...) 编辑 收藏