Android屬性動畫高階用法-Interpolator,TypeEvaluatory以及貝塞爾曲線公式的使用

前言:開發當中,一般屬性動畫的縮放、平移、淡出、旋轉,可以解決大部分需求,但是如果App本身對動畫要求較高需要自定義動畫移動路徑,或者速率,則對Interpolator,TyperEvaluator瞭解便必不可少。
如:想實現如下圖動畫效果 縮放、平移、淡出、旋轉便顯得不足了。

購物車動畫

這裏寫圖片描述

目錄
  1. Interpolator,TyperEvaluator介紹
  2. Interpolator,TyperEvaluator簡單使用例子
  3. 組合動畫
  4. 自定義TypeEvaluator的使用
  5. 貝爾塞爾曲線公式結合TypeEvaluator的使用

1.Interpolator,TyperEvaluator介紹

TimeInterpolator
這裏寫圖片描述

TypeEvaluator
這裏寫圖片描述

官網的介紹,英文不好,就不獻醜了。
這裏我按我的理解簡單解釋一下

TimeInterpolator:

用於控制動畫的變換速率,如:小球從A點到B點是加速運動還是減速運動,便是由它來控制。
它是一個接口,所有的Interpolator都要實現這個接口, 它只有一個返回float值 getInterpolation(float input)方法, 方法中的 input 是系統根據設置的 duration 經過計算後傳入到這裏來的,從0勻速增加到1的值。
如:設置了一個3s的 duration 和 1s的 duration 變化都是由 0 到1,只不過速率不一樣

TypeEvaluator

用於控制動畫如何從開始過渡到結束的,如:A(0,0) B(0,5)過兩點之間的線可以是一條直線,也能是一條曲線,這個便由TypeEvaluator控制。
它是一個接口,只有一個返回泛型evaluate(float fraction, T startValue, T endValue); 方法 ,第一個參數是 TimeInterpolator中的getInterpolation(float input) 計算完成之後的返回值,第二、三個參數,就如字面意思,開始值和結束值,值得注意的是它返回的是一個Object(泛型會被擦除),這意味着它開始點和結束點可以由多個係數決定。
如:可以傳入直角座標系中的x,y 也可以傳入空間直角座標系中的 x,y,z 等等。

說了這麼多,可能理解還是有些晦澀,那麼,實踐、實踐!

2. Interpolator,TyperEvaluator簡單使用例子

這裏寫圖片描述

TyperEvaluatorde 的作用是用於控制動畫如何過渡的
如圖,我想控制小球由屏幕中間的上方移動到屏幕右下角。
先看代碼:

/**
     *  這個方法hasFocus true表示頁面繪製完成,這樣 view.getWidth 之類的方法就不會返回 0
     * @param hasFocus
     */
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus){
            init();
        }
    }

    private void init() {
        int width = rootView.getWidth();
        int height = rootView.getHeight();
        PointF pointF1 = new PointF(rootView.getWidth()/2, 0);
        PointF pointF2 = new PointF(width - imgBall.getWidth(), height - imgBall.getHeight());

        ValueAnimator valueAnimator = new ValueAnimator();
        valueAnimator.setObjectValues(pointF1, pointF2);
        valueAnimator.setEvaluator(new TypeEvaluator() {
            @Override
            public Object evaluate(float fraction, Object startValue, Object endValue) {
                PointF startPoint = (PointF) startValue;
                PointF endPoint = (PointF) endValue;
                float x = startPoint.x + fraction * (endPoint.x - startPoint.x);
                float y = startPoint.y + fraction * (endPoint.y - startPoint.y);
                PointF point = new PointF(x, y);
                Log.d("TypeEvaluator","fraction" + fraction +
                        "\n startPoint.x = "+ startPoint.x +"startPoint.y = " + startPoint.y
                        +"\nx = " + x+ "__y = "+ y);
                return point;
            }
        });
        valueAnimator.setDuration(5000);
        valueAnimator.start();

        //這裏並沒有給 View 設置動畫,而是監聽動畫變化值,不斷更新View 的位置,
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                PointF pointF = (PointF) animation.getAnimatedValue();
                imgBall.setX(pointF.x);
                imgBall.setY(pointF.y);

            }
        });

    }

