天天看點

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表

Path之貝塞爾曲線

作者微網誌: @GcsSloop

【本系列相關文章】

在上一篇文章Path之基本圖形中我們了解了Path的基本使用方法,本次了解Path中非常非常非常重要的内容-貝塞爾曲線。

一.Path常用方法表

為了相容性(偷懶) 本表格中去除了在API21(即安卓版本5.0)以上才添加的方法。忍不住吐槽一下,為啥看起來有些順手就能寫的重載方法要等到API21才添加上啊。寶寶此刻内心也是崩潰的。
作用 相關方法 備注
移動起點 moveTo 移動下一次操作的起點位置
設定終點 setLastPoint 重置目前path中最後一個點位置,如果在繪制之前調用,效果和moveTo相同
連接配接直線 lineTo 添加上一個點到目前點之間的直線到Path
閉合路徑 close 連接配接第一個點連接配接到最後一個點,形成一個閉合區域
添加内容 addRect, addRoundRect, addOval, addCircle, addPath, addArc, arcTo 添加(矩形, 圓角矩形, 橢圓, 圓, 路徑, 圓弧) 到目前Path (注意addArc和arcTo的差別)
是否為空 isEmpty 判斷Path是否為空
是否為矩形 isRect 判斷path是否是一個矩形
替換路徑 set 用新的路徑替換到目前路徑所有内容
偏移路徑 offset 對目前路徑之前的操作進行偏移(不會影響之後的操作)
貝塞爾曲線 quadTo, cubicTo 分别為二次和三次貝塞爾曲線的方法
rXxx方法 rMoveTo, rLineTo, rQuadTo, rCubicTo 不帶r的方法是基于原點的坐标系(偏移量),rXxx方法是基于目前點坐标系(偏移量)
填充模式 setFillType, getFillType, isInverseFillType, toggleInverseFillType 設定,擷取,判斷和切換填充模式
提示方法 incReserve 提示Path還有多少個點等待加入(這個方法貌似會讓Path優化存儲結構)
布爾操作(API19) op 對兩個Path進行布爾運算(即取交集、并集等操作)
計算邊界 computeBounds 計算Path的邊界
重置路徑 reset, rewind 清除Path中的内容(reset相當于重置到new Path階段,rewind會保留Path的資料結構)
矩陣操作 transform 矩陣變換

二.Path詳解

上一次除了一些常用函數之外,講解的基本上都是直線,本次需要了解其中的曲線部分,說到曲線,就不得不提大名鼎鼎的貝塞爾曲線。它的發明者是下面這個人(法國數學家PierreBézier)。

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表

貝塞爾曲線能幹什麼?

貝塞爾曲線的運用是十分廣泛的,可以說貝塞爾曲線奠定了計算機繪圖的基礎(因為它可以将任何複雜的圖形用精确的數學語言進行描述),在你不經意間就已經使用過它了。

你會使用Photoshop的話,你可能會注意到裡面有一個鋼筆工具,這個鋼筆工具核心就是貝塞爾曲線。

你說你不會PS? 沒關系,你如果看過前面的文章或者用過2D繪圖,肯定繪制過圓,圓弧,圓角矩形等這些東西。這裡面的圓弧部分全部都是貝塞爾曲線的運用。

貝塞爾曲線作用十分廣泛,簡單舉幾個的栗子:

  • QQ小紅點拖拽效果
  • 一些炫酷的下拉重新整理控件
  • 閱讀軟體的翻書效果
  • 一些平滑的折線圖的制作
  • 很多炫酷的動畫效果

如何輕松入門貝塞爾曲線?

雖然貝塞爾曲線用途非常廣泛,然而目前貌似并沒有适合的中文教程,能夠搜尋出來Android關于貝塞爾曲線的中文文章基本可以分為以下幾種:

* 科普型(隻是讓人了解貝塞爾,并沒有實質性的内容)

* 裝逼型(擺出來一大堆公式,引用一堆英文原文)

