天天看點

安卓繪制貝塞爾曲線

從去年開始了解貝塞爾曲線之後,發現開發中,不管是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

這個工具網站,可以幫助我們去繪制貝塞爾曲線 http://bezier.method.ac/#

貝塞爾曲線代碼實作:

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

[java]

view plain copy print ?

  1. public void quadTo(float x1, float y1, float x2, float y2) {  
  2.     isSimplePath = false;  
  3.     native_quadTo(mNativePath, x1, y1, x2, y2);  
  4. }  
/**
 * 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)的貝塞爾曲線。是以,通過不斷改變這三個點的位置,我們可以繪制出各種各樣的曲線

[java]

view plain copy print ?

  1. public void cubicTo(float x1, float y1, float x2, float y2,  
  2.                     float x3, float y3) {  
  3.     isSimplePath = false;  
  4.     native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3);  
  5. }  
/**
 * 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() 代碼:

[java]

view plain copy print ?

  1. @Override  
  2. protected void onDraw(Canvas canvas) {  
  3.     super.onDraw(canvas);  
  4.     if (!mIsRunning || !mHasInit)  
  5.         return;  
  6.     mPath.reset();  
  7.     mPath.moveTo(mLeft1.x, mLeft1.y);  
  8.     mPath.quadTo(mControlLeft1.x, mControlLeft1.y, mLeft2.x, mLeft2.y);  
  9.     mPath.quadTo(mControlLeft2.x, mControlLeft2.y, mFirst.x, mFirst.y);  
  10.     mPath.quadTo(mControlFirst.x, mControlFirst.y, mSecond.x, mSecond.y);  
  11.     mPath.quadTo(mControlSecond.x, mControlSecond.y, mRight.x, mRight.y);  
  12.     mPath.lineTo(mRight.x, mHeight);  
  13.     mPath.lineTo(mLeft1.x, mHeight);  
  14.     mPath.lineTo(mLeft1.x, mLeft1.y);  
  15.     canvas.drawPath(mPath, mPaint);  
  16. }  
@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度角)可以用來控制,也可以作為控制曲線波動度的參數。 

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

[java]

view plain copy print ?

  1. public static Path drawAdhesionBody(float cx1, float cy1, float r1, float offset1, float   
  2.         cx2, float cy2, float r2, float offset2) {  
  3.     float degrees =(float) Math.toDegrees(Math.atan(Math.abs(cy2 - cy1) / Math.abs(cx2 - cx1)));  
  4.     float differenceX = cx1 - cx2;  
  5.     float differenceY = cy1 - cy2;  
  6.     float x1,y1,x2,y2,x3,y3,x4,y4;  
  7.     if (differenceX == 0 && differenceY > 0) {  
  8.         x2 = cx2 - r2 * (float) Math.sin(Math.toRadians(offset2));  
  9.         y2 = cy2 + r2 * (float) Math.cos(Math.toRadians(offset2));  
  10.         x4 = cx2 + r2 * (float) Math.sin(Math.toRadians(offset2));  
  11.         y4 = cy2 + r2 * (float) Math.cos(Math.toRadians(offset2));  
  12.         x1 = cx1 - r1 * (float) Math.sin(Math.toRadians(offset1));  
  13.         y1 = cy1 - r1 * (float) Math.cos(Math.toRadians(offset1));  
  14.         x3 = cx1 + r1 * (float) Math.sin(Math.toRadians(offset1));  
  15.         y3 = cy1 - r1 * (float) Math.cos(Math.toRadians(offset1));  
  16.     }  
  17.     else if (differenceX == 0 && differenceY < 0) {  
  18.         x2 = cx2 - r2 * (float) Math.sin(Math.toRadians(offset2));  
  19.         y2 = cy2 - r2 * (float) Math.cos(Math.toRadians(offset2));  
  20.         x4 = cx2 + r2 * (float) Math.sin(Math.toRadians(offset2));  
  21.         y4 = cy2 - r2 * (float) Math.cos(Math.toRadians(offset2));  
  22.         x1 = cx1 - r1 * (float) Math.sin(Math.toRadians(offset1));  
  23.         y1 = cy1 + r1 * (float) Math.cos(Math.toRadians(offset1));  
  24.         x3 = cx1 + r1 * (float) Math.sin(Math.toRadians(offset1));  
  25.         y3 = cy1 + r1 * (float) Math.cos(Math.toRadians(offset1));  
  26.     }  
  27.     else if (differenceX > 0 && differenceY == 0) {  
  28.         x2 = cx2 + r2 * (float) Math.cos(Math.toRadians(offset2));  
  29.         y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(offset2));  
  30.         x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(offset2));  
  31.         y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(offset2));  
  32.         x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(offset1));  
  33.         y1 = cy1 + r1 * (float) Math.sin(Math.toRadians(offset1));  
  34.         x3 = cx1 - r1 * (float) Math.cos(Math.toRadians(offset1));  
  35.         y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(offset1));  
  36.     }   
  37.     else if (differenceX < 0 && differenceY == 0 ) {  
  38.         x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(offset2));  
  39.         y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(offset2));  
  40.         x4 = cx2 - r2 * (float) Math.cos(Math.toRadians(offset2));  
  41.         y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(offset2));  
  42.         x1 = cx1 + r1 * (float) Math.cos(Math.toRadians(offset1));  
  43.         y1 = cy1 + r1 * (float) Math.sin(Math.toRadians(offset1));  
  44.         x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(offset1));  
  45.         y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(offset1));  
  46.     }  
  47.     else if (differenceX > 0 && differenceY > 0) {  
  48.         x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees));  
  49.         y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees));  
  50.         x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(degrees - offset2));  
  51.         y4 = cy2 + r2 * (float) Math.sin(Math.toRadians(degrees - offset2));  
  52.         x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(degrees - offset1));  
  53.         y1 = cy1 - r1 * (float) Math.sin(Math.toRadians(degrees - offset1));  
  54.         x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees));  
  55.         y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees));  
  56.     }  
  57.     else if (differenceX < 0 && differenceY < 0) {  
  58.         x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(degrees - offset2));  
  59.         y2 = cy2 - r2 * (float) Math.sin(Math.toRadians(degrees - offset2));  
  60.         x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees));  
  61.         y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees));  
  62.         x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees));  
  63.         y1 = cy1 + r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees));  
  64.         x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(degrees - offset1));  
  65.         y3 = cy1 + r1 * (float) Math.sin(Math.toRadians(degrees - offset1));  
  66.     }  
  67.     else if (differenceX < 0 && differenceY > 0) {  
  68.         x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(degrees - offset2));  
  69.         y2 = cy2 + r2 * (float) Math.sin(Math.toRadians(degrees - offset2));  
  70.         x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees));  
  71.         y4 = cy2 + r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees));  
  72.         x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees));  
  73.         y1 = cy1 - r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees));  
  74.         x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(degrees - offset1));  
  75.         y3 = cy1 - r1 * (float) Math.sin(Math.toRadians(degrees - offset1));  
  76.     }  
  77.     else {  
  78.         x2 = cx2 - r2 * (float) Math.cos(Math.toRadians(180 - offset2 - degrees));  
  79.         y2 = cy2 - r2 * (float) Math.sin(Math.toRadians(180 - offset2 - degrees));  
  80.         x4 = cx2 + r2 * (float) Math.cos(Math.toRadians(degrees - offset2));  
  81.         y4 = cy2 - r2 * (float) Math.sin(Math.toRadians(degrees - offset2));  
  82.         x1 = cx1 - r1 * (float) Math.cos(Math.toRadians(degrees - offset1));  
  83.         y1 = cy1 + r1* (float) Math.sin(Math.toRadians(degrees - offset1));  
  84.         x3 = cx1 + r1 * (float) Math.cos(Math.toRadians(180 - offset1 - degrees));  
  85.         y3 = cy1 + r1 * (float) Math.sin(Math.toRadians(180 - offset1 - degrees));  
  86.     }  
  87.     float anchorX1,anchorY1,anchorX2,anchorY2;  
  88.     if (r1 > r2) {  
  89.         anchorX1 = (x2 + x3) / 2;  
  90.         anchorY1 = (y2 + y3) / 2;  
  91.         anchorX2 = (x1 + x4) / 2;  
  92.         anchorY2 = (y1 + y4) / 2;  
  93.     }  
  94.     else {  
  95.         anchorX1 = (x1 + x4) / 2;  
  96.         anchorY1 = (y1 + y4) / 2;  
  97.         anchorX2 = (x2 + x3) / 2;  
  98.         anchorY2 = (y2 + y3) / 2;  
  99.     }  
  100.     Path path = new Path();  
  101.     path.reset();  
  102.     path.moveTo(x1, y1);  
  103.     path.quadTo(anchorX1, anchorY1, x2, y2);  
  104.     path.lineTo(x4, y4);  
  105.     path.quadTo(anchorX2, anchorY2, x3, y3);  
  106.     path.lineTo(x1, y1);  
  107.     return path;  
  108. }  
/**
 * 畫粘連體
 * @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)回彈效果

安卓繪制貝塞爾曲線

彈性球代碼:

[java]

view plain copy print ?

  1.  package com.zero.bezier.widget.elastic;  
  2. import android.animation.Animator;  
  3. import android.animation.ValueAnimator;  
  4. import android.graphics.Path;  
  5. import android.graphics.PointF;  
  6. import android.view.animation.AccelerateDecelerateInterpolator;  
  7. public class ElasticBall extends Ball {  
  8.     private static final int DIRECTION_UP = 1;  
  9.     private static final int DIRECTION_DOWN = 2;  
  10.     private static final int DIRECTION_LEFT = 3;  
  11.     private static final int DIRECTION_RIGHT = 4;  
  12.     private int mDirection;  
  13.     private float mAnimPercent;  
  14.     private float mElasticDistance;  
  15.     private float mElasticPercent = 0.8f;  
  16.     private float mMoveDistance;  
  17.     private long mDuration = 1500;  
  18.     private float offsetTop, offsetBottom, offsetLeft, offsetRight;  
  19.     private float c = 0.551915024494f;  
  20.     private float c2 = 0.65f;  
  21.     private Ball mStartPoint;  
  22.     private Ball mEndPoint;  
  23.     public ElasticBall(float x, float y, float radius) {  
  24.         super(x, y, radius);  
  25.         init();  
  26.     }  
  27.     private void init() {  
  28.         mElasticDistance = mElasticPercent * radius;  
  29.         offsetTop = c * radius;  
  30.         offsetBottom = c * radius;  
  31.         offsetLeft = c * radius;  
  32.         offsetRight = c * radius;  
  33.     }  
  34.     public interface ElasticBallInterface{  
  35.         void onChange(Path path);  
  36.         void onFinish();  
  37.     }  
  38.     private ElasticBallInterface mElasticBallInterface;  
  39.     public void setElasticPercent(float elasticPercent) {  
  40.     }  
  41.     public void setDuration(long duration) {  
  42.         this.mDuration = duration;  
  43.     }  
  44.     public void startElasticAnim(PointF endPoint, ElasticBallInterface elasticBallInterface) {  
  45.         this.mEndPoint = new ElasticBall(endPoint.x, endPoint.y, radius);  
  46.         this.mStartPoint = new ElasticBall(x, y, radius);  
  47.         this.mStatusPoint1 = new ElasticBall(x, y, radius);  
  48.         this.mStatusPoint2 = new ElasticBall(x, y, radius);  
  49.         this.mStatusPoint3 = new ElasticBall(x, y, radius);  
  50.         this.mStatusPoint4 = new ElasticBall(x, y, radius);  
  51.         this.mStatusPoint5 = new ElasticBall(x, y, radius);  
  52.         this.mElasticBallInterface = elasticBallInterface;  
  53.         judgeDirection();  
  54.         mMoveDistance = getDistance(mStartPoint.x, mStatusPoint1.y, endPoint.x, endPoint.y);  
  55.         animStatus0();  
  56.         ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);  
  57.         valueAnimator.setDuration(mDuration);  
  58.         valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());  
  59.         valueAnimator.start();  
  60.         valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  
  61.             @Override  
  62.             public void onAnimationUpdate(ValueAnimator animation) {  
  63.                 mAnimPercent = (float) animation.getAnimatedValue();  
  64.                 if(mAnimPercent>=0 && mAnimPercent <= 0.2){  
  65.                     animStatus1();  
  66.                 }  
  67.                 else if(mAnimPercent > 0.2 && mAnimPercent <= 0.5){  
  68.                     animStatus2();  
  69.                 }  
  70.                 else if(mAnimPercent > 0.5 && mAnimPercent <= 0.8){  
  71.                     animStatus3();  
  72.                 }  
  73.                 else if(mAnimPercent > 0.8 && mAnimPercent <= 0.9){  
  74.                     animStatus4();  
  75.                 }  
  76.                 else if(mAnimPercent > 0.9&&mAnimPercent <= 1){  
  77.                     animStatus5();  
  78.                 }  
  79.                 if (mElasticBallInterface != null) {  
  80.                     mElasticBallInterface.onChange(drawElasticCircle(topX, topY, offsetTop, offsetTop,  
  81.                             bottomX, bottomY, offsetBottom, offsetBottom,  
  82.                             leftX, leftY, offsetLeft, offsetLeft,  
  83.                             rightX, rightY, offsetRight, offsetRight));  
  84.                 }  
  85.             }  
  86.         });  
  87.         valueAnimator.addListener(new Animator.AnimatorListener() {  
  88.             @Override  
  89.             public void onAnimationStart(Animator animation) {  
  90.             }  
  91.             @Override  
  92.             public void onAnimationEnd(Animator animation) {  
  93.                 if (mElasticBallInterface != null) {  
  94.                     mElasticBallInterface.onFinish();  
  95.                 }  
  96.             }  
  97.             @Override  
  98.             public void onAnimationCancel(Animator animation) {  
  99.             }  
  100.             @Override  
  101.             public void onAnimationRepeat(Animator animation) {  
  102.             }  
  103.         });  
  104.     }  
  105.     private void judgeDirection() {  
  106.         if (mEndPoint.x - mStartPoint.x > 0) {  
  107.             mDirection = DIRECTION_RIGHT;  
  108.         }else if (mEndPoint.x - mStartPoint.x < 0) {  
  109.             mDirection = DIRECTION_LEFT;  
  110.         }else if (mEndPoint.y - mStartPoint.x > 0) {  
  111.             mDirection = DIRECTION_DOWN;  
  112.         }else if (mEndPoint.y - mStartPoint.y < 0){  
  113.             mDirection = DIRECTION_UP;  
  114.         }  
  115.     }  
  116.     private void animStatus0() {  
  117.         offsetTop = c * radius;  
  118.         offsetBottom = c * radius;  
  119.         offsetLeft = c * radius;  
  120.         offsetRight = c * radius;  
  121.     }  
  122.     private Ball mStatusPoint1;  
  123.     private void animStatus1() {  
  124.         float percent = mAnimPercent * 5f;  
  125.         if (mDirection == DIRECTION_LEFT) {  
  126.             leftX = mStartPoint.leftX - percent * mElasticDistance;  
  127.         } else if (mDirection == DIRECTION_RIGHT) {  
  128.             rightX = mStartPoint.rightX + percent * mElasticDistance;  
  129.         } else if (mDirection == DIRECTION_UP) {  
  130.             topY = mStartPoint.topY - percent * mElasticDistance;  
  131.         } else if (mDirection == DIRECTION_DOWN) {  
  132.             bottomY = mStartPoint.bottomY + percent * mElasticDistance;  
  133.         }  
  134.         mStatusPoint1.refresh(x, y, topX, topY, bottomX, bottomY,  
  135.                 leftX, leftY, rightX, rightY);  
  136.     }  
  137.     private Ball mStatusPoint2;  
  138.     private void animStatus2() {  
  139.         float percent = (float) ((mAnimPercent - 0.2) * (10f / 3));  
  140.         if (mDirection == DIRECTION_LEFT) {  
  141.             leftX = mStatusPoint1.leftX - percent * (mMoveDistance / 2 - mElasticDistance / 2 );  
  142.             x = mStatusPoint1.x - percent * (mMoveDistance / 2);  
  143.             rightX = mStatusPoint1.rightX - percent * (mMoveDistance / 2 - mElasticDistance / 2 );  
  144.             topX = x;  
  145.             bottomX = x;  
  146.             //偏移值稍作變化  
  147.             offsetTop = radius * c + radius * ( c2 - c ) * percent;  
  148.             offsetBottom = radius * c + radius * ( c2 - c ) * percent;  
  149.         } else if (mDirection == DIRECTION_RIGHT) {  
  150.             rightX = mStatusPoint1.rightX + percent * (mMoveDistance / 2 - mElasticDistance / 2 );  
  151.             x = mStatusPoint1.x + percent * (mMoveDistance / 2);  
  152.             leftX = mStatusPoint1.leftX + percent * (mMoveDistance / 2 - mElasticDistance / 2 );  
  153.             topX = x;  
  154.             bottomX = x;  
  155.             //偏移值稍作變化  
  156.             offsetTop = radius * c + radius * ( c2 - c ) * percent;  
  157.             offsetBottom = radius * c + radius * ( c2 - c ) * percent;  
  158.         } else if (mDirection == DIRECTION_UP) {  
  159.             topY = mStatusPoint1.topY - percent * (mMoveDistance / 2 - mElasticDistance / 2 );  
  160.             y = mStatusPoint1.y - percent * (mMoveDistance / 2);  
  161.             bottomY = mStatusPoint1.bottomY - percent * (mMoveDistance / 2 - mElasticDistance / 2 );  
  162.             leftY = y;  
  163.             rightY = y;  
  164.             //偏移值稍作變化  
  165.             offsetLeft = radius * c + radius * ( c2 - c ) * percent;  
  166.             offsetRight = radius * c + radius * ( c2 - c ) * percent;  
  167.         } else if (mDirection == DIRECTION_DOWN) {  
  168.             bottomY = mStatusPoint1.bottomY + percent * (mMoveDistance / 2 - mElasticDistance / 2 );  
  169.             y = mStatusPoint1.y + percent * (mMoveDistance / 2);  
  170.             topY = mStatusPoint1.topY + percent * (mMoveDistance / 2 - mElasticDistance / 2 );  
  171.             leftY = y;  
  172.             rightY = y;  
  173.             //偏移值稍作變化  
  174.             offsetLeft = radius * c + radius * ( c2 - c ) * percent;  
  175.             offsetRight = radius * c + radius * ( c2 - c ) * percent;  
  176.         }  
  177.         mStatusPoint2.refresh(x, y, topX, topY, bottomX, bottomY,  
  178.                 leftX, leftY, rightX, rightY);  
  179.     }  
  180.     private Ball mStatusPoint3;  
  181.     private void animStatus3() {  
  182.         float percent = (mAnimPercent - 0.5f) * (10f / 3f);  
  183.         if (mDirection == DIRECTION_LEFT) {  
  184.             leftX = mStatusPoint2.leftX - Math.abs(percent * (mEndPoint.rightX - mStatusPoint2  
  185.                     .rightX));  
  186.             x = mStatusPoint2.x - Math.abs(percent * (mEndPoint.x - mStatusPoint2.x));  
  187.             rightX = mStatusPoint2.rightX - Math.abs(percent * (mEndPoint.x - mStatusPoint2.x));  
  188.             topX = x;  
  189.             bottomX = x;  
  190.             //偏移值稍作變化  
  191.             offsetTop = radius * c2 - radius * ( c2 - c ) * percent;  
  192.             offsetBottom = radius * c2 - radius * ( c2 - c ) * percent;  
  193.         } else if (mDirection == DIRECTION_RIGHT) {  
  194.             rightX = mStatusPoint2.rightX + percent * (mEndPoint.rightX - mStatusPoint2.rightX);  
  195.             x = mStatusPoint2.x + percent * (mEndPoint.x - mStatusPoint2.x);  
  196.             leftX = mStatusPoint2.leftX + percent * (mEndPoint.x - mStatusPoint2.x);  
  197.             topX = x;  
  198.             bottomX = x;  
  199.             //偏移值稍作變化  
  200.             offsetTop = radius * c2 - radius * ( c2 - c ) * percent;  
  201.             offsetBottom = radius * c2 - radius * ( c2 - c ) * percent;  
  202.         } else if (mDirection == DIRECTION_UP) {  
  203.             topY = mStatusPoint2.topY - Math.abs(percent * (mEndPoint.topY - mStatusPoint2  
  204.                     .topY));  
  205.             y = mStatusPoint2.y - Math.abs(percent * (mEndPoint.y - mStatusPoint2.y));  
  206.             bottomY = mStatusPoint2.bottomY - Math.abs(percent * (mEndPoint.y - mStatusPoint2.y));  
  207.             leftY = y;  
  208.             rightY = y;  
  209.             //偏移值稍作變化  
  210.             offsetLeft = radius * c2 - radius * ( c2 - c ) * percent;  
  211.             offsetRight = radius * c2 - radius * ( c2 - c ) * percent;  
  212.         } else if (mDirection == DIRECTION_DOWN) {  
  213.             bottomY = mStatusPoint2.bottomY + percent * (mEndPoint.bottomY - mStatusPoint2  
  214.                     .bottomY);  
  215.             y = mStatusPoint2.y + percent * (mEndPoint.y - mStatusPoint2.y);  
  216.             topY = mStatusPoint2.topY + percent * (mEndPoint.y - mStatusPoint2.y);  
  217.             leftY = y;  
  218.             rightY = y;  
  219.             //偏移值稍作變化  
  220.             offsetLeft = radius * c2 - radius * ( c2 - c ) * percent;  
  221.             offsetRight = radius * c2 - radius * ( c2 - c ) * percent;  
  222.         }  
  223.         mStatusPoint3.refresh(x, y, topX, topY, bottomX, bottomY,  
  224.                 leftX, leftY, rightX, rightY);  
  225.     }  
  226.     private Ball mStatusPoint4;  
  227.     private void animStatus4() {  
  228.         float percent = (float) (mAnimPercent - 0.8) * 10;  
  229.         if (mDirection == DIRECTION_LEFT) {  
  230.             rightX = mStatusPoint3.rightX - percent * (Math.abs(mEndPoint.rightX - mStatusPoint3  
  231.                     .rightX) + mElasticDistance/2);  
  232.             //再做一次指派,防止和終點不重合  
  233.             leftX = mEndPoint.leftX;  
  234.             x = mEndPoint.x;  
  235.             bottomX = mEndPoint.bottomX;  
  236.             topX = mEndPoint.topX;  
  237.         } else if (mDirection == DIRECTION_RIGHT) {  
  238.             leftX = mStatusPoint3.leftX + percent * (mEndPoint.leftX - mStatusPoint3.leftX +  
  239.                     mElasticDistance/2);  
  240.             //再做一次指派,防止和終點不重合  
  241.             rightX = mEndPoint.rightX;  
  242.             x = mEndPoint.x;  
  243.             bottomX = mEndPoint.bottomX;  
  244.             topX = mEndPoint.topX;  
  245.         } else if (mDirection == DIRECTION_UP) {  
  246.             bottomY = mStatusPoint3.bottomY - percent * (Math.abs(mEndPoint.bottomY - mStatusPoint3  
  247.                     .bottomY) + mElasticDistance/2);  
  248.             //再做一次指派,防止和終點不重合  
  249.             topY = mEndPoint.topY;  
  250.             y = mEndPoint.y;  
  251.             leftY = mEndPoint.leftY;  
  252.             rightY = mEndPoint.rightY;  
  253.         } else if (mDirection == DIRECTION_DOWN) {  
  254.             topY = mStatusPoint3.topY + percent * (mEndPoint.topY - mStatusPoint3  
  255.                     .topY + mElasticDistance/2);  
  256.             //再做一次指派,防止和終點不重合  
  257.             bottomY = mEndPoint.bottomY;  
  258.             y = mEndPoint.y;  
  259.             leftY = mEndPoint.leftY;  
  260.             rightY = mEndPoint.rightY;  
  261.         }  
  262.         mStatusPoint4.refresh(x, y, topX, topY, bottomX, bottomY,  
  263.                 leftX, leftY, rightX, rightY);  
  264.     }  
  265.     private Ball mStatusPoint5;  
  266.     private void animStatus5() {  
  267.         float percent = (float) (mAnimPercent - 0.9) * 10;  
  268.         if (mDirection == DIRECTION_LEFT) {  
  269.             rightX = mStatusPoint4.rightX + percent * (mEndPoint.rightX - mStatusPoint4.rightX);  
  270.         } else if (mDirection == DIRECTION_RIGHT) {  
  271.             leftX = mStatusPoint4.leftX + percent * (mEndPoint.leftX - mStatusPoint4.leftX);  
  272.         } else if (mDirection == DIRECTION_UP) {  
  273.             bottomY = mStatusPoint4.bottomY + percent * (mEndPoint.bottomY - mStatusPoint4.bottomY);  
  274.         } else if (mDirection == DIRECTION_DOWN) {  
  275.             topY = mStatusPoint4.topY + percent * (mEndPoint.topY - mStatusPoint4.topY);  
  276.         }  
  277.         mStatusPoint5.refresh(x, y, topX, topY, bottomX, bottomY,  
  278.                 leftX, leftY, rightX, rightY);  
  279.     }  
  280.     private Path drawElasticCircle(  
  281.             float topX, float topY, float offsetTop1, float offsetTop2,  
  282.             float bottomX, float bottomY, float offsetBottom1, float offsetBottom2,  
  283.             float leftX, float leftY, float offsetLeft1, float offsetLeft2,  
  284.             float rightX, float rightY, float offsetRight1, float offsetRight2  
  285.     ) {  
  286.         PointF controlTop1, controlTop2, controlBottom1, controlBottom2,  
  287.                 controlLeft1, controlLeft2, controlRight1, controlRight2;  
  288.         controlTop1 = new PointF();  
  289.         controlTop1.x = topX - offsetTop1;  
  290.         controlTop1.y = topY;  
  291.         controlTop2 = new PointF();  
  292.         controlTop2.x = topX + offsetTop2;  
  293.         controlTop2.y = topY;  
  294.         controlBottom1 = new PointF();  
  295.         controlBottom1.x = bottomX - offsetBottom1;  
  296.         controlBottom1.y = bottomY;  
  297.         controlBottom2 = new PointF();  
  298.         controlBottom2.x = bottomX + offsetBottom2;  
  299.         controlBottom2.y = bottomY;  
  300.         controlLeft1 = new PointF();  
  301.         controlLeft1.x = leftX;  
  302.         controlLeft1.y = leftY - offsetLeft1;  
  303.         controlLeft2 = new PointF();  
  304.         controlLeft2.x = leftX;  
  305.         controlLeft2.y = leftY + offsetLeft2;  
  306.         controlRight1 = new PointF();  
  307.         controlRight1.x = rightX;  
  308.         controlRight1.y = rightY - offsetRight1;  
  309.         controlRight2 = new PointF();  
  310.         controlRight2.x = rightX;  
  311.         controlRight2.y = rightY + offsetRight2;  
  312.         Path path = new Path();  
  313.         path.moveTo(topX, topY);  
  314.         path.cubicTo(controlTop1.x, controlTop1.y, controlLeft1.x, controlLeft1.y, leftX, leftY);  
  315.         path.cubicTo(controlLeft2.x ,controlLeft2.y, controlBottom1.x, controlBottom1.y, bottomX,  
  316.                 bottomY);  
  317.         path.cubicTo(controlBottom2.x, controlBottom2.y, controlRight2.x, controlRight2.y,  
  318.                 rightX, rightY);  
  319.         path.cubicTo(controlRight1.x, controlRight1.y, controlTop2.x, controlTop2.y, topX, topY);  
  320.         return path;  
  321.     }  
  322.     private float getDistance(float x1, float y1, float x2, float y2) {  
  323.         return (float) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));  
  324.     }  
  325. }  
 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:

安卓繪制貝塞爾曲線

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

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

參考:

http://www.jianshu.com/p/791d3a791ec2

http://spencermortensen.com/articles/bezier-circle/

項目源碼下載下傳:

1)Github 

 我的GitHub  https://github.com/82367825/BezierMaster

2)CSDN下載下傳頻道

 如果GitHub通路不了,也可以到CSDN下載下傳頻道,源碼工程