1. 程式人生 > >Android-貝塞爾曲線

Android-貝塞爾曲線

從去年開始瞭解貝塞爾曲線之後,發現開發中,不管是Android/Ios平臺,還是web前端等,都有貝塞爾曲線的應用,通過繪製貝塞爾曲線,可以幫助開發者實現很多效果,例如一段時間內很流行的粘合型的下拉重新整理、又如天氣曲線圖,同時,以貝塞爾曲線為基礎的貝塞爾工具是所有繪圖軟體的最常用最實用的工具。

什麼是貝塞爾曲線

貝塞爾曲線(Bézier curve),又稱貝茲曲線或貝濟埃曲線,是應用於二維圖形應用程式的數學曲線。一般的向量圖形軟體通過它來精確畫出曲線,貝茲曲線由線段節點組成,節點是可拖動的支點,線段像可伸縮的皮筋,我們在繪圖工具上看到的鋼筆工具就是來做這種向量曲線的。主要結構:起始點、終止點(也稱錨點)、控制點。通過調整控制點,貝塞爾曲線的形狀會發生變化。


貝塞爾曲線的分類 

瞭解一下貝塞爾曲線,根據影響變數的個數不同,我們可以看到不同型別的曲線

一階貝塞爾曲線(線段):

公式: 


意義:由 P0 至 P1 的連續點, 描述的一條線段


二階貝塞爾曲線(拋物線)

公式: 



原理:由 P0 至 P1 的連續點 Q0,描述一條線段。 由 P1 至 P2 的連續點 Q1,描述一條線段。 由 Q0 至 Q1 的連續點 B(t),描述一條二次貝塞爾曲線。


三階貝塞爾曲線:



當然還有四階曲線、五階曲線......只不過隨著變數的增加,複雜維度會越來越高

雖然從公式上理解是非常難得,我們在開發中,也不是必須要完全理解這些公式,大概知道原理即可,通過這篇文章,我們可以大概理解它的圖形上面的變化實現 

http://www.html-js.com/article/1628


貝塞爾曲線程式碼實現:

我們一般使用的是二階貝塞爾曲線和三階貝塞爾曲線,從動態圖和公式我們可以看出,貝塞爾曲線主要由於三個部分控制:起點,終點,中間的輔助控制點。如何利用這三個點畫出貝塞爾曲線,在android自帶的Path類中自帶了方法,可以幫助我們實現貝塞爾曲線:

/**
 * Add a quadratic bezier from the last point, approaching control point
 * (x1,y1), and ending at (x2,y2). If no moveTo() call has been made for
 * this contour, the first point is automatically set to (0,0).
 *
 * @param x1 The x-coordinate of the control point on a quadratic curve
 * @param y1 The y-coordinate of the control point on a quadratic curve
 * @param x2 The x-coordinate of the end point on a quadratic curve
 * @param y2 The y-coordinate of the end point on a quadratic curve
 */
public void quadTo(float x1, float y1, float x2, float y2) {
    isSimplePath = false;
    native_quadTo(mNativePath, x1, y1, x2, y2);
}


quadTo()方法從上一個點為起點開始繪製貝塞爾曲線,其中(x1,y1)為輔助控制點,(x2,y2)為終點。

Path mPath = new Path();
mPath.moveTo(x0,y0);
mPath.quadTo(x1,y1,x2,y2);

如呼叫以上程式碼,即繪製起點(x0,y0),終點(x2,y2),輔助控制點(x1,y1)的貝塞爾曲線。因此,通過不斷改變這三個點的位置,我們可以繪製出各種各樣的曲線



/**
 * Add a cubic bezier from the last point, approaching control points
 * (x1,y1) and (x2,y2), and ending at (x3,y3). If no moveTo() call has been
 * made for this contour, the first point is automatically set to (0,0).
 *
 * @param x1 The x-coordinate of the 1st control point on a cubic curve
 * @param y1 The y-coordinate of the 1st control point on a cubic curve
 * @param x2 The x-coordinate of the 2nd control point on a cubic curve
 * @param y2 The y-coordinate of the 2nd control point on a cubic curve
 * @param x3 The x-coordinate of the end point on a cubic curve
 * @param y3 The y-coordinate of the end point on a cubic curve
 */
public void cubicTo(float x1, float y1, float x2, float y2,
                    float x3, float y3) {
    isSimplePath = false;
    native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}

cubicTo()方法從上一個點為起點開始繪製三階貝塞爾曲線,其中(x1,y1),( x2, y2 )為輔助控制點,(x3,y3)為終點。

貝塞爾曲線的應用

(1)二階貝塞爾曲線——波浪

要實現一個波浪不斷湧動的效果,這種效果在很多手機應用中可見,例如手機電量,記憶體剩餘等。類似這種需要實現波浪的效果,我們需要繪製帶有平滑自然效果的曲線,這時候就需要貝塞爾曲線來輔助了。

動態圖:

    

原理圖:

   

