Path從懵逼到精通(2)——貝塞爾曲線

上一篇我們說了 Path 的基本操作,這一篇讓我們來說一下 Path 的進(jìn)階用法——貝塞爾曲線。

那什么是貝塞爾曲線?貝塞爾曲線能在 Android 中實(shí)現(xiàn)什么效果?以及如何做到的?這篇文章都會告訴你。

什么是貝塞爾曲線?

貝塞爾曲線是由皮埃爾·貝塞爾發(fā)表的,他主要應(yīng)用于汽車的主體進(jìn)行設(shè)計(jì),后來成為計(jì)算機(jī)圖形學(xué)相當(dāng)重要的參數(shù)曲線。

貝塞爾曲線由什么組成的?它通常由數(shù)據(jù)點(diǎn)和控制點(diǎn)兩個部分組成的。那什么是數(shù)據(jù)點(diǎn)和控制點(diǎn)呢?請看下表:

類型 作用
數(shù)據(jù)點(diǎn) 曲線的起點(diǎn)和終點(diǎn)
控制點(diǎn) 控制曲線的彎曲程度

這樣聽起來可能還有點(diǎn)抽象,我們直接上圖來看看。

一階貝塞爾曲線:

一階貝塞爾曲線其實(shí)就是一條直線,沒有控制點(diǎn),只有數(shù)據(jù)點(diǎn) P0,P1,如下圖:

一階貝塞爾曲線

Android提供方法:lineTo()

二階貝塞爾曲線:

二階貝塞爾曲線有一個控制點(diǎn) P1 和兩個數(shù)據(jù)點(diǎn) P0,P2。如下圖:


二階貝塞爾曲線

Android 提供方法:quadTo()

三階貝塞爾曲線:

三階貝塞爾曲線有兩個控制點(diǎn) P1,P2 和兩個數(shù)據(jù)點(diǎn) P0,P3。如下圖:

三階貝塞爾曲線

Android 提供方法:cubicTo()

更高階的曲線 Android 并沒有提供 API ,所以在這只會介紹二階和三階曲線,如果對更高階的曲線有興趣的話,可以去貝塞爾曲線———維基百科貝塞爾曲線動態(tài)演示這兩個網(wǎng)站多了解一下。

貝塞爾曲線是怎么形成的

那么這條曲線究竟是怎么形成的呢?先從二階曲線分析一下:

二階貝塞爾曲線形成原理:

1.連接 A,B 形成 AB 線段,連接 B,C 形成 BC 線段。

連成AB,BC線段

2.在 AB 線段取一個點(diǎn) D,BC 線段取一個點(diǎn) E ,使其滿足條件: AD/AB = BE/BC,連接 D,E 形成線段 DE。

連接DE

3.在 DE 取一個點(diǎn) F,使其滿足條件:AD/AB = BE/BC = DF/DE。

4.而滿足這些條件的所有的 F 點(diǎn)所形成的軌跡就是二階貝塞爾曲線,動態(tài)過程如下:


二階貝塞爾曲線
三階貝塞爾曲線形成原理:

1.連接 A,B 形成 AB 線段,連接 B,C 形成 BC 線段,連接 C,D 形成 CD 線段。

2.在AB線段取一個點(diǎn) E,BC 線段取一個點(diǎn) F,CD 線段取一個點(diǎn) G,使其滿足條件: AE/AB = BF/BE = CG/CD。連接 E,F(xiàn) 形成線段 EF,連接 F,G 形成線段 FG。

3.在EF線段取一個點(diǎn) H,F(xiàn)G 線段取一個點(diǎn) I,使其滿足條件: AE/AB = BF/BE = CG/CD = EH/EF = FI/FG。連接 H,I 形成線段 HI。

4.在 HI 線段取一個點(diǎn) J,使其滿足條件: AE/AB = BF/BE = CG/CD = EH/EF = FI/FG = HJ/HI。

5.而滿足這些條件的所有的J點(diǎn)所形成的軌跡就是三階貝塞爾曲線,動態(tài)過程如下:


三階貝塞爾曲線

在 Android 中使用貝塞爾曲線

說了這么多原理,是時候要知道要怎么運(yùn)用貝塞爾曲線了。這里我會用兩個例子來說明二階和三階貝塞爾曲線的運(yùn)用:

二階曲貝塞爾曲線的應(yīng)用:
方法預(yù)覽:
public void quadTo (float x1, float y1, float x2, float y2)
有什么用:

畫出二階貝塞爾曲線

怎么用:

因?yàn)槎A貝塞爾曲線需要三個點(diǎn)才能確定,所以 quadTo 方法中的四個參數(shù)分別是確定第二,第三的點(diǎn)的。第一個點(diǎn)就是 path 上次操作的點(diǎn)。
現(xiàn)在用一個實(shí)例來練習(xí)下這個方法:

水波紋效果:
效果圖:
水波紋效果
實(shí)現(xiàn)思路:
  1. 畫出至少兩段的波紋
  2. 使用 ValueAnimator 不斷獲取平移的值 offset
  3. 利用 offset 不斷的改變波紋的位置