關鍵代碼:valueAnimator.setEvaluator()方法,我設置了一個匿名內部類用於實現TypeEvaluator,仔細看肯定注意到了 log.d 語句,上圖!
這裏寫圖片描述
這裏的開始點是 (rootView.getWidth()/2, 0) 結束點是(width - imgBall.getWidth(), height - imgBall.getHeight()) 其實就是屏幕最上方中間,到屏幕右下角 fraction 由Interpolator的返回值決定, 按照公式自己套幾個值進去TypeEvaluator 的作用此時應該是明明白白了。

注意:TypeEvaluator的 evaluate方法中的的三個參數開始和結束值都是固定的,對應的是動畫開始位置和結束位置,fraction這個參數是TimeInterpolator中getInterpolation(float input) 計算完成之後的返回值;而這個input和動畫的duration有關。

InterpolatorDemo
這裏寫圖片描述

Interpolator用於控制動畫的變化速率,如圖我想控制小球從開始點到結束點做減速運動
代碼:

 /**
     *  這個方法hasFocus true表示頁面繪製完成,這樣 view.getWidth 之類的方法就不會返回 0
     * @param hasFocus
     */
    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if(hasFocus){
            init();
        }
    }

    private void init() {

        Interpolator line = new LinearInterpolator();//線性
        Interpolator acc = new AccelerateInterpolator();//加速
        Interpolator dce = new DecelerateInterpolator();//減速
        Interpolator accdec = new AccelerateDecelerateInterpolator();//先加速後減速

        int width = rootView.getWidth();
        int height = rootView.getHeight();
        PointF pointF1 = new PointF(rootView.getWidth()/2, 0);
        PointF pointF2 = new PointF(width / 2 - imgBall.getWidth(), height - imgBall.getHeight());

        ValueAnimator valueAnimator = new ValueAnimator();
        valueAnimator.setObjectValues(pointF1, pointF2);
        valueAnimator.setInterpolator(dce); //設置插值器 默認是 LinearInterpolator(線性);

        valueAnimator.setEvaluator(new TypeEvaluator() {
            @Override
            public Object evaluate(float fraction, Object startValue, Object endValue) {
                PointF startPoint = (PointF) startValue;
                PointF endPoint = (PointF) endValue;
                float x = startPoint.x + fraction * (endPoint.x - startPoint.x);
                float y = startPoint.y + fraction * (endPoint.y - startPoint.y);
                PointF point = new PointF(x, y);
                Log.d("TypeEvaluator","fraction" + fraction);
                return point;
            }
        });
        valueAnimator.setDuration(1000);
        valueAnimator.start();

        //這裏並沒有給 View 設置動畫,而是監聽動畫變化值,不斷更新View 的位置,
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                PointF pointF = (PointF) animation.getAnimatedValue();
                imgBall.setX(pointF.x);
                imgBall.setY(pointF.y);

            }
        });

    }

這裏我使用了系統提供的Interpolator
DecelerateInterpolator

    public float getInterpolation(float input) {
        float result;
        if (mFactor == 1.0f) {
            result = (float)(1.0f - (1.0f - input) * (1.0f - input));
        } else {
            result = (float)(1.0f - Math.pow((1.0f - input), 2 * mFactor));
        }
        return result;
    }

看見這些公式我就頭疼····,還是那句話 fraction是 TimeInterpolator中getInterpolation(float input) 計算完成之後的返回值,要查看這個result只需要去TypeEvaluator中的fraction 值就好了.
這裏寫圖片描述
具體我就不多說了,自己看數據吧。

東西弄清楚了,不弄幾個東西實踐一下總覺得有種拔劍四顧心茫然之感~
下面我說明一下,這些Demo都是網上找的,看着效果自己實踐了一遍,而效果的真正作者我也不知道是誰了~ 能確定的只有damajia,所以一下借鑑的代碼我就不說明出處了,如作者深究可聯繫我。
整個項目源碼會在文章末尾放出來,所以下面的Demo我只貼關鍵代碼了。

3. 組合動畫