圖中的矩陣即為檢視的可見範圍,也就是我們手機常見的區域。通過屬性動畫類ValueAnimator不斷改變點1的橫座標,隨著點1橫座標向右移動,點2,點3,點4,點5,以及四個控制點的座標隨著點1的移動同時位移相同距離,每一次座標點更新,我們呼叫一次invalidate()方法,呼叫draw重新繪製檢視,繪製四段貝塞爾曲線。最後點1移動到原先點3的位置,這樣就完成了一次動畫。

這樣,通過迴圈不斷的動畫效果,我們就實現了波浪的效果。

#onDraw() 程式碼:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (!mIsRunning || !mHasInit)
        return;
    mPath.reset();
    mPath.moveTo(mLeft1.x, mLeft1.y);
    mPath.quadTo(mControlLeft1.x, mControlLeft1.y, mLeft2.x, mLeft2.y);
    mPath.quadTo(mControlLeft2.x, mControlLeft2.y, mFirst.x, mFirst.y);
    mPath.quadTo(mControlFirst.x, mControlFirst.y, mSecond.x, mSecond.y);
    mPath.quadTo(mControlSecond.x, mControlSecond.y, mRight.x, mRight.y);
    mPath.lineTo(mRight.x, mHeight);
    mPath.lineTo(mLeft1.x, mHeight);
    mPath.lineTo(mLeft1.x, mLeft1.y);
    canvas.drawPath(mPath, mPaint);
}

(2)二階貝塞爾曲線——粘連體

利用二階貝塞爾曲線還可以實現,類似兩種物體粘合在一起的效果,比如我們常用的qq,在qq聊天列表上有一個非常有意思的功能,就是當我們用手指移動聊天列表上的未讀訊息標誌的時候,它與聊天列表會產生粘連的效果:

    

    

     現在,我們來模擬這種效果,利用學到的二階貝塞爾曲線來繪製。

     

   

我們看到原理圖,基本構造為兩個圓,和兩端貝塞爾曲線,繪製貝塞爾曲線,由於這是一個二階的貝塞爾曲線,我們只需要一個控制點,在這個圖裡,我們的兩條貝塞爾曲線的兩個控制點分別為(x1,y1)(x4, y4)的中點,(x2, y2)(x3, y3)的中點。

從圖中可以看出,我們的貝塞爾曲線由我們的控制點控制,控制點又是被起點和終點控制著,因此,當兩個圓距離越大,曲線越趨於平緩,當兩個圓距離越小,曲線的波動度越大,這樣,我們想要的粘連的效果就實現了。另外,這裡有一個還有角度(圖中的45度角)可以用來控制,也可以作為控制曲線波動度的引數。 

通過以上分析,我們通過一個方法來繪製兩個圓之間的粘連體路徑:

/**
 * 畫粘連體
 * @param cx1     圓心x1
 * @param cy1     圓心y1
 * @param r1      圓半徑r1
 * @param offset1 貝塞爾曲線偏移角度offset1
 * @param cx2     圓心x2
 * @param cy2     圓心y2
 * @param r2      圓半徑r2
 * @param offset2 貝塞爾曲線偏移角度offset2
 * @return
 */