現(xiàn)在分步驟來說明:

1. 畫出至少兩段波紋

我們首先要畫出兩段波紋。一段波紋就包含兩條曲線。每條曲線我們可以使用 quadTo() 方法來畫。

為了更容易理解,請看下圖:

水波紋坐標(biāo)圖

mWL 是一段波紋的長度,mCenterY 是屏幕高度的一半。

  • 畫第一段波紋的第一條曲線:
mPath.moveTo(-mWL, mCenterY); //將path操作的起點(diǎn)移動到(-mWL,mCenterY)
mPath.quadTo((-mWL * 3 / 4) , mCenterY + 60, (-mWL / 2), mCenterY); //畫出第一段波紋的第一條曲線
  • 畫出第一段波紋的第二條曲線:
mPath.quadTo((-mWL / 4) , mCenterY - 60, 0, mCenterY); //畫出第一段波紋的第二條曲線
  • 畫出第二段波紋的第一條曲線:
mPath.quadTo((mWL /4) , mCenterY + 60, (mWL / 2), mCenterY); //畫出第二段波紋的第一條曲線
  • 畫出第二段波紋的第二條曲線:
mPath.quadTo((mWL * 3/ 4) , mCenterY - 60, mWL, mCenterY);  //畫出第二段波紋的第二條曲線
2. 使用 ValueAnimator 不斷獲取平移的值 offset

那么現(xiàn)在來想一下應(yīng)該怎么讓這幾段波紋動起來呢?我們需要一個 offset 的平移值。這個值應(yīng)該加在每個點(diǎn)的x坐標(biāo)上,并且 offset 是不斷變化的,這樣才會形成一個向右平移的效果。那怎么才能獲取到這個變化的offset的值呢?答案就是要使用 ValueAnimator 。用法如下:

   ValueAnimator animator = ValueAnimator.ofInt(0, mWL); //mWL是一段波紋的長度
   animator.setDuration(1000);
   animator.setRepeatCount(ValueAnimator.INFINITE);
   animator.setInterpolator(new LinearInterpolator());
   animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                offset = (Integer) animation.getAnimatedValue(); //offset 的值的范圍在[0,mWL]之間。
                postInvalidate();
            }
        });
   animator.start();
    }

這樣只要動畫開始,offset 就會不斷從 0~mWL 變化。

3. 利用offset不斷的改變波紋的位置

現(xiàn)在為曲線的所有 X 坐標(biāo)都加上 offset 值。這樣就會產(chǎn)生平移的效果,為了簡化代碼,這里使用的 for 循環(huán)來畫曲線。

    mPath.moveTo(-mWL + mOffset, mCenterY);
    for (int i = 0; i < 2; i++) {
        mPath.quadTo((-mWL * 3 / 4) + (i * mWL) + offset, mCenterY + 60, (-mWL / 2) + (i * mWL) + offset, mCenterY);
        mPath.quadTo((-mWL / 4) + (i * mWL) + offset, mCenterY - 60, i * mWL + offset, mCenterY);
      }

接下來為了適配各種屏幕,需要根據(jù)手機(jī)的寬度來計(jì)算出所需要的波紋的數(shù)目:

mWaveCount = (int) Math.round(mScreenWidth / mWL + 1.5); //這樣就保證波紋能覆蓋整個屏幕

上面的 for 循環(huán)也可以改為:

    mPath.moveTo(-mWL + mOffset, mCenterY);
    for (int i = 0; i < mWaveCount; i++) {
        mPath.quadTo((-mWL * 3 / 4) + (i * mWL) + offset, mCenterY + 60, (-mWL / 2) + (i * mWL) + offset, mCenterY);
        mPath.quadTo((-mWL / 4) + (i * mWL) + offset, mCenterY - 60, i * mWL + offset, mCenterY);
      }
三階貝塞爾曲線的應(yīng)用:
彈性的圓:
效果圖:
彈性的圓
實(shí)現(xiàn)思路:
  1. 用貝塞爾曲線畫出正圓
  2. 通過修改坐標(biāo)的大小來形成圓收縮的效果
1. 用貝塞爾曲線畫出正圓

我們首先要知道怎么使用 cubicTo() 方法來畫個半徑為 r 的正圓。其實(shí)使用 cubicTo() 來畫正圓就需要 4 條三階貝塞爾曲線組合而成。如圖所示:


三階貝塞爾曲線形成的圓

如果要畫 P0P3 那道曲線應(yīng)該怎么畫呢?我們就要知道 P0,P1,P2,P3 這四個點(diǎn)的坐標(biāo)。P0,P3 的坐標(biāo)我們已經(jīng)知道了,分別是 (0,-r),(r,0)。那么 P1 和 P2 的坐標(biāo)是什么呢?其實(shí)這里有個論證的過程,這個過程在這篇文章就有:Approximate a circle with cubic Bézier curves,感興趣的可以看看。這里只說結(jié)果,最后得到一個數(shù),這個數(shù)就是 c = 0.551915024494。也就是說 P1,P2 的坐標(biāo)就是 (cr,-r),(r,-cr)。其他點(diǎn)的坐標(biāo)也是用同樣的方法得出的,這里就不細(xì)說了。

