1. 程式人生 > >一個精美的跳動小球—手把手教你用貝塞爾曲線實現一個酷炫跳動動畫。

一個精美的跳動小球—手把手教你用貝塞爾曲線實現一個酷炫跳動動畫。

前言介紹

手把手教你用貝塞爾曲線實現一個精美的跳動的小球。

正文

效果展示:

說點題外話

一開始呢,我就想實現一個這樣的效果,於是就新建了一個專案開始擼,結果中間嘗試了幾種實現方案都不是很理想,也有一些條件未能實現就停止開發了,後面又去惡補了一下相關知識,突然在某一天的某一刻的某一瞬間靈感來了(就是這麼神奇~),於是又繼續擼程式碼,終於功夫不負有心人大致上實現了效果,還有待優化和調整,就在準備繼續擼的時候公司有一個專案要我接手,之前用H5開發的由於體驗性極差所以換成原生開發,於是乎加班生活就來了,強擼了大概半個月時間終於算是告一段落,這時候想起了我的這個效果還沒搞完,然後沒過半天,又安排了一個即將爛尾的專案給我,又來了沒完沒了的加班生活,有時候就在想為什麼加班沒有加班費的情況下加班現象還這麼普遍和嚴重???

(TAG:當你看到這裡的時候,我已經是第二天在繼續寫這篇部落格了)前兩天在上班時間抽出點時間總算是把這個效果徹底完善好了,現在回頭想想已經過了半個多月了,在寫的過程中修修改改不知道跑了多少遍,現在是時候做個總結了

照例先放個GitHub傳送門:

https://github.com/SuperKotlin/PointLoadingView

對於這篇部落格呢,我不想講的太細,因為裡面有些東西我怕越講越亂,我只講一下我的思想和分析過程以及實現過程,如果你真的想去實現一個這樣的效果,只要一步步走,把坑一個一個的填完,你才會明白我為什麼這麼去寫~理解最重要!!!

(TAG:當你看到這裡的時候,我已經是第三天在繼續寫這篇部落格了)好的,接下來就分析一下整個的實現的大流程~

  • 1.我們把整個過程分為兩個部分:第一部分是小球從最高點下降到最低點,第二部分是從最低點再彈跳到最高點,如此反覆;

  • 2.第一個部分:我只要判斷當球體接觸到繩子的瞬間使小球和繩子接觸著一起下降到最低點即可。

  • 3.第二個部分:小球正常彈起,在球線分離之前,繩子和小球都是一塊上升,球線分離之後繩子開始回彈再回彈~

  • 備註:這裡面最難處理的就是曲線的狀態,我們考慮用二階貝塞爾曲線來做,只要動態改變曲線的控制點座標即可,

  • 而這個控制點的座標可以根據曲線的中心點座標(就是繩子的中點)套用貝塞爾曲線公式求得,所以我們只要根據小球的圓心座標來求出繩子中點座標,進而求出控制點來繪製貝塞爾曲線即可。可能說的太籠統不好理解,這裡我畫了兩個草圖可以幫助理解~

第一個:下降過程

第二個:彈起過程

如果覺得還是不夠幫助理解,那好,我再放個慢鏡頭gif效果(說真的,每次擷取gif圖都是耗時間頭疼的一件事,我用的迅雷影音截的,沒有什麼好工具,截取出來不是掉幀就是莫名其妙的少那麼一兩個片段,求推薦好的工具~)。

對著示意圖,我們來分析一下如何根據效果去寫程式碼。

先看下降過程:

圖①:小球在最高點,準備下降;

圖②:小球接觸繩子,這個時候小球壓著繩子一塊下降一段距離;

圖③:從小球開始接觸繩子,這個時候獲取到圓心Y座標,根據這個座標可以求出接觸點(也就是繩子中點)的Y座標,進而求出曲線控制點Y座標。

再看上升過程:

圖①:小球在最低點,準備上升,假如繩子這裡下降100個畫素;

圖②:小球彈起,繩子上升了100個畫素,這個時候小球和繩子即將分離 ; 圖③:小球繼續上升,繩子開始回彈。這裡回彈50個畫素

圖④:繩子靜止。

好吧,到了這裡我基本上已經分析完了,具體實現過程就看程式碼吧~程式碼中我也寫了很詳細的註釋。

PointLoadingView.java