public static Path drawAdhesionBody(float cx1, float cy1, float r1, float offset1, float 
        cx2, float cy2, float r2, float offset2) {
    
    /* 求三角函式 */
    float degrees =(float) Math.toDegrees(Math.atan(Math.abs(cy2 - cy1) / Math.abs(cx2 - cx1)));
    
    /* 根據圓1與圓2的相對位置求四個點 */
    float differenceX = cx1 - cx2;
    float differenceY = cy1 - cy2;

    /* 兩條貝塞爾曲線的四個端點 */
    float x1,y1,x2,y2,x3,y3,x4,y4;
    
    /* 圓1在圓2的下邊 */
    if (differenceX == 0 && differenceY > 0) {
        x2 = cx2 - r2 * (float) Math.sin(Math.toRadians(offset2));
        y2 = cy2 + r2 * (float) Math.cos(Math.toRadians(offset2));
        x4 = cx2 + r2 * (float) Math.sin(Math.toRadians(offset2));
        y4 = cy2 + r2 * (float) Math.cos(Math.toRadians(offset2));
        x1 = cx1 - r1 * (float) Math.sin(Math.toRadians(offset1));
        y1 = cy1 - r1 * (float) Math.cos(Math.toRadians(offset1));
        x3 = cx1 + r1 * (float) Math.sin(Math.toRadians(offset1));
        y3 = cy1 - r1 * (float) Math.cos(Math.toRadians(offset1));
    }
    /* 圓1在圓2的上邊 */
    else if (differenceX == 0 && differenceY < 0) {
        x2 = cx2 - r2 * (float) Math.sin(Math.toRadians(offset2));
        y2 = cy2 - r2 * (float) Math.cos(Math.toRadians(offset2));
        x4 = cx2 + r2 * (float) Math.sin(Math.toRadians(offset2));
        y4 = cy2 - r2 * (float) Math.cos(Math.toRadians(offset2));
        x1 = cx1 - r1 * (float) Math.sin(Math.toRadians(offset1));
        y1 = cy1 + r1 * (float) Math.cos(Math.toRadians(offset1));
        x3 = cx1 + r1 * (float) Math.sin(Math.toRadians(offset1));
        y3 = cy1 + r1 * (float) Math.cos(Math.toRadians(offset1));
    }
    /* 圓1在圓2的右邊 */
    else if (differenceX > 0 && differenceY == 0) {
        x2 = cx2 + r2 * (float) Math.cos(Math.toRadians(offset2));
        y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(offset2));
        x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(offset2));
        y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(offset2));
        x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(offset1));
        y1 = cy1 + r1 * (float) Math.sin(Math.toRadians(offset1));
        x3 = cx1 - r1 * (float) Math.cos(Math.toRadians(offset1));
        y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(offset1));
    } 
    /* 圓1在圓2的左邊 */
    else if (differenceX < 0 && differenceY == 0 ) {
        x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(offset2));
        y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(offset2));
        x4 = cx2 - r2 * (float) Math.cos(Math.toRadians(offset2));
        y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(offset2));
        x1 = cx1 + r1 * (float) Math.cos(Math.toRadians(offset1));
        y1 = cy1 + r1 * (float) Math.sin(Math.toRadians(offset1));
        x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(offset1));
        y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(offset1));
    }
    /* 圓1在圓2的右下角 */
    else if (differenceX > 0 && differenceY > 0) {
        x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees));
        y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees));
        x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(degrees - offset2));
        y4 = cy2 + r2 * (float) Math.sin(Math.toRadians(degrees - offset2));
        x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(degrees - offset1));
        y1 = cy1 - r1 * (float) Math.sin(Math.toRadians(degrees - offset1));
        x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees));
        y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees));
    }
    /* 圓1在圓2的左上角 */
    else if (differenceX < 0 && differenceY < 0) {
        x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(degrees - offset2));
        y2 = cy2 - r2 * (float) Math.sin(Math.toRadians(degrees - offset2));
        x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees));
        y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees));
        x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees));
        y1 = cy1 + r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees));
        x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(degrees - offset1));
        y3 = cy1 + r1 * (float) Math.sin(Math.toRadians(degrees - offset1));
    }
    /* 圓1在圓2的左下角 */
    else if (differenceX < 0 && differenceY > 0) {
        x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(degrees - offset2));
        y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(degrees - offset2));
        x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees));
        y4 = cy2 + r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees));
        x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees));
        y1 = cy1 - r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees));
        x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(degrees - offset1));
        y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(degrees - offset1));
    }
    /* 圓1在圓2的右上角 */
    else {
        x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees));
        y2 = cy2 - r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees));
        x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(degrees - offset2));
        y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(degrees - offset2));
        x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(degrees - offset1));
        y1 = cy1 + r1* (float) Math.sin(Math.toRadians(degrees - offset1));
        x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees));
        y3 = cy1 + r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees));
    }
    
    /* 貝塞爾曲線的控制點 */
    float anchorX1,anchorY1,anchorX2,anchorY2;
    
    /* 圓1大於圓2 */
    if (r1 > r2) {
        anchorX1 = (x2 + x3) / 2;
        anchorY1 = (y2 + y3) / 2;
        anchorX2 = (x1 + x4) / 2;
        anchorY2 = (y1 + y4) / 2;
    }
    /* 圓1小於或等於圓2 */
    else {
        anchorX1 = (x1 + x4) / 2;
        anchorY1 = (y1 + y4) / 2;
        anchorX2 = (x2 + x3) / 2;
        anchorY2 = (y2 + y3) / 2;
    }
    
    /* 畫粘連體 */
    Path path = new Path();
    path.reset();
    path.moveTo(x1, y1);
    path.quadTo(anchorX1, anchorY1, x2, y2);
    path.lineTo(x4, y4);
    path.quadTo(anchorX2, anchorY2, x3, y3);
    path.lineTo(x1, y1);
    return path;
}

再來看仿QQ聊天列表的粘連效果,我們已經實現了粘連體的繪製,接下來,我們需要實現以上的基本效果,我們給控制元件設定一個粘連的最大距離,即如果兩個圓之間的距離超過這個值,則不再繪製粘連體。

好了,我們看效果圖:

 

粘連體除了在類似QQ上這種效果,其實還可以做很多事,比如,如果我們用它來實現一個載入頁面的效果呢。


 

(3)三階貝塞爾曲線——彈性球

三階貝塞爾曲線,就是有兩個控制點,公式太複雜,我也不是很理解,不過通過之前給出的那個網址來理解,還是比較好明白的,它相比二階曲線的優點是,由於控制點的增加,它能夠更加輕鬆地繪製出更平滑更自然的曲線。

先來看一個web前端的效果:


真的是很酷炫......

如何繪製類似這種,看起來具有彈性球的滑動球,我們需要使用三階貝塞爾曲線,那麼首先如何用三階貝塞爾曲線繪製出一個圓,這裡有一篇文章,是關於如何用貝塞爾曲線繪製圓:http://spencermortensen.com/articles/bezier-circle/ ,大概意思是講,我們需要一個值就是c = 0.552284749,如下圖,要繪製右上角的圓弧,我們需要兩個控制點,其中B就是一個控制點,我們需要保證AB = c *r,即可以畫出1/4的圓弧,以此類推,連續畫四段這樣的圓弧,就可以畫出一個標準的圓。