這個動畫和本文無關,去網上搜索看見這動畫,感覺思路很有意思所以貼了出來。
這裏寫圖片描述
剛開始看着效果的時候說實話有點懵逼,不過看見下面那個圖我想肯定會一拍大腿~。

   private void starAnim() {
        ObjectAnimator ballAnim1 = ObjectAnimator.ofFloat(llPointCircle1,
                "rotation", 0, 360);
        ballAnim1.setDuration(2000);
        ballAnim1.setInterpolator(new AccelerateDecelerateInterpolator());

        ObjectAnimator ballAnim2 = ObjectAnimator.ofFloat(llPointCircle2,
                "rotation", 0, 360);
        ballAnim2.setStartDelay(150);
        ballAnim2.setDuration(2000);
        ballAnim2.setInterpolator(new AccelerateDecelerateInterpolator());

        ObjectAnimator ballAnim3 = ObjectAnimator.ofFloat(llPointCircle3,
                "rotation", 0, 360);
        ballAnim3.setStartDelay(2 * 150);
        ballAnim3.setDuration(2000);
        ballAnim3.setInterpolator(new AccelerateDecelerateInterpolator());

        ObjectAnimator ballAnim4 = ObjectAnimator.ofFloat(llPointCircle4,
                "rotation", 0, 360);
        ballAnim4.setStartDelay(3 * 150);
        ballAnim4.setDuration(2000);
        ballAnim4.setInterpolator(new AccelerateDecelerateInterpolator());

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.play(ballAnim1).with(ballAnim2).with(ballAnim3).with(ballAnim4);
        animatorSet.start();
    }

它其實就是一個組合動畫,然後給4個ViewAnim加了一個延時執行。

4.自定義TypeEvaluator

接着來看來看大神daimajia的一個動畫開源項目
這裏寫圖片描述

    private void setDropout(){
        int distance = tvView.getTop() + tvView.getHeight();
        AnimatorSet animSet = new AnimatorSet();

        ObjectAnimator alpha =  ObjectAnimator.ofFloat(tvView, "alpha", 0, 1);

        BounceEaseOut b = new BounceEaseOut(1000);
        ValueAnimator dropout = ObjectAnimator.ofFloat(tvView, "translationY", -distance, 0);
        dropout.setEvaluator(b);

        animSet.playTogether(alpha,dropout);
        animSet.setDuration(1200);
        animSet.start();
    }

這個其實是一個組合動畫,一個透明和一下下墜動畫,關鍵是這個下墜動畫來看BounceEaseOut的代碼

public class BounceEaseOut implements TypeEvaluator<Number> {

    private float mDuration;

    public BounceEaseOut(float mDuration) {
        this.mDuration = mDuration;
    }

    @Override
    public Number evaluate(float fraction, Number startValue, Number endValue) {
        float t = mDuration * fraction;
        float b = startValue.floatValue();
        float c = endValue.floatValue() - startValue.floatValue();
        float d = mDuration;

        if ((t/=d) < (1/2.75f)) {
            return c*(7.5625f*t*t) + b;
        } else if (t < (2/2.75f)) {
            return c*(7.5625f*(t-=(1.5f/2.75f))*t + .75f) + b;
        } else if (t < (2.5/2.75)) {
            return c*(7.5625f*(t-=(2.25f/2.75f))*t + .9375f) + b;
        } else {
            return c*(7.5625f*(t-=(2.625f/2.75f))*t + .984375f) + b;
        }
    }
}

更多的效果見https://github.com/daimajia/AndroidViewAnimations
這段公式大致意思是到一個時間點把View位置移動到終點,然後設置反覆回彈…
看見這計算驚喜不驚喜,意外不意外? 反正我高中數學早就丟給老師了,不過~
這裏寫圖片描述

5. 貝爾塞爾曲線公式結合TypeEvaluator的使用

···搞了半天總於到這裏了,我們做動畫的時候常常會看見什麼貝塞爾曲線,二階、三階、四階balabala···
OK 來看看這個Bezier到底是啥玩意。

