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):源碼下載

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370

推薦閱讀更多精彩內容