接下來我們觀察彈性球的運動,大概可以分為以下幾個階段:

 1)開始啟動,此時右邊點位移,其他點不動


2)開始加速


3)減速


4)到達終點


5)回彈效果


彈性球程式碼:

 package com.zero.bezier.widget.elastic;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.graphics.Path;
import android.graphics.PointF;
import android.view.animation.AccelerateDecelerateInterpolator;

/**
 * 彈性球
 * @author linzewu
 * @date 2016/6/1
 */
public class ElasticBall extends Ball {
    /**
     * 向上運動
     */
    private static final int DIRECTION_UP = 1;
    /**
     * 向下運動
     */
    private static final int DIRECTION_DOWN = 2;
    /**
     * 向左運動
     */
    private static final int DIRECTION_LEFT = 3;
    /**
     * 向右運動
     */
    private static final int DIRECTION_RIGHT = 4;
    /**
     * 運動方向
     */
    private int mDirection;
    /**
     * 動畫完成百分比(0~1)
     */
    private float mAnimPercent;
    /**
     * 彈性距離
     */
    private float mElasticDistance;
    /**
     * 彈性比例
     */
    private float mElasticPercent = 0.8f;
    /**
     * 位移距離
     */
    private float mMoveDistance;
    /**
     * 動畫消費時間
     */
    private long mDuration = 1500;
    
    /**
     * 偏移值
     */
    private float offsetTop, offsetBottom, offsetLeft, offsetRight;
    /**
     * 圓形偏移比例
     */
    private float c = 0.551915024494f;
    
    private float c2 = 0.65f;
    /**
     * 動畫開始點
     */
    private Ball mStartPoint;

    /**
     * 動畫結束點
     */
    private Ball mEndPoint;
    
    /**
     * 構造方法
     *
     * @param x 圓心橫座標
     * @param y 圓心縱座標
     * @param radius 圓半徑
     */
    public ElasticBall(float x, float y, float radius) {
        super(x, y, radius);
        init();
    }
    
    
    private void init() {
        mElasticDistance = mElasticPercent * radius;
        offsetTop = c * radius;
        offsetBottom = c * radius;
        offsetLeft = c * radius;
        offsetRight = c * radius;
    }
    
    public interface ElasticBallInterface{
        void onChange(Path path);
        void onFinish();
    }

    private ElasticBallInterface mElasticBallInterface;

    /**
     * 對外公佈方法,設定彈性比例 (0~1)
     * @param elasticPercent
     */
    public void setElasticPercent(float elasticPercent) {
        
    }
    /**
     * 對外公佈方法,設定動畫時間
     * @param duration
     */
    public void setDuration(long duration) {
        this.mDuration = duration;
    }
    