/**
 * 貝塞爾曲線控制元件
 * Created by zhuyong on 2017/7/18.
 * 實現思想:
 * 1.我們把整個過程分為兩個部分:第一部分是小球從最高點下降到最低點,第二部分是從最低點再彈跳到最高點,如此反覆;
 * 2.第一個部分:我只要判斷當球體接觸到繩子的瞬間使小球和繩子接觸著一起下降到最低點即可。
 * 3.第二個部分:小球正常彈起,在球線分離之前,繩子和小球都是一塊上升,球線分離之後繩子開始回彈再回彈~
 * 備註:這裡面最難處理的就是曲線的狀態,我們考慮用二階貝塞爾曲線來做,只要動態改變曲線的控制點座標即可,
 * 而這個控制點的座標可以根據曲線的中心點座標(就是繩子的中點)套用貝塞爾曲線公式求得,所以我們只要根據
 * 小球的圓心座標來求出繩子中點座標,進而求出控制點來繪製貝塞爾曲線即可。
 */
public class PointLoadingView extends View {  ......      @Override    protected void onDraw(Canvas canvas) {        super.onDraw(canvas);         ......        //繪製彈跳的小球        canvas.drawCircle(mViewWidth / 2, mY, mPointRadius, paintCircle);    }    @Override    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {        super.onLayout(changed, left, top, right, bottom);        mY = mViewHeight / 2 - mJumpHeight;//小球圓心座標(X座標不變,Y座標等於view高度的一半減去彈跳高度)        mBerSaiErY = mViewHeight / 2;//貝塞爾曲線控制點座標(X座標不變,在最開始的時候,Y座標等於view高度的一半)        start = new Point(0 + 10 + mPaintWidth + getPaddingLeft(), mViewHeight / 2);//左端點        end = new Point(mViewWidth - 10 - mPaintWidth - getPaddingRight(), mViewHeight / 2);//右端點        //這裡判斷mJumpHeight能不能達到需要的高度,如果不行則根據view高度重新計算(跳起高度最大不能超過view高度的一半減去小球的半徑)        if (mJumpHeight > mViewHeight / 2 - mPointRadius / 2) {            mJumpHeight = mViewHeight / 2 - mPointRadius;//為什麼減去直徑而不是半徑,因為小球彈起的起點高度是從半徑開始的        }        //為什麼是3倍,因為這個過程中球上升的高度是100的3倍,就像我們預設是繩子下降100個畫素,        // 然後上升100個畫素後球線分離,線再下降150個畫素,線在上升50個畫素到最後繩子靜止,而這個過程中球上升的高度是300個畫素        if (mJumpHeight <= 3 * mDownPx) {//球上升的最大高度必須大於3倍的繩子下降最低距離,這裡減去10個畫素之差,否則繩子會失去彈性效果            mDownPx = mJumpHeight / 3 - 10;        }        initAnimator();    }      /**     * 初始化動畫     */    private void initAnimator() {      
        //(下降過程部分)        //功能:球從最高處降落到最低處        //這個過程中需要考慮兩個過程,一是球下降到接觸繩子之前,二是接觸繩子之後一直到下降到最低點然後結束。        //mY的範圍是從最高點下降到最低點(mViewHeight / 2 - mJumpHeight——>mViewHeight / 2 + mDownPx)        //這裡我們實時獲取到mY(小球圓心Y座標),根據mY的值判斷何時接觸繩子,接觸繩子之後開始給控制點mBerSaiErY賦值,重新繪製貝塞爾曲線。        mAnimatorDown = ValueAnimator.ofFloat(mViewHeight / 2 - mJumpHeight, mViewHeight / 2 + mDownPx);        mAnimatorDown.setDuration(TIME);        mAnimatorDown.setInterpolator(new AccelerateInterpolator());//加速下降        mAnimatorDown.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {             @Override          
            public void onAnimationUpdate(ValueAnimator valueAnimator)
{                mY = (float) valueAnimator.getAnimatedValue();                if (mY < mViewHeight / 2 - mPointRadius - mPaintWidth / 2) {//小球下降,沒有接觸繩子                    mBerSaiErY = mViewHeight / 2;//在這個過程中繩子沒有發生變化                } else if (mY >= mViewHeight / 2 - mPointRadius - mPaintWidth / 2 && mY <= mViewHeight / 2 + mDownPx) {                    //在這個過程中,繩子貼著小球一塊下降到最低點。                    mBerSaiErY = getControlPointF(start, end,                            new Point(mViewWidth / 2, mY + mPointRadius + mPaintWidth / 2)).getY();                }              
              invalidate();//重新繪製            }        });        mAnimatorDown.addListener(new AnimatorListenerAdapter() {            @Override            
           public void onAnimationEnd(Animator animation)
{                super.onAnimationEnd(animation);                up();//當下降完成後開始上升            }        });        
       //(彈起過程部分)        //功能:球從最低處彈起到最高處        //這個過程中需要考慮兩個部分,第一部分是小球一直在上升,第二部分是繩子的回彈效果。        //小球:小球的上升很好理解,直接從最低點到最高點減速上升        //繩子:小球上升到距離水平面一定距離後(這裡預設和繩子下降的最低距離相同),小球和繩子分離,小球繼續上升,繩子開始回彈。        //繩子的回彈過程,從最低點彈起——>水平面(繩子水平是的位置)——>上升到球線分離——>水平面(繩子水平是的位置)——>最低點一半的距離——>水平面(繩子水平是的位置)        mAnimatorUp = ValueAnimator.ofFloat(mViewHeight / 2 + mDownPx, mViewHeight / 2 - mJumpHeight);        mAnimatorUp.setInterpolator(new DecelerateInterpolator());//減速上升        mAnimatorUp.setDuration(TIME);        mAnimatorUp.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {              @Override            
             public void onAnimationUpdate(ValueAnimator valueAnimator)
{                mY = (float) valueAnimator.getAnimatedValue();                if (mY >= mViewHeight / 2 - mDownPx - mPointRadius - mPaintWidth / 2                        && mY <= mViewHeight / 2 + mDownPx) {//上升100個畫素後球線分離                    mBerSaiErY = getControlPointF(start, end,                            new Point(mViewWidth / 2, mY + mPointRadius + mPaintWidth / 2)).getY();                } else if (mY >= mViewHeight / 2 - mDownPx - (mDownPx + mDownPx / 2) - mPointRadius - mPaintWidth / 2                        && mY < mViewHeight / 2 - mDownPx - mPointRadius - mPaintWidth / 2) {//線下降150個畫素                    //獲取曲線上的中心點座標                    float mCenterY = (mViewHeight / 2 - mDownPx - mPointRadius - mPaintWidth / 2) * 2 - mY;                    //根據中心點座標獲取曲線的控制點座標Y                    mBerSaiErY = getControlPointF(start, end, new Point(mViewWidth / 2, mCenterY + mPointRadius + mPaintWidth / 2)).getY();                } else if (mY >= mViewHeight / 2 - mDownPx - (mDownPx + mDownPx / 2) - mDownPx / 2 - mPointRadius - mPaintWidth / 2                        && mY < mViewHeight / 2 - mDownPx - (mDownPx + mDownPx / 2) - mPointRadius - mPaintWidth / 2) {//線上升50個畫素                    //獲取曲線上的中心點座標                    float mCenterY = mY + 3 * mDownPx;                    //根據中心點座標獲取曲線的控制點座標Y                    mBerSaiErY = getControlPointF(start, end, new Point(mViewWidth / 2, mCenterY + mPointRadius + mPaintWidth / 2)).getY();                } else {//線靜止                    mBerSaiErY = mViewHeight / 2;                }                invalidate();//重新繪製            }        });        mAnimatorUp.addListener(new AnimatorListenerAdapter() {            @Override            public void onAnimationEnd(Animator animation) {                super.onAnimationEnd(animation);                Startdown();//當上升完成後開始下降            }        });    } ...... }

裡面的座標類我沒有使用系統的PointF類,而是自己寫了一個Point類:

Point.java

package com.zhuyong.pointloading;/**
 * Created by zhuyong on 2017/7/18.
 */

public class Point {    // 設定兩個變數用於記錄座標的位置    private float x;    
   private float y;    // 構造方法用於設定座標    public Point(float x, float y) {        
        this.x = x;  
        this.y = y;    }    
   // get方法用於獲取座標    public float getX() {        
       return x;    }    
   
   public
float getY()
{        
        return y;    }  
   public void setX(float x) {    
      this.x = x;    }    
 
  public void setY(float y) {    
          this.y = y;    } }

最後只要在activity中呼叫Startdown方法就好了

mPointLoadingView = (PointLoadingView) findViewById(R.id.BeisaierTwoView);
mPointLoadingView.post(new Runnable() {    
  @Override    public void run() {        mPointLoadingView.Startdown();    } });

唉,這個view這篇部落格前前後後折騰了大半個月,今天終於趁著週末把它完成了,好了,我也可以安心的去吃一個冰激凌了(手動滑稽~)!!!

最後再補一句:這樣的效果,我的實現方案肯定不是最好的,而且有些地方判斷考慮的不是非常完美,肯定會有bug存在,不過這都不是最重要的,重要的是分析的過程和在實際開發中遇到的問題如何去一個一個的解決,去實現我們想要的效果~我覺得不管是寫程式碼也好還是去做其他的事情也好,都要帶著自己的思想去做,心中要明白這一步該做什麼,下一步該做什麼,這個才是進步的關鍵吧我覺得!!!

demo地址:

https://github.com/SuperKotlin/PointLoadingView

部落格地址:

http://www.jianshu.com/p/2b0a53957bb5

終端研發部提倡沒有做不到的,只有想不到的

在這裡獲得的不僅僅是技術!

讓心,在陽光下學會舞蹈

讓靈魂,在痛苦中學會微笑

—終端研發部—


如果你覺得此文對您有所幫助,歡迎入群 QQ交流群 :232203809   

微信公眾號:終端研發部