貝塞爾曲線
貝塞爾曲線就是這樣的一條曲線,它是依據四個位置任意的點座標繪製出的一條光滑曲線。在歷史上,研究貝塞爾曲線的人最初是按照已知曲線參數方程來確定四個點的思路設計出這種矢量曲線繪製法。貝塞爾曲線的有趣之處更在於它的“皮筋效應”,也就是說,隨着點有規律地移動,曲線將產生皮筋伸引一樣的變換,帶來視覺上的衝擊。1962年,法國數學家Pierre Bézier第一個研究了這種矢量繪製曲線的方法,並給出了詳細的計算公式,因此按照這樣的公式繪製出來的曲線就用他的姓氏來命名是爲貝塞爾曲線。

···說白了它就是一個公式,分了很多階。

一階
這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

二階
這裏寫圖片描述

這裏寫圖片描述

這裏寫圖片描述

更多自行百度 Google~

下面看下用這個公式套入TypeEvaluator會有些什麼效果
這裏寫圖片描述

在購物車中,這種動畫應該是很常見的。

public class BezierEvaluator implements TypeEvaluator<Point> {

        private Point controllPoint;
        public BezierEvaluator(Point controllPoint) {
            this.controllPoint = controllPoint;
        }
        @Override
        public Point evaluate(float t, Point startValue, Point endValue) {
        //二階Bezier
            int x = (int) ((1 - t) * (1 - t) * startValue.x + 2 * t * (1 - t) * controllPoint.x + t * t * endValue.x);
            int y = (int) ((1 - t) * (1 - t) * startValue.y + 2 * t * (1 - t) * controllPoint.y + t * t * endValue.y);

            return new Point(x, y);
        }
    }

這裏大致說一下思路,整個是一個RecyclerView 然後給每一個條目的圖片一個點擊事件,每一次點擊獲取到條目上的圖片的位置,然後把整個位置設置爲startValue, 而結束值是底部那個圖片的位置,整個動畫關鍵在於這個 BezierEvaluator 沒啥好說的,照着二階的Bezier公式套就好了,每一次會返回Point的位置,無數個Point不斷變化就是我們看見的動畫了。

下面是一個綜合起來的例子,一個點贊效果。
這裏寫圖片描述

/**
 * 使用  bubbingLayout.setHeart();
 */

public class BubbingLayout extends RelativeLayout {

    private Interpolator line = new LinearInterpolator();//線性
    private Interpolator acc = new AccelerateInterpolator();//加速
    private Interpolator dce = new DecelerateInterpolator();//減速
    private Interpolator accdec = new AccelerateDecelerateInterpolator();//先加速後減速
    private Interpolator[] interpolators;

    private int mHeight;
    private int mWidth;
    private LayoutParams lp;
    private Drawable[] drawables;
    private Random random = new Random();

    private int dHeight;
    private int dWidth;

    public BubbingLayout(Context context) {
        super(context);
        init();
    }

    public BubbingLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public BubbingLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {

        //初始化小球
        drawables = new Drawable[3];
        Drawable red = getResources().getDrawable(R.drawable.red_ball);
        Drawable yellow = getResources().getDrawable(R.drawable.yellow_ball);
        Drawable blue = getResources().getDrawable(R.drawable.blue_ball);

        drawables[0] = red;
        drawables[1] = yellow;
        drawables[2] = blue;
        //獲取圖的寬高 用於後面的計算
        //注意 這裏由於圖片的大小都是一樣的,所以只取了一個
        dHeight = red.getIntrinsicHeight();
        dWidth = red.getIntrinsicWidth();

        //位置 底部 並且 水平居中
        lp = new LayoutParams(dWidth, dHeight);
        lp.addRule(CENTER_HORIZONTAL, TRUE);
        lp.addRule(ALIGN_PARENT_BOTTOM, TRUE);

        // 初始化插值器
        interpolators = new Interpolator[4];
        interpolators[0] = line;
        interpolators[1] = acc;
        interpolators[2] = dce;
        interpolators[3] = accdec;

    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
    }


    public void setHeart() {

        ImageView imageView = new ImageView(getContext());
        //隨機一個小球
        imageView.setImageDrawable(drawables[random.nextInt(3)]);
        imageView.setLayoutParams(lp);

        addView(imageView);

        Animator set = getAnimator(imageView);
        set.addListener(new AnimEndListener(imageView));
        set.start();
    }