    /**
     * 對外公佈方法, 開啟動畫
     * @param endPoint
     */
    public void startElasticAnim(PointF endPoint, ElasticBallInterface elasticBallInterface) {
        this.mEndPoint = new ElasticBall(endPoint.x, endPoint.y, radius);
        this.mStartPoint = new ElasticBall(x, y, radius);
        this.mStatusPoint1 = new ElasticBall(x, y, radius);
        this.mStatusPoint2 = new ElasticBall(x, y, radius);
        this.mStatusPoint3 = new ElasticBall(x, y, radius);
        this.mStatusPoint4 = new ElasticBall(x, y, radius);
        this.mStatusPoint5 = new ElasticBall(x, y, radius);
        this.mElasticBallInterface = elasticBallInterface;
        judgeDirection();
        mMoveDistance = getDistance(mStartPoint.x, mStatusPoint1.y, endPoint.x, endPoint.y);
        animStatus0();
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
        valueAnimator.setDuration(mDuration);
        valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        valueAnimator.start();
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mAnimPercent = (float) animation.getAnimatedValue();
                if(mAnimPercent>=0 && mAnimPercent <= 0.2){
                    animStatus1();
                }
                else if(mAnimPercent > 0.2 && mAnimPercent <= 0.5){
                    animStatus2();
                }
                else if(mAnimPercent > 0.5 && mAnimPercent <= 0.8){
                    animStatus3();
                }
                else if(mAnimPercent > 0.8 && mAnimPercent <= 0.9){
                    animStatus4();
                }
                else if(mAnimPercent > 0.9&&mAnimPercent <= 1){
                    animStatus5();
                }
                if (mElasticBallInterface != null) {
                    mElasticBallInterface.onChange(drawElasticCircle(topX, topY, offsetTop, offsetTop,
                            bottomX, bottomY, offsetBottom, offsetBottom,
                            leftX, leftY, offsetLeft, offsetLeft,
                            rightX, rightY, offsetRight, offsetRight));
                }
            }
        });
        valueAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (mElasticBallInterface != null) {
                    mElasticBallInterface.onFinish();
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
    }

    private void judgeDirection() {
        if (mEndPoint.x - mStartPoint.x > 0) {
            mDirection = DIRECTION_RIGHT;
        }else if (mEndPoint.x - mStartPoint.x < 0) {
            mDirection = DIRECTION_LEFT;
        }else if (mEndPoint.y - mStartPoint.x > 0) {
            mDirection = DIRECTION_DOWN;
        }else if (mEndPoint.y - mStartPoint.y < 0){
            mDirection = DIRECTION_UP;
        }
    }
    
    /**
     * 動畫狀態0 (初始狀態:圓形)
     */
    private void animStatus0() {
        offsetTop = c * radius;
        offsetBottom = c * radius;
        offsetLeft = c * radius;
        offsetRight = c * radius;
    }
    
    private Ball mStatusPoint1;
    
    /**
     * 動畫狀態1 (0~0.2)
     */
    private void animStatus1() {
        float percent = mAnimPercent * 5f;
        if (mDirection == DIRECTION_LEFT) {
            leftX = mStartPoint.leftX - percent * mElasticDistance;
        } else if (mDirection == DIRECTION_RIGHT) {
            rightX = mStartPoint.rightX + percent * mElasticDistance;
        } else if (mDirection == DIRECTION_UP) {
            topY = mStartPoint.topY - percent * mElasticDistance;
        } else if (mDirection == DIRECTION_DOWN) {
            bottomY = mStartPoint.bottomY + percent * mElasticDistance;
        }
        mStatusPoint1.refresh(x, y, topX, topY, bottomX, bottomY,
                leftX, leftY, rightX, rightY);
    }

    private Ball mStatusPoint2;
    
    /**
     * 動畫狀態2 (0.2~0.5)
     */
    private void animStatus2() {
        float percent = (float) ((mAnimPercent - 0.2) * (10f / 3));
        if (mDirection == DIRECTION_LEFT) {
            leftX = mStatusPoint1.leftX - percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            x = mStatusPoint1.x - percent * (mMoveDistance / 2);
            rightX = mStatusPoint1.rightX - percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            topX = x;
            bottomX = x;
            //偏移值稍作變化
            offsetTop = radius * c + radius * ( c2 - c ) * percent;
            offsetBottom = radius * c + radius * ( c2 - c ) * percent;
        } else if (mDirection == DIRECTION_RIGHT) {
            rightX = mStatusPoint1.rightX + percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            x = mStatusPoint1.x + percent * (mMoveDistance / 2);
            leftX = mStatusPoint1.leftX + percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            topX = x;
            bottomX = x;
            //偏移值稍作變化
            offsetTop = radius * c + radius * ( c2 - c ) * percent;
            offsetBottom = radius * c + radius * ( c2 - c ) * percent;
        } else if (mDirection == DIRECTION_UP) {
            topY = mStatusPoint1.topY - percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            y = mStatusPoint1.y - percent * (mMoveDistance / 2);
            bottomY = mStatusPoint1.bottomY - percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            leftY = y;
            rightY = y;
            //偏移值稍作變化
            offsetLeft = radius * c + radius * ( c2 - c ) * percent;
            offsetRight = radius * c + radius * ( c2 - c ) * percent;
        } else if (mDirection == DIRECTION_DOWN) {
            bottomY = mStatusPoint1.bottomY + percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            y = mStatusPoint1.y + percent * (mMoveDistance / 2);
            topY = mStatusPoint1.topY + percent * (mMoveDistance / 2 - mElasticDistance / 2 );
            leftY = y;
            rightY = y;
            //偏移值稍作變化
            offsetLeft = radius * c + radius * ( c2 - c ) * percent;
            offsetRight = radius * c + radius * ( c2 - c ) * percent;
        }
        mStatusPoint2.refresh(x, y, topX, topY, bottomX, bottomY,
                leftX, leftY, rightX, rightY);
    }

    private Ball mStatusPoint3;
    
    /**
     * 動畫狀態3 (0.5~0.8)
     */
    private void animStatus3() {
        float percent = (mAnimPercent - 0.5f) * (10f / 3f);
        if (mDirection == DIRECTION_LEFT) {
            leftX = mStatusPoint2.leftX - Math.abs(percent * (mEndPoint.rightX - mStatusPoint2
                    .rightX));
            x = mStatusPoint2.x - Math.abs(percent * (mEndPoint.x - mStatusPoint2.x));
            rightX = mStatusPoint2.rightX - Math.abs(percent * (mEndPoint.x - mStatusPoint2.x));
            topX = x;
            bottomX = x;
            //偏移值稍作變化
            offsetTop = radius * c2 - radius * ( c2 - c ) * percent;
            offsetBottom = radius * c2 - radius * ( c2 - c ) * percent;
        } else if (mDirection == DIRECTION_RIGHT) {
            rightX = mStatusPoint2.rightX + percent * (mEndPoint.rightX - mStatusPoint2.rightX);
            x = mStatusPoint2.x + percent * (mEndPoint.x - mStatusPoint2.x);
            leftX = mStatusPoint2.leftX + percent * (mEndPoint.x - mStatusPoint2.x);
            topX = x;
            bottomX = x;
            //偏移值稍作變化
            offsetTop = radius * c2 - radius * ( c2 - c ) * percent;
            offsetBottom = radius * c2 - radius * ( c2 - c ) * percent;
        } else if (mDirection == DIRECTION_UP) {
            topY = mStatusPoint2.topY - Math.abs(percent * (mEndPoint.topY - mStatusPoint2
                    .topY));
            y = mStatusPoint2.y - Math.abs(percent * (mEndPoint.y - mStatusPoint2.y));
            bottomY = mStatusPoint2.bottomY - Math.abs(percent * (mEndPoint.y - mStatusPoint2.y));
            leftY = y;
            rightY = y;
            //偏移值稍作變化
            offsetLeft = radius * c2 - radius * ( c2 - c ) * percent;
            offsetRight = radius * c2 - radius * ( c2 - c ) * percent;
        } else if (mDirection == DIRECTION_DOWN) {
            bottomY = mStatusPoint2.bottomY + percent * (mEndPoint.bottomY - mStatusPoint2
                    .bottomY);
            y = mStatusPoint2.y + percent * (mEndPoint.y - mStatusPoint2.y);
            topY = mStatusPoint2.topY + percent * (mEndPoint.y - mStatusPoint2.y);
            leftY = y;
            rightY = y;
            //偏移值稍作變化
            offsetLeft = radius * c2 - radius * ( c2 - c ) * percent;
            offsetRight = radius * c2 - radius * ( c2 - c ) * percent;
        }
        mStatusPoint3.refresh(x, y, topX, topY, bottomX, bottomY,
                leftX, leftY, rightX, rightY);
    }

    private Ball mStatusPoint4;
    
    /**
     * 動畫狀態4 (0.8~0.9)
     */
    private void animStatus4() {
        float percent = (float) (mAnimPercent - 0.8) * 10;
        if (mDirection == DIRECTION_LEFT) {
            rightX = mStatusPoint3.rightX - percent * (Math.abs(mEndPoint.rightX - mStatusPoint3
                    .rightX) + mElasticDistance/2);
            //再做一次賦值,防止和終點不重合
            leftX = mEndPoint.leftX;
            x = mEndPoint.x;
            bottomX = mEndPoint.bottomX;
            topX = mEndPoint.topX;
        } else if (mDirection == DIRECTION_RIGHT) {
            leftX = mStatusPoint3.leftX + percent * (mEndPoint.leftX - mStatusPoint3.leftX +
                    mElasticDistance/2);
            //再做一次賦值,防止和終點不重合
            rightX = mEndPoint.rightX;
            x = mEndPoint.x;
            bottomX = mEndPoint.bottomX;
            topX = mEndPoint.topX;
        } else if (mDirection == DIRECTION_UP) {
            bottomY = mStatusPoint3.bottomY - percent * (Math.abs(mEndPoint.bottomY - mStatusPoint3
                    .bottomY) + mElasticDistance/2);
            //再做一次賦值,防止和終點不重合
            topY = mEndPoint.topY;
            y = mEndPoint.y;
            leftY = mEndPoint.leftY;
            rightY = mEndPoint.rightY;
        } else if (mDirection == DIRECTION_DOWN) {
            topY = mStatusPoint3.topY + percent * (mEndPoint.topY - mStatusPoint3
                    .topY + mElasticDistance/2);
            //再做一次賦值,防止和終點不重合
            bottomY = mEndPoint.bottomY;
            y = mEndPoint.y;
            leftY = mEndPoint.leftY;
            rightY = mEndPoint.rightY;
        }
        mStatusPoint4.refresh(x, y, topX, topY, bottomX, bottomY,
                leftX, leftY, rightX, rightY);
    }

    private Ball mStatusPoint5;
    
    /**
     * 動畫狀態5 (0.9~1)回彈
     */
    private void animStatus5() {
        float percent = (float) (mAnimPercent - 0.9) * 10;
        if (mDirection == DIRECTION_LEFT) {
            rightX = mStatusPoint4.rightX + percent * (mEndPoint.rightX - mStatusPoint4.rightX);
        } else if (mDirection == DIRECTION_RIGHT) {
            leftX = mStatusPoint4.leftX + percent * (mEndPoint.leftX - mStatusPoint4.leftX);
        } else if (mDirection == DIRECTION_UP) {
            bottomY = mStatusPoint4.bottomY + percent * (mEndPoint.bottomY - mStatusPoint4.bottomY);
        } else if (mDirection == DIRECTION_DOWN) {
            topY = mStatusPoint4.topY + percent * (mEndPoint.topY - mStatusPoint4.topY);
        }
        mStatusPoint5.refresh(x, y, topX, topY, bottomX, bottomY,
                leftX, leftY, rightX, rightY);
    }

    /**
     * 繪製彈性圓
     * 通過繪製四段三階貝塞爾曲線,來實現有彈性變化的圓
     * @param topX
     * @param topY
     * @param offsetTop1
     * @param offsetTop2
     * @param bottomX
     * @param bottomY
     * @param offsetBottom1
     * @param offsetBottom2
     * @param leftX
     * @param leftY
     * @param offsetLeft1
     * @param offsetLeft2
     * @param rightX
     * @param rightY
     * @param offsetRight1
     * @param offsetRight2
     * @return
     */
    private Path drawElasticCircle(
            float topX, float topY, float offsetTop1, float offsetTop2,
            float bottomX, float bottomY, float offsetBottom1, float offsetBottom2,
            float leftX, float leftY, float offsetLeft1, float offsetLeft2,
            float rightX, float rightY, float offsetRight1, float offsetRight2
    ) {
        /**
         * 繪製每一段三階貝塞爾曲線需要兩個控制點
         */
        PointF controlTop1, controlTop2, controlBottom1, controlBottom2,
                controlLeft1, controlLeft2, controlRight1, controlRight2;
        controlTop1 = new PointF();
        controlTop1.x = topX - offsetTop1;
        controlTop1.y = topY;
        controlTop2 = new PointF();
        controlTop2.x = topX + offsetTop2;
        controlTop2.y = topY;
        controlBottom1 = new PointF();
        controlBottom1.x = bottomX - offsetBottom1;
        controlBottom1.y = bottomY;
        controlBottom2 = new PointF();
        controlBottom2.x = bottomX + offsetBottom2;
        controlBottom2.y = bottomY;
        controlLeft1 = new PointF();
        controlLeft1.x = leftX;
        controlLeft1.y = leftY - offsetLeft1;
        controlLeft2 = new PointF();
        controlLeft2.x = leftX;
        controlLeft2.y = leftY + offsetLeft2;
        controlRight1 = new PointF();
        controlRight1.x = rightX;
        controlRight1.y = rightY - offsetRight1;
        controlRight2 = new PointF();
        controlRight2.x = rightX;
        controlRight2.y = rightY + offsetRight2;

        Path path = new Path();
        /**
         * 繪製top到left的圓弧
         */
        path.moveTo(topX, topY);
        path.cubicTo(controlTop1.x, controlTop1.y, controlLeft1.x, controlLeft1.y, leftX, leftY);
        /**
         * 繪製left到bottom的圓弧
         */
        path.cubicTo(controlLeft2.x ,controlLeft2.y, controlBottom1.x, controlBottom1.y, bottomX,
                bottomY);
        /**
         * 繪製bottom到right的圓弧
         */
        path.cubicTo(controlBottom2.x, controlBottom2.y, controlRight2.x, controlRight2.y,
                rightX, rightY);
        /**
         * 繪製right到top的圓弧
         */
        path.cubicTo(controlRight1.x, controlRight1.y, controlTop2.x, controlTop2.y, topX, topY);
        return path;
    }

    /**
     * 求兩點之間的距離
     * @param x1 第一個點的橫座標
     * @param y1 第一個點的縱座標
     * @param x2 第二個點的橫座標
     * @param y2 第二個點的縱座標
     * @return 兩點距離
     */
    private float getDistance(float x1, float y1, float x2, float y2) {
        return (float) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
    }
    
}

