1. 程式人生 > >Android開發之貝塞爾曲線進階篇(仿直播送禮物,餓了麼購物車動畫)

Android開發之貝塞爾曲線進階篇(仿直播送禮物,餓了麼購物車動畫)

又是一年畢業季,今年終於輪到我了,最近一邊忙著公司的專案,一邊趕著畢設和論文,還私下和朋友搞了些小外包,然後還要還抽出時間寫部落格,真是忙的不要不要的。

好了,言歸正傳,前幾天寫了一篇關於貝塞爾曲線的基礎篇,如果你對貝塞爾曲線還不是很瞭解,建議你先去閱讀下:Android開發之貝塞爾曲線初體驗 ,今天這篇文章主要來講講關於貝塞爾曲線的實際應用。

國際慣例,先來看下今天要實現的效果圖:


仿直播送禮動畫 
仿餓了麼購物車動畫

上面兩張圖分別是仿直播平臺送禮動畫和餓了麼商品加入購物車動畫。

1、小試牛刀

我們先來熱熱身,這裡我打算用二階貝塞爾曲線畫出動態波浪的效果,效果如下:


動態波浪

效果還是不錯的,很自然的動畫呈現,平滑的過渡。
我們來一步步分析下:
1、首先,我們先單純的思考螢幕內的可見區域,可以把它理解成近似一個週期的sin函式,只是它的幅度沒有那麼高,類似下圖:


sin函式

根據上面的圖,其實我們可以發現它的起始點分別是(0,0)和(2π,0),控制點分別是(π/2,1)和(3π/2,-1),由於有兩個控制點,所以這裡可以用三階貝塞爾曲線來畫,不過我暫時打算先用二階貝塞爾曲線來畫,也就是把上面的圖拆分成兩部分:
第一部分:起始點為(0,0)和(π,0),控制點為(π/2,1)
第二部分:起始點為(π,0)和(2π,0),控制點為(3π/2,-1)
然後我們把2π的距離當成是螢幕的寬度,那麼π的位置就是螢幕寬度的一半,這樣分解下來,配合谷歌官方給我們提供的API,我們就可以很好的實現這2段曲線的繪製,我們先暫定波浪的高度為100px,實現程式碼也就是:

        mPath.moveTo
(0, mScreenHeight / 2); mPath.quadTo(mScreenWidth / 4, mScreenHeight / 2 - 100, mScreenWidth / 2 , mScreenHeight / 2); mPath.quadTo(mScreenWidth * 3 / 4, mScreenHeight / 2 + 100, mScreenWidth , mScreenHeight / 2);

然後我們把下面的空白區域鋪滿:

        mPath.lineTo(mScreenWidth, mScreenHeight);
        mPath.lineTo
(0, mScreenHeight);

來看下此時的效果圖:


波浪圖

2、實現了初步的效果,那現在我們就應該來思考如何讓這個波浪動起來,其實很簡單,只需要我們在螢幕外再畫出另一週期的曲線,然後讓它做平移動畫這樣就可以了,熟悉sin函式的朋友,肯定能想到下面這幅圖:


sin函式

現在我們把螢幕外的另一半也曲線也畫出來(具體座標這裡就不再寫出來了,大家畫下圖就能清楚):

        mPath.moveTo(-mScreenWidth + mOffset, mScreenHeight / 2);
        mPath.quadTo(-mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 - 100, -mScreenWidth / 2 + mOffset, mScreenHeight / 2);
        mPath.quadTo(-mScreenWidth / 4 + mOffset, mScreenHeight / 2 + 100, 0 + mOffset, mScreenHeight / 2);
        mPath.quadTo(mScreenWidth / 4 + mOffset, mScreenHeight / 2 - 100, mScreenWidth / 2 + mOffset, mScreenHeight / 2);
        mPath.quadTo(mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 + 100, mScreenWidth + mOffset, mScreenHeight / 2);

3、平移動畫的實現,這裡我們利用到了Android3.0以後給我們提供的屬性動畫,然後平移長度即為一個週期長度(螢幕寬度):

    /**
     * 設定動畫效果
     */
    private void setViewanimator() {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(0, mScreenWidth);
        valueAnimator.setDuration(1200);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mOffset = (int) animation.getAnimatedValue();//當前平移的值
                invalidate();
            }
        });
        valueAnimator.start();
    }