    private Animator getAnimator(View target) {
        AnimatorSet set = getEnterAnimtor(target);

        ValueAnimator bezierValueAnimator = getBezierValueAnimator(target);

        AnimatorSet finalSet = new AnimatorSet();
        finalSet.playSequentially(set);
        finalSet.playSequentially(set, bezierValueAnimator);
        finalSet.setInterpolator(interpolators[random.nextInt(4)]);
        finalSet.setTarget(target);
        return finalSet;
    }

    /**
     * 開始動畫 先由暗到明 並且 中心放大
     * @param target
     * @return
     */
    private AnimatorSet getEnterAnimtor(final View target) {
        ObjectAnimator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, 0.2f, 1f);
        ObjectAnimator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, 0.2f, 1f);
        ObjectAnimator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, 0.2f, 1f);
        AnimatorSet enter = new AnimatorSet();
        enter.setDuration(500);
        enter.setInterpolator(new LinearInterpolator());
        enter.playTogether(alpha, scaleX, scaleY);
        enter.setTarget(target);
        return enter;
    }

    private ValueAnimator getBezierValueAnimator(View target) {

        //貝塞爾計算器 傳入三階公式的必要參數 ( 由於起點和終點已確定,這裏控制曲線則由中間2個點來控制 )
        HeartBezierEvaluator evaluator = new HeartBezierEvaluator(getPointF(2), getPointF(1));

        PointF startPoint = new PointF((mWidth - dWidth) / 2, mHeight - dHeight);
        PointF endPoint = new PointF(random.nextInt(getWidth()), 0);
        // 第三個參數是起點 第四個參數是終點
        ValueAnimator animator = ValueAnimator.ofObject(evaluator, startPoint,endPoint);
        animator.addUpdateListener(new BezierListener(target));
        animator.setTarget(target);
        animator.setDuration(3000);
        return animator;
    }

    /**
     * 獲取中間的兩個 點
     *
     * @param scale
     */
    private PointF getPointF(int scale) {

        PointF pointF = new PointF();
        pointF.x = random.nextInt((mWidth - 100));//減去100 是爲了控制 x軸活動範圍,看效果 隨意~~
        //再Y軸上 爲了確保第二個點 在第一個點之上,我把Y分成了上下兩半 這樣動畫效果好一些  也可以用其他方法
        pointF.y = random.nextInt((mHeight - 100)) / scale;
        return pointF;
    }

    private class BezierListener implements ValueAnimator.AnimatorUpdateListener {

        private View target;

        public BezierListener(View target) {
            this.target = target;
        }

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            //獲得曲線路徑點的值,不斷更新View的位置
            PointF pointF = (PointF) animation.getAnimatedValue();
            target.setX(pointF.x);
            target.setY(pointF.y);
            // 由明到暗的動畫, 注意 fraction 值是 一個由 0 到 1 的值。
            target.setAlpha(1 - animation.getAnimatedFraction());
        }
    }


    private class AnimEndListener extends AnimatorListenerAdapter {
        private View target;

        public AnimEndListener(View target) {
            this.target = target;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            //移除View
            removeView((target));
        }
    }
}

大致說一下,整個動畫曲線是由三階Bezier控制的,起點固定,終點的y值固定x值是Layou寬度的一個隨機值。每次曲線以及速率隨機生成(也就是TypeEvaluator和Interpolator 是隨機的),隨機範圍可以看代碼,並且自下而上添加了一個透明動畫。 代碼關鍵地方都有註釋,就不過多囉嗦了。

總結:TypeEvanluator和Interpolator是息息相關的,一個控制動畫的過渡方式,一個控制動畫的過度速率,動畫的過渡路徑可以藉助Beizer公式實現一些比較好看的曲線,而過渡速率系統已經提供了比較豐富的實現了(如果非要自己實現,可參考系統的計算公式~)

源碼 https://github.com/FmrChina/AnimaDemo

好了屬性動畫到這裏應該可以滿足絕大部分開發需求了,自己想實現一些比較酷的動畫,只要有耐心以及數學功底夠,應該也是不難的了,不過話說回來~數學是硬傷!

這裏提下個人認爲的動畫知識體系
動畫我大致分爲了三類
- 頁面元素動畫
- 頁面切換動畫
- 以及ViewPager或ReccyclerView加載動畫

這篇文章對應的便是頁面元素動畫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章