上面完成了一個彈性球的封裝,可以實現四個方向的運動,然後我們實現一個彈性球的loader:


貝塞爾曲線還有很多應用的地方,或者說在各個領域都有。

去年開始在黃同學的影響下,慢慢地去實現一些利用貝塞爾曲線實現的效果,原始碼有相當一部分程式碼也是來自於黃同學,非常感謝黃,也得益於網路上大多數技術部落格無私的分享,希望自己能夠通過學習這樣一個開發的繪圖曲線,有所提高。

參考:

專案原始碼下載:

1)Github 

https://github.com/82367825/BezierMaster

2)CSDN下載頻道

 如果GitHub訪問不了,也可以到CSDN下載頻道,原始碼工程  

相關推薦

Android曲線應用-跳動的水滴

dir 貝塞爾曲線 href 完成 通過 android load 繪制 canvas 主要通過6個控制點實現。 val startPoint = PointF() val endPoint = PointF() val control1 = PointF() val c

Android曲線實現水波紋的效果

前兩天朋友找我實現一個水波紋的效果,因為這塊一直沒做過,所以花了一上午時間研究一下,參考了網上的一些方法,得知Android還有Path.quadTo()這麼一個方法。 話不多說,程式碼如下: public class MyView extends View implem

Android-曲線