拿到平移的值後,我們只需要在各點的x軸動態的加上值,這樣就會呈現出動態波浪了。

        mPath.quadTo(-mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 - 100, -mScreenWidth / 2 + mOffset, mScreenHeight / 2);
        mPath.quadTo(-mScreenWidth / 4 + mOffset, mScreenHeight / 2 + 100, 0 + mOffset, mScreenHeight / 2);
        mPath.quadTo(mScreenWidth / 4 + mOffset, mScreenHeight / 2 - 100, mScreenWidth / 2 + mOffset, mScreenHeight / 2);
        mPath.quadTo(mScreenWidth * 3 / 4 + mOffset, mScreenHeight / 2 + 100, mScreenWidth + mOffset, mScreenHeight / 2);

可以簡化寫成

        for (int i = 0; i < 2; i++) {
            mPath.quadTo(-mScreenWidth * 3 / 4 + (mScreenWidth * i) + mOffset, mScreenHeight / 2 - 100, -mScreenWidth / 2 + (mScreenWidth * i) + mOffset, mScreenHeight / 2);
            mPath.quadTo(-mScreenWidth / 4 + (mScreenWidth * i) + mOffset, mScreenHeight / 2 + 100, +(mScreenWidth * i) + mOffset, mScreenHeight / 2);
        }

2、仿餓了麼商品加入動畫效果:

如果你理解了上面的“小試牛刀”例子,要實現這個效果就非常容易了,首先我們要確定新增購物車“+”的位置,然後確定購物車的位置,也就是我們貝塞爾曲線的起始點了,然後再給出一個控制點,只需要讓它比“+”的位置高一些,讓它成拋物線的效果即可。

1、要確定一個View所在螢幕內的位置,我們可以利用谷歌官方給我們提供的API(具體根據介面中的佈局來確定):
getLocationInWindow(一個控制元件在其父視窗中的座標位置)
getLocationOnScreen(一個控制元件在其整個螢幕上的座標位置)

 /**
     * <p>Computes the coordinates of this view on the screen. The argument
     * must be an array of two integers. After the method returns, the array
     * contains the x and y location in that order.</p>
     *
     * @param outLocation an array of two integers in which to hold the coordinates
     */
    public void getLocationOnScreen(@Size(2) int[] outLocation) {
        getLocationInWindow(outLocation);

        final AttachInfo info = mAttachInfo;
        if (info != null) {
            outLocation[0] += info.mWindowLeft;
            outLocation[1] += info.mWindowTop;
        }
    }

    /**
     * <p>Computes the coordinates of this view in its window. The argument
     * must be an array of two integers. After the method returns, the array
     * contains the x and y location in that order.</p>
     *
     * @param outLocation an array of two integers in which to hold the coordinates
     */
    public void getLocationInWindow(@Size(2) int[] outLocation) {
        if (outLocation == null || outLocation.length < 2) {
            throw new IllegalArgumentException("outLocation must be an array of two integers");
        }

        outLocation[0] = 0;
        outLocation[1] = 0;

        transformFromViewToWindowSpace(outLocation);
    }

這裡可以獲取到一個int型別的陣列,陣列下標0和1分別代表著x和y座標,需要注意的一點是,別在onCreate裡去呼叫這個方法(點選事件內可以),否則獲取到的座標只會是(0,0),這個方法需要在Activity獲取到焦點後呼叫才有效果。

2、當我們拿到了這3點座標,我們就可以畫出對應的貝塞爾曲線。然後我們只需要讓這個小紅點在這條曲線路徑裡去做平滑移動就可以了,由於小紅點是帶有x,y座標的,曲線的每一個點也是帶有x,y座標的,聰明的你應該已經想到這裡還是一樣用到了屬性動畫,動態的去改變當前小紅點的x,y座標即可。由於谷歌官方只給我們提供了一些比較基礎的插值器,比如Int,Float,Argb等,並沒有給我們提供關於座標的插值器,不過好在它給我們開放了相關介面,我們只需要對應的去實現它即可,這個介面叫TypeEvaluator:

/**
 * Interface for use with the {@link ValueAnimator#setEvaluator(TypeEvaluator)} function. Evaluators
 * allow developers to create animations on arbitrary property types, by allowing them to supply
 * custom evaluators for types that are not automatically understood and used by the animation
 * system.
 *
 * @see ValueAnimator#setEvaluator(TypeEvaluator)
 */
public interface TypeEvaluator<T> {

    /**
     * This function returns the result of linearly interpolating the start and end values, with
     * <code>fraction</code> representing the proportion between the start and end values. The
     * calculation is a simple parametric calculation: <code>result = x0 + t * (x1 - x0)</code>,
     * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
     * and <code>t</code> is <code>fraction</code>.
     *
     * @param fraction   The fraction from the starting to the ending values
     * @param startValue The start value.
     * @param endValue   The end value.
     * @return A linear interpolation between the start and end values, given the
     *         <code>fraction</code> parameter.
     */
    public T evaluate(float fraction, T startValue, T endValue);

}

從註釋裡我們可以得到這些資訊,首先我們需要去實現evaluate方法,然後這裡提供了3個回撥引數,它們分別代表:
float fraction:動畫的完成程度,0~1
T startValue:動畫開始值
T endValue: 動畫結束值(這裡而外補充一點,要想得到當前的動畫值其實也很簡單,只需要用(動畫開始值+動畫完成程度*動畫結束值))
這裡貼下關於小紅點移動座標的插值器程式碼:(Point是系統自帶的類,可以用來記錄X,Y座標點)

    /**
     * 自定義Evaluator
     */
    public class CirclePointEvaluator implements TypeEvaluator {

        /**
         * @param t   當前動畫進度
         * @param startValue 開始值
         * @param endValue   結束值
         * @return
         */
        @Override
        public Object evaluate(float t, Object startValue, Object endValue) {

            Point startPoint = (Point) startValue;
            Point endPoint = (Point) endValue;

            int x = (int) (Math.pow((1-t),2)*startPoint.x+2*(1-t)*t*mCircleConPoint.x+Math.pow(t,2)*endPoint.x);
            int y = (int) (Math.pow((1-t),2)*startPoint.y+2*(1-t)*t*mCircleConPoint.y+Math.pow(t,2)*endPoint.y);

            return new Point(x,y);
        }

    }

這裡的x和y是根據二階貝塞爾曲線計算出來的,對應的公式為:


二階貝塞爾表示式

然後我們在值變化監聽器中去不斷繪製這個小紅點的位置就可以了。

        //設定值動畫
        ValueAnimator valueAnimator = ValueAnimator.ofObject(new CirclePointEvaluator(), mCircleStartPoint, mCircleEndPoint);
        valueAnimator.setDuration(600);
        valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                Point goodsViewPoint = (Point) animation.getAnimatedValue();
                mCircleMovePoint.x = goodsViewPoint.x;
                mCircleMovePoint.y = goodsViewPoint.y;
                invalidate();
            }
        });

3、仿直播送禮物:

有了前兩個例子的基礎,現在要做類似於這種運動軌跡的效果是不是很有感覺了?打鐵要趁熱,我們接著來說直播送禮這個效果。
首先,我們先簡化一下,看下圖:


仿直播送禮

1、首先我們需要知道這條曲線的路徑要怎麼畫,這裡我應該不需要我再說了,三階貝塞爾曲線,起始點和結束點分別為(螢幕寬度的一半,螢幕高度)和(螢幕寬度的一半,0),然後控制點有2個,分別是(螢幕寬度,四分之三螢幕高度)和(0,四分之一螢幕高度)

        mPath.moveTo(mStartPoint.x, mStartPoint.y);
        mPath.cubicTo(mConOnePoint.x, mConOnePoint.y, mConTwoPoint.x, mConTwoPoint.y, mEndPoint.x, mEndPoint.y);
        canvas.drawPath(mPath, mPaint);

2、然後我們來說下關於這個星星的實現,這裡是用到一張星星的圖片,通過資原始檔轉Bitmap物件,再賦予給所建立的Canvas畫布,然後通過Xfermodes將圖片進行渲染變色,最後通過ImageView來載入。


來自Graphics下的XferModes

這裡我們取SrcIn模式,也就是我們先繪製Dst(資原始檔),然後再繪製Src(畫筆顏色),當我們設定SrcIn模式時,自然就剩下的Dst的形狀+Src的顏色,也就是不同顏色的星星。

    /**
     * 畫星星並隨機賦予不同的顏色
     *
     * @param color
     * @return
     */
    private Bitmap drawStar(int color) {
        //建立和資原始檔Bitmap相同尺寸的Bitmap填充Canvas
        Bitmap outBitmap = Bitmap.createBitmap(mBitmap.getWidth(), mBitmap.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(outBitmap);
        canvas.drawBitmap(mBitmap, 0, 0, mPaint);
        //利用Graphics中的XferModes對Canvas進行著色
        canvas.drawColor(color, PorterDuff.Mode.SRC_IN);
        canvas.setBitmap(null);
        return outBitmap;
    }

3、接下來就是讓星星動起來,老套路,我們利用屬性動畫,去獲取貝塞爾曲線上的各點座標位置,然後動態的給ImageView設定座標即可。這裡的座標點我們需要通過三階貝塞爾曲線公式來計算:


三階貝塞爾表示式
   public class StarTypeEvaluator implements TypeEvaluator<Point> {

        @Override
        public Point evaluate(float t, Point startValue, Point endValue) {
            //利用三階貝塞爾曲線公式算出中間點座標
            int x = (int) (startValue.x * Math.pow((1 - t), 3) + 3 * mConOnePoint.x * t * Math.pow((1 - t), 2) + 3 *
                    mConTwoPoint.x * Math.pow(t, 2) * (1 - t) + endValue.x * Math.pow(t, 3));
            int y = (int) (startValue.y * Math.pow((1 - t), 3) + 3 * mConOnePoint.y * t * Math.pow((1 - t), 2) + 3 *
                    mConTwoPoint.y * Math.pow(t, 2) * (1 - t) + endValue.y * Math.pow(t, 3));
            return new Point(x, y);
        }
    }

4、然後再帶上一個漸隱(透明度)的屬性動畫動畫即可。

        //設定屬性動畫
        ValueAnimator valueAnimator = ValueAnimator.ofObject(new StarTypeEvaluator(pointFFirst, pointFSecond), pointFStart,
                pointFEnd);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                Point point = (Point) animation.getAnimatedValue();
                imageView.setX(point.x);
                imageView.setY(point.y);
            }
        });

        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                StarViewGroup.this.removeView(imageView);
            }
        });


        //透明度動畫
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(imageView, "alpha", 1.0f, 0f);

        //組合動畫
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(3500);
        animatorSet.play(valueAnimator).with(objectAnimator);
        animatorSet.start();


        valueAnimator.start();

這樣我們就實現了上面簡化版的效果,然後我們來完成下最終滿屏星星。1、首先,這個星星我們是通過資原始檔載入到Canvas畫布,然後再裝載到ImageView裡去顯示,現在螢幕裡有很多星星,所以我們考慮自定義一個ViewGroup,讓其繼承於RelativeLayout。

2、再來觀察下效果圖,發現這些星星大致是往一定的軌跡在飄動,但是位置好像又不是一層不變的,所以這裡我們可以知道,這4個關鍵點(起始點,結束點,2個控制點)是會變化的,所以我們只可以監聽下這個ViewGroup的onTouch事件,在使用者觸控式螢幕幕的時候,去動態生成這幾個點的座標,其他的就沒變化了,根據三階貝塞爾曲線公式就可以星星當前所在的位置,然後進行繪製。

    /**
     * 監聽onTouch事件,動態生成對應座標
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mStartPoint = new Point(mScreenWidth / 2, mScreenHeight);
        mEndPoint = new Point((int) (mScreenWidth / 2 + 150 * mRandom.nextFloat()), 0);
        mConOnePoint = new Point((int) (mScreenWidth * mRandom.nextFloat()), (int) (mScreenHeight * 3 * mRandom.nextFloat() / 4));
        mConTwoPoint = new Point(0, (int) (mScreenHeight * mRandom.nextFloat() / 4));

        addStar();
        return true;
    }

好了,文章到這裡就結束了,由於篇幅限制,這裡不能對一些東西講的太細,比如一些自定義View的基礎,還有屬性動畫的用法,大家自行查閱相關資料哈。

原始碼下載:

這裡附上原始碼地址(歡迎Star,歡迎Fork):安卓巴士