* 基礎型(僅僅是講解貝塞爾曲線的兩個函數用法)

* 實戰型(根據執行個體講解其中貝塞爾曲線的運用)

以上幾種類型中比較有用的就是基礎型和實戰型,但兩者各有不足,本文會綜合兩者内容,從零開始學習貝塞爾曲線。

第一步.了解貝塞爾曲線的原理

此處了解貝塞爾曲線并非是學會公式的推倒過程,而是要了解貝塞爾曲線是如何生成的。貝塞爾曲線是用一系列點來控制曲線狀态的,我将這些點簡單分為兩類:

類型 作用
資料點 确定曲線的起始和結束位置
控制點 确定曲線的彎曲程度
此處暫時僅作了解概念,接下來就會講解其中詳細的含義。

一階曲線原理:

一階曲線是沒有控制點的,僅有兩個資料點(A 和 B),最終效果一個線段。

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表
上圖表示的是一階曲線生成過程中的某一個階段,動态過程可以參照下圖。
安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表
PS:一階曲線其實就是前面講解過的lineTo。

二階曲線原理:

二階曲線由兩個資料點(A 和 C),一個控制點(B)來描述曲線狀态,大緻如下:

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表

上圖中紅色曲線部分就是傳說中的二階貝塞爾曲線,那麼這條紅色曲線是如何生成的呢?接下來我們就以其中的一個狀态分析一下:

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表

連接配接AB BC,并在AB上取點D,BC上取點E,使其滿足條件:

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表
安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表

連接配接DE,取點F,使得:

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表

這樣擷取到的點F就是貝塞爾曲線上的一個點,動态過程如下:

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表
PS: 二階曲線對應的方法是quadTo

三階曲線原理:

三階曲線由兩個資料點(A 和 D),兩個控制點(B 和 C)來描述曲線狀态,如下:

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表

三階曲線計算過程與二階類似,具體可以見下圖動态效果:

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表
PS: 三階曲線對應的方法是cubicTo

貝塞爾曲線速查表

強烈推薦點選這裡練習貝塞爾曲線,可以加深對貝塞爾曲線的了解程度。

第二步.了解貝塞爾曲線相關函數使用方法

一階曲線:

一階曲線是一條線段,非常簡單,可以參見上一篇文章Path之基本操作,此處就不詳細講解了。

二階曲線:

通過上面對二階曲線的簡單了解,我們知道二階曲線是由兩個資料點,一個控制點構成,接下來我們就用一個執行個體來示範二階曲線是如何運用的。

首先,兩個資料點是控制貝塞爾曲線開始和結束的位置,比較容易了解,而控制點則是控制貝塞爾的彎曲狀态,相對來說比較難以了解,是以本示例重點在于了解貝塞爾曲線彎曲狀态與控制點的關系,廢話不多說,先上效果圖:

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表
為了更加容易看出控制點與曲線彎曲程度的關系,上圖中繪制出了輔助點和輔助線,從上面的動态圖可以看出,貝塞爾曲線在動态變化過程中有類似于橡皮筋一樣的彈性效果,是以在制作一些彈性效果的時候很常用。

主要代碼如下:

public class Bezier extends View {

    private Paint mPaint;
    private int centerX, centerY;

    private PointF start, end, control;

    public Bessel1(Context context) {
        super(context);
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setTextSize();

        start = new PointF(,);
        end = new PointF(,);
        control = new PointF(,);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        centerX = w/;
        centerY = h/;

        // 初始化資料點和控制點的位置
        start.x = centerX-;
        start.y = centerY;
        end.x = centerX+;
        end.y = centerY;
        control.x = centerX;
        control.y = centerY-;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 根據觸摸位置更新控制點,并提示重繪
        control.x = event.getX();
        control.y = event.getY();
        invalidate();
        return true;
    }

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

        // 繪制資料點和控制點
        mPaint.setColor(Color.GRAY);
        mPaint.setStrokeWidth();
        canvas.drawPoint(start.x,start.y,mPaint);
        canvas.drawPoint(end.x,end.y,mPaint);
        canvas.drawPoint(control.x,control.y,mPaint);

        // 繪制輔助線
        mPaint.setStrokeWidth();
        canvas.drawLine(start.x,start.y,control.x,control.y,mPaint);
        canvas.drawLine(end.x,end.y,control.x,control.y,mPaint);

        // 繪制貝塞爾曲線
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth();

        Path path = new Path();

        path.moveTo(start.x,start.y);
        path.quadTo(control.x,control.y,end.x,end.y);

        canvas.drawPath(path, mPaint);
    }
}
           

三階曲線:

三階曲線由兩個資料點和兩個控制點來控制曲線狀态。

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表

代碼:

public class Bezier2 extends View {

    private Paint mPaint;
    private int centerX, centerY;

    private PointF start, end, control1, control2;
    private boolean mode = true;

    public Bezier2(Context context) {
        this(context, null);

    }

    public Bezier2(Context context, AttributeSet attrs) {
        super(context, attrs);

        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setTextSize();

        start = new PointF(, );
        end = new PointF(, );
        control1 = new PointF(, );
        control2 = new PointF(, );
    }

    public void setMode(boolean mode) {
        this.mode = mode;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        centerX = w / ;
        centerY = h / ;

        // 初始化資料點和控制點的位置
        start.x = centerX - ;
        start.y = centerY;
        end.x = centerX + ;
        end.y = centerY;
        control1.x = centerX;
        control1.y = centerY - ;
        control2.x = centerX;
        control2.y = centerY - ;

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 根據觸摸位置更新控制點,并提示重繪
        if (mode) {
            control1.x = event.getX();
            control1.y = event.getY();
        } else {
            control2.x = event.getX();
            control2.y = event.getY();
        }
        invalidate();
        return true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //drawCoordinateSystem(canvas);

        // 繪制資料點和控制點
        mPaint.setColor(Color.GRAY);
        mPaint.setStrokeWidth();
        canvas.drawPoint(start.x, start.y, mPaint);
        canvas.drawPoint(end.x, end.y, mPaint);
        canvas.drawPoint(control1.x, control1.y, mPaint);
        canvas.drawPoint(control2.x, control2.y, mPaint);

        // 繪制輔助線
        mPaint.setStrokeWidth();
        canvas.drawLine(start.x, start.y, control1.x, control1.y, mPaint);
        canvas.drawLine(control1.x, control1.y,control2.x, control2.y, mPaint);
        canvas.drawLine(control2.x, control2.y,end.x, end.y, mPaint);

        // 繪制貝塞爾曲線
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth();

        Path path = new Path();

        path.moveTo(start.x, start.y);
        path.cubicTo(control1.x, control1.y, control2.x,control2.y, end.x, end.y);

        canvas.drawPath(path, mPaint);
    }
}
           
三階曲線相比于二階曲線可以制作更加複雜的形狀,但是對于高階的曲線,用低階的曲線組合也可達到相同的效果,就是傳說中的降階。是以我們對貝塞爾曲線的封裝方法一般最高隻到三階曲線。

降階與升階

類型 釋義 變化
降階 在保持曲線形狀與方向不變的情況下,減少控制點數量,即降低曲線階數 方法變得簡單,資料點變多,控制點可能減少,靈活性變弱
升階 在保持曲線形狀與方向不變的情況下,增加控制點數量,即升高曲線階數 方法更加複雜,資料點不變,控制點增加,靈活性變強

第三步.貝塞爾曲線使用執行個體

在制作這個執行個體之前,首先要明确一個内容,就是在什麼情況下需要使用貝塞爾曲線?

需要繪制不規則圖形時? 當然不是!目前來說,我覺得使用貝塞爾曲線主要有以下幾個方面(僅個人拙見,可能存在錯誤,歡迎指正)
序号 内容 用例
1 事先不知道曲線狀态,需要實時計算時 天氣預報氣溫變化的平滑折線圖
2 顯示狀态會根據使用者操作改變時 QQ小紅點,仿真翻書效果
3 一些比較複雜的運動狀态(配合PathMeasure使用) 複雜運動狀态的動畫效果

至于隻需要一個靜态的曲線圖形的情況,用圖檔豈不是更好,大量的計算會很不劃算。

如果是顯示SVG矢量圖的話,已經有相關的解析工具了(内部依舊運用的有貝塞爾曲線),不需要手動計算。

貝塞爾曲線的主要優點是可以實時控制曲線狀态,并可以通過改變控制點的狀态實時讓曲線進行平滑的狀态變化。

接下來我們就用一個簡單的示例讓一個圓漸變成為心形:

效果圖:

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表

思路分析:

我們最終的需要的效果是将一個圓轉變成一個心形,通過分析可知,圓可以由四段三階貝塞爾曲線組合而成,如下:

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表

心形也可以由四段的三階的貝塞爾曲線組成,如下:

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表

兩者的差别僅僅在于資料點和控制點位置不同,是以隻需要調整資料點和控制點的位置,就能将圓形變為心形。

核心難點:

1.如何得到資料點和控制點的位置?

關于使用繪制圓形的資料點與控制點早就已經有人詳細的計算好了,可以參考stackoverflow的一個回答How to create circle with Bézier curves?其中的資料隻需要拿來用即可。

而對于心形的資料點和控制點,可以由圓形的部分資料點和控制點平移後得到,具體參數可以自己慢慢調整到一個滿意的效果。

2.如何達到漸變效果?

漸變其實就是每次對資料點和控制點稍微移動一點,然後重繪界面,在短時間多次的調整資料點與控制點,使其逐漸接近目标值,通過不斷的重繪界面達到一種漸變的效果。過程可以參照下圖動态效果:

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表

代碼:

public class Bezier3 extends View {
    private static final float C = f;     // 一個常量,用來計算繪制圓形貝塞爾曲線控制點的位置

    private Paint mPaint;
    private int mCenterX, mCenterY;

    private PointF mCenter = new PointF(,);
    private float mCircleRadius = ;                  // 圓的半徑
    private float mDifference = mCircleRadius*C;        // 圓形的控制點與資料點的內插補點

    private float[] mData = new float[];               // 順時針記錄繪制圓形的四個資料點
    private float[] mCtrl = new float[];              // 順時針記錄繪制圓形的八個控制點

    private float mDuration = ;                     // 變化總時長
    private float mCurrent = ;                         // 目前已進行時長
    private float mCount = ;                         // 将時長總共劃分多少份
    private float mPiece = mDuration/mCount;            // 每一份的時長


    public Bezier3(Context context) {
        this(context, null);

    }

    public Bezier3(Context context, AttributeSet attrs) {
        super(context, attrs);

        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStrokeWidth();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setTextSize();


        // 初始化資料點

        mData[] = ;
        mData[] = mCircleRadius;

        mData[] = mCircleRadius;
        mData[] = ;

        mData[] = ;
        mData[] = -mCircleRadius;

        mData[] = -mCircleRadius;
        mData[] = ;

        // 初始化控制點

        mCtrl[]  = mData[]+mDifference;
        mCtrl[]  = mData[];

        mCtrl[]  = mData[];
        mCtrl[]  = mData[]+mDifference;

        mCtrl[]  = mData[];
        mCtrl[]  = mData[]-mDifference;

        mCtrl[]  = mData[]+mDifference;
        mCtrl[]  = mData[];

        mCtrl[]  = mData[]-mDifference;
        mCtrl[]  = mData[];

        mCtrl[] = mData[];
        mCtrl[] = mData[]-mDifference;

        mCtrl[] = mData[];
        mCtrl[] = mData[]+mDifference;

        mCtrl[] = mData[]-mDifference;
        mCtrl[] = mData[];
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mCenterX = w / ;
        mCenterY = h / ;
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
         drawCoordinateSystem(canvas);       // 繪制坐标系

        canvas.translate(mCenterX, mCenterY); // 将坐标系移動到畫布中央
        canvas.scale(,-);                 // 翻轉Y軸

        drawAuxiliaryLine(canvas);


        // 繪制貝塞爾曲線
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth();

        Path path = new Path();
        path.moveTo(mData[],mData[]);

        path.cubicTo(mCtrl[],  mCtrl[],  mCtrl[],  mCtrl[],     mData[], mData[]);
        path.cubicTo(mCtrl[],  mCtrl[],  mCtrl[],  mCtrl[],     mData[], mData[]);
        path.cubicTo(mCtrl[],  mCtrl[],  mCtrl[], mCtrl[],    mData[], mData[]);
        path.cubicTo(mCtrl[], mCtrl[], mCtrl[], mCtrl[],    mData[], mData[]);

        canvas.drawPath(path, mPaint);

        mCurrent += mPiece;
        if (mCurrent < mDuration){

            mData[] -= /mCount;
            mCtrl[] += /mCount;
            mCtrl[] += /mCount;

            mCtrl[] -= /mCount;
            mCtrl[] += /mCount;

        //    postInvalidateDelayed((long) mPiece);
        }
    }

    // 繪制輔助線
    private void drawAuxiliaryLine(Canvas canvas) {
        // 繪制資料點和控制點
        mPaint.setColor(Color.GRAY);
        mPaint.setStrokeWidth();

        for (int i=; i<; i+=){
            canvas.drawPoint(mData[i],mData[i+], mPaint);
        }

        for (int i=; i<; i+=){
            canvas.drawPoint(mCtrl[i], mCtrl[i+], mPaint);
        }


        // 繪制輔助線
        mPaint.setStrokeWidth();

        for (int i=, j=; i<; i+=, j+=){
            canvas.drawLine(mData[i],mData[i+],mCtrl[j],mCtrl[j+],mPaint);
            canvas.drawLine(mData[i],mData[i+],mCtrl[j+],mCtrl[j+],mPaint);
        }
        canvas.drawLine(mData[],mData[],mCtrl[],mCtrl[],mPaint);
        canvas.drawLine(mData[],mData[],mCtrl[],mCtrl[],mPaint);
    }

    // 繪制坐标系
    private void drawCoordinateSystem(Canvas canvas) {
        canvas.save();                      // 繪制做坐标系

        canvas.translate(mCenterX, mCenterY); // 将坐标系移動到畫布中央
        canvas.scale(,-);                 // 翻轉Y軸

        Paint fuzhuPaint = new Paint();
        fuzhuPaint.setColor(Color.RED);
        fuzhuPaint.setStrokeWidth();
        fuzhuPaint.setStyle(Paint.Style.STROKE);

        canvas.drawLine(, -, , , fuzhuPaint);
        canvas.drawLine(-, , , , fuzhuPaint);

        canvas.restore();
    }
}
           

三.總結

其實關于貝塞爾曲線最重要的是核心了解貝塞爾曲線的生成方式,隻有了解了貝塞爾曲線的生成方式,才能更好的運用貝塞爾曲線。在上一篇末尾說本篇要涉及一點自相交圖形渲染問題,不幸的是,本篇沒有了,請期待下一篇(可能會在下一篇中出現o( ̄︶ ̄)o),下一篇依舊Path相關内容,教給大家一些更好玩的東西。

解鎖新的境界之【繪制一個彈性的圓】:

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表

(,,• ₃ •,,)

PS: 由于本人水準有限,某些地方可能存在誤解或不準确,如果你對此有疑問可以送出Issues進行回報。

About Me

作者微網誌: @GcsSloop

安卓自定義View進階 - 貝塞爾曲線Path之貝塞爾曲線一.Path常用方法表

參考資料

Path

Canvas

貝塞爾曲線掃盲

貝塞爾曲線-維基百科

How to create circle with Bézier curves?

三次貝塞爾曲線練習之彈性的圓