從去年開始瞭解貝塞爾曲線之後,發現開發中,不管是Android/Ios平臺,還是web前端等,都有貝塞爾曲線的應用,通過繪製貝塞爾曲線,可以幫助開發者實現很多效果,例如一段時間內很流行的粘合型的下拉重新整理、又如天氣曲線圖,同時,以貝塞爾曲線為基礎的貝塞爾工具是所有繪圖軟

Android曲線 二階的簡單處理

二階效果圖 控制點只有一個 private float mStartPointX; private float mStartPointY; private float mEndPointX; private fl

Android曲線-水波篇

在做自定義view時,很多時候會用到貝塞爾曲線這個東西去實現一些效果,像以前寫的那個仿直播點贊動畫的實現就是用到了貝塞爾曲線,這次說的水波也會用到貝塞爾曲線這個東西。 Android貝塞爾曲線api 首先看下貝塞爾曲線公式: 一階 二階 其

Android 曲線,撒花了

撒花了 佈局 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/re

Android曲線————波浪效果(大波浪)

Hello大家好,很高興又一次與大家見面,今天是農曆丁酉雞年(大年初四),現在跟大家拜年有點晚,算是拜晚年,祝大家晚年幸福。 這麼快大夥都到了晚年了,Android貝塞爾曲線我也準備以一個大波浪來結束。所以今天給大家帶來的就是android貝塞爾曲線製作波浪效果

Android 曲線實踐——波浪式運動

開發十年,就只剩下這套架構體系了! >>>   

Android開源專案解析】QQ“一鍵下班”功能實現解析——學習Path及曲線的基本使用

早在很久很久以前,QQ就實現了“一鍵下班”功能。何為“一鍵下班”?當你QQ有資訊時,下部會有資訊數量提示紅點,點選拖動之後,就會出現“一鍵下班”效果。本文將結合github上關於此功能的一個簡單實現,介紹這個功能的基本實現思路。 專案地址

android流式佈局、待辦事項應用、曲線、MVP+Rxjava+Retrofit、藝術圖片應用等原始碼

Android精選原始碼 android模仿淘寶首頁效果原始碼 一款藝術圖片應用,採用T-MVVM打造 Android MVP + RxJava + Retrofit專案 android流式佈局實現熱門標籤效果 android仿淘寶客戶端商品詳

2014-11-6Android學習------Android 模擬翻頁效果實現--------曲線(二)

寫一篇文章很辛苦啊!!! 轉載請註明,聯絡請郵件[email protected] 我學習Android都是結合原始碼去學習,這樣比較直觀,非常清楚的看清效果,覺得很好,今天的學習原始碼是網上找的原始碼 百度搜就知道很多下載的地方  網上原始碼的名字叫:A

Android自定義View——曲線實現水波紋進度球

效果圖 原理分析 首先需要了解的水波紋實現效果,可以在部落格的自定義View專題找到,其實現原理如下 利用貝塞爾曲線繪製螢幕外和螢幕內的sin曲線 利用path將sin曲線的左下角和右下角連線起來成為一塊區域 通過不斷的平移sin曲線,然後平移完

Android 控制元件沿曲線運動(中)

看了Android貝塞爾曲線屬性動畫(上)是不是在罵我SB,換個貝塞爾曲線的起始點,控制點,終點,控制元件還是按原來路徑運動,So.... 下面我實現了控制元件在隨機的一個貝塞爾曲線上的運動 package com.example.propertyanimsecdemo;

Android開發之Path的高階用法用曲線繪製波浪線

前言:貝塞爾曲線分為一級曲線,二級曲線,三級曲線和多級曲線,利用貝塞爾曲線可以做出很多有意思的動畫和圖形,今天我們就來實現一個比較簡單的波浪線。 -----------------分割線--------------- 初步認識貝塞爾曲線: mPath.moveTo:設定起點

Android自定義View進階 - 曲線

                Path之貝塞爾曲線 作者微博: @GcsSloop 【本系列相關文章】 在

Android自定義View阻尼動畫&曲線的實現

效果圖:直接上程式碼啦:package com.example.administrator.myapplication.customview; import android.animation.Animator; import android.animation.Anim

Android仿蘋果版QQ下拉重新整理實現(二) ——曲線開發"鼻涕"下拉粘連效果

前言 下面上一下本章需要實現的效果圖: 大家看到這個效果肯定不會覺得陌生,QQ已經把粘滯效果做的滿大街都是,相信不少讀者或多或少對於貝塞爾曲線有所瞭解,不瞭解的朋友們也沒有關係,在這裡我會帶領讀者領略一下貝塞爾的魅力! 一、關於貝塞爾曲線 我們知道

Android 繪圖基礎:Path(繪製三角形、曲線、正餘弦)

學習重點: 理解path的使用 理解貝塞爾曲線的繪製原理 可動正餘弦的繪製 Path的簡單介紹   在 Android 繪圖基礎:Canvas畫布——自定義View(繪製錶盤、矩形、圓形、弧、漸變) 中我們可以看到Canvas的強大功能,其實Canva

Android 自定義View高階特效,神奇的曲線

初始化引數 private static final String TAG = "BIZIER"; private static final int LINEWIDTH = 5; private static final int POINTWIDTH = 10; private Context mContex

Android開發——曲線解析

相信很多同學都知道“貝塞爾曲線”這個詞,我們在很多地方都能經常看到。利用“貝塞爾曲線”可以做出很多好看的UI效果,本篇部落格就讓我們一起學習“貝塞爾曲線”。貝塞爾曲線的原理貝塞爾曲線是用一系列點來控制曲線狀態的,這些點簡單分為兩類:型別作用資料點確定曲線的起始和結束位置控制點確定曲線的彎曲程度一階貝塞爾曲線