為了更方便管理這幾個點(diǎn),我將這幾個點(diǎn)封裝分成兩個類。分別是 HorizontalLine 和 VerticalLine 。圓的上下兩條線屬于 HorizontalLine,圓的左右兩條線屬于 VerticalLine。

以下是這兩個類的代碼:

private float c = 0.551915024494f;

class HorizontalLine {
        public PointF left = new PointF(); //P7 P11
        public PointF middle = new PointF(); //P0 P6
        public PointF right = new PointF(); //P1 P5

        public HorizontalLine(float x,float y) {
            left.x = -radius*c;
            left.y = y;
            middle.x = x;
            middle.y = y;
            right.x = radius*c;
            right.y = y;
        }

        public void setY(float y) {
            left.y = y;
            middle.y = y;
            right.y = y;
        }

    }

    class VerticalLine {
        public PointF top = new PointF(); //P2 P10
        public PointF middle = new PointF(); //P3 P9
        public PointF bottom = new PointF(); //P4 P8


        public VerticalLine(float x,float y) {
            top.x = x;
            top.y = -radius*c;
            middle.x = x;
            middle.y = y;
            bottom.x = x;
            bottom.y = radius*c;
        }


        public void setX(float x) {
            top.x = x;
            middle.x = x;
            bottom.x = x;
        }
    }

以下是用cubicTo()方法畫圓的代碼:

    private Paint mPaint;

    private Path mPath;

    private int mScreenHeight;//屏幕高度

    private int mScreenWidth;//屏幕寬度

    private float radius = 100;

    private void initPaint() {
        mPath = new Path();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.parseColor("#59c3e2"));
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
    }

    private void initPoint() {
        mTopLine = new HorizontalLine(0,-radius);
        mBottomLine = new HorizontalLine(0,radius);
        mLeftLine = new VerticalLine(-radius,0);
        mRightLine = new VerticalLine(radius,0);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();
        
        //將畫布移動到屏幕正中間
        canvas.translate(mScreenWidth / 2, mScreenHeight / 2); 

        mPath.moveTo(mTopLine.middle.x,mTopLine.middle.y);
        mPath.cubicTo(mTopLine.right.x,mTopLine.right.y,mRightLine.top.x,mRightLine.top.y,
                mRightLine.middle.x,mRightLine.middle.y);
        mPath.cubicTo(mRightLine.bottom.x,mRightLine.bottom.y,mBottomLine.right.x,mBottomLine.right.y,
                mBottomLine.middle.x,mBottomLine.middle.y);
        mPath.cubicTo(mBottomLine.left.x,mBottomLine.left.y,mLeftLine.bottom.x,mLeftLine.bottom.y,
                mLeftLine.middle.x,mLeftLine.middle.y);
        mPath.cubicTo(mLeftLine.top.x,mLeftLine.top.y,mTopLine.left.x,mTopLine.left.y,
                mTopLine.middle.x,mTopLine.middle.y);
        canvas.drawPath(mPath,mPaint);
    }

效果如下:
2.通過修改坐標(biāo)的大小來形成圓收縮的效果

想要達(dá)到圓收縮的效果只要增加和減少某些坐標(biāo)就可以了。比如我要達(dá)成如圖的這種效果,應(yīng)該怎么做呢?

只要增加 P2,P3,P4 的橫坐標(biāo),就可以達(dá)到這種效果。

現(xiàn)在試試把圓收縮起來:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath.reset();

        //將畫布移動到手機(jī)屏幕的正中間
        canvas.translate(mScreenWidth / 2, mScreenHeight / 2); 


        //將右邊的線的點(diǎn)的橫坐標(biāo)都增大
        mRightLine.setX(radius * 1.5f); 
        
        mPath.moveTo(mTopLine.middle.x,mTopLine.middle.y);
        mPath.cubicTo(mTopLine.right.x,mTopLine.right.y,mRightLine.top.x,mRightLine.top.y,
                mRightLine.middle.x,mRightLine.middle.y);
        mPath.cubicTo(mRightLine.bottom.x,mRightLine.bottom.y,mBottomLine.right.x,mBottomLine.right.y,
                mBottomLine.middle.x,mBottomLine.middle.y);
        mPath.cubicTo(mBottomLine.left.x,mBottomLine.left.y,mLeftLine.bottom.x,mLeftLine.bottom.y,
                mLeftLine.middle.x,mLeftLine.middle.y);
        mPath.cubicTo(mLeftLine.top.x,mLeftLine.top.y,mTopLine.left.x,mTopLine.left.y,
                mTopLine.middle.x,mTopLine.middle.y);
        canvas.drawPath(mPath,mPaint);
    }
效果如下:

以此類推,如果要達(dá)到剛剛那個動圖的效果,就要減少上下兩條線的點(diǎn)的縱坐標(biāo),然后不斷平移畫布就可以了。具體代碼可以下載源碼來看。

源碼下載:

參考資料:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容