淺析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.50.6]區段打印了81個數據,而[0.91.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

如果覺得不錯請繼續關注哦!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM