Android 進階——高級UI必知必會之Path和貝塞爾曲線(五)

引言

在Android開發中經常會碰到自定義控件,自定義控件涉及的內容比較多,如測量和繪製、事件分發的處理、動畫效果的渲染與實現,前面的文章已經介紹了繪製的相關知識,而一些曲線或曲面的UI效果離不開Path貝塞爾曲線(Bézier curve),那麼到底什麼是貝塞爾曲線,它擁有哪些特點,如何結合Path繪製出酷炫的UI效果呢?相關係列文件鏈接如下:

一、Path概述

Path是由直線段,二次曲線和三次曲線組成的複合幾何路徑,是一個封裝類可以指導Paint的繪製軌跡和方向。可以使用canvas.drawPath(path,paint)進行填充或描邊繪製(基於Paint的Style),也可以用於剪切或在路徑上繪製文本,簡而言之,Path只是負責具體圖形的輪廓,需要調用Canvas的drawPath方法之後才真正顯示出來

二、貝塞爾曲線的歷史故事

貝塞爾曲線(Bézier curve)又稱貝茲曲線或貝濟埃曲線,是應用於二維圖形應用程序的數學曲線。一般的矢量圖形軟件通過它來精確畫出曲線,貝賽爾曲線由線段與節點組成,其中節點是可拖動的支點,線段像可伸縮的皮筋,(我們在繪圖工具上看到的鋼筆工具就是來做這種矢量曲線的)。貝塞爾曲線是計算機圖形學中相當重要的參數曲線,它是依據四個位置任意的點座標繪製出的一條光滑曲線。貝塞爾曲線的有趣之處更在於它的“皮筋效應”,(即隨着點有規律地移動,曲線將產生皮筋伸引一樣的變換,帶來視覺上的衝擊)。1962年法國數學家Pierre Bézier第一個研究了這種矢量繪製曲線的方法,並給出了詳細的計算公式(德卡斯特里奧算法),因此按照這樣的公式繪製出來的曲線就用他的姓氏來命名是爲貝塞爾曲線。簡而言之,貝塞爾曲線有以下特性:

  • 可精確畫出光滑的曲線
  • 皮筋效應

才被廣泛使用於各種酷炫的UI效果之中,要記住任何曲線都是由一段段線段連接起來的。

三、繪製貝塞爾曲線的原理

貝賽爾曲線的本質是通過數學計算公式去計算得到一系列的點,再把這些點平滑的連接起來,形成一條平滑的曲線,而Bezier曲線是由一系列點來控制曲線狀態的,這些點可分爲兩類:

  • 數據點——用於確定曲線的起始和結束位置
  • 控制點——用於確定曲線的彎曲程度(由公式推導出來)

根據數學公式求出這些控制點再將它們平滑的連接起來就得到貝塞爾曲線。

1、一階(線性)貝塞爾曲線

一階貝塞爾曲線只需要由兩個數據點來確定曲線的起始和結束位置(兩個數據點連接構成一條線段,因此叫一階)就可以繪製出來,比如給定數據點P0、P1,一階貝塞爾曲線只是一條兩點之間的直線,這條線由下式給出:
在這裏插入圖片描述
在這裏插入圖片描述
通過上面的公式根據t的就可以得出對應線段上那個控制點的座標,就能夠實現兩個數據點控制的一條直線的目標。

2、二階貝塞爾曲線

二階貝塞爾曲線則需要由三個數據點來確定(因爲三個不在一直線上的數據點首尾連接可以構成兩條線段,因此叫二階),二階貝塞爾曲線的路徑是由兩個數據點一個控制點去決定的。二階貝塞爾曲線是由一個控制點去控制一條的曲線,而曲線的運動是由兩個線段所控制的,具體繪製二階貝塞爾曲線如下:
在這裏插入圖片描述
再看不懂可以看下網上借來的動圖:
在這裏插入圖片描述

動圖裏的P0、P1、P2分別代表的是上圖的:P0 == A;P1 == B;P2 == C。那麼這個黑色點,代表的就是F點,綠色線段的2個端點(P0-P1線段上的綠色點,代表是就是D點,P0-P2線段上的綠色點,代表是就是E點)。

線段上面點的獲取,必須要滿足等比關係,計算公式如下:
在這裏插入圖片描述

簡而言之,要想控制曲線的彎度,只需要知道兩個數據點(起始點和結束點)和一個控制點

3、三階貝塞爾曲線與N階貝塞爾曲線

其實三階貝塞爾與四階貝賽爾曲線以及N階貝賽爾曲線曲線的規則都是一樣的,都是先在線段上找點,這個點必須要滿足等比關係,然後依次連接,所謂三階貝塞爾曲線,是有三個線段控制的,繪製步驟也大同小異:

在這裏插入圖片描述
同樣看下網上借來的動圖
在這裏插入圖片描述
線段上面點的獲取,必須要滿足等比關係,計算公式如下:
在這裏插入圖片描述
那麼四階貝賽爾曲線的實現步驟也類似,逐層降階,平面上先選取5個點(5點4線)、依次選點(滿足等比關係)、依次連接、根據計算規則找到所有的點
在這裏插入圖片描述
五階貝塞爾曲線:
在這裏插入圖片描述
那麼由點P0、P1、…、Pn所決定的N階貝茲曲線通用公式爲:
在這裏插入圖片描述

四、Path的基礎應用

Path主要就是繪製輪廓的,而輪廓在真正被繪製到Canvas之前,可以進行一系列的運算操作,從而得出各種各樣的輪廓。

1、添加輪廓系方法

Path中類似addArc之類的方法是用於添加指定形狀的輪廓,以下只列出部分方法(重載的不包含)

方法名 說明
void addArc(RectF oval, float startAngle, float sweepAngle) 在當前路徑實例上添加上弧形的輪廓
void addCircle(float x, float y, float radius, Path.Direction dir) 添加一個閉環的圓形,其中Path.Direction表示繪製方向(在進行PathMesure時影響對應的值),逆時針Path.Direction.CCW、 順時針Path.Direction.CW
void addOval(RectF oval, Path.Direction dir) 添加橢圓輪廓
void addPath(Path src, Matrix matrix) 添加指定的Path和利用矩陣進行變化
void addRect(RectF rect, Path.Direction dir) 添加矩形輪廓
void addRoundRect(RectF rect, float rx, float ry, Path.Direction dir) 添加圓矩形輪廓
Path path=new Path();
//添加矩形, 圓角矩形, 橢圓, 圓, 路徑, 圓弧輪廓
path.addCircle(300, 400, 240, Path.Direction.CW);
path.addArc(200, 200, 300, 300, 0, -90);
path.arcTo(200, 200, 300, 300, 0, 90, true);
path.addOval(300, 300, 400, 450, Path.Direction.CW);
path.addRect(100, 400, 300, 500, Path.Direction.CW);
path.addRoundRect(100, 600, 300, 700, 20, 40, Path.Direction.CW);
canvas.drawPath(path, mPaint);

在這裏插入圖片描述

2、輪廓之間進行運算的方法

對輪廓進行邏輯運算,其實和數學中集合的運算類似,Path提供了op方法對兩個Path進行布爾運算(即取交集、並集等操作),進行op方法操作之後,只有調用op方法的路徑纔會改變,而傳入的路徑不受影響。

運算符 說明
Path.Op.DIFFERENCE 減去path1中path1與path2都存在的部分,path1 = (path1 - (path1 ∩ path2))
Path.Op.INTERSECT 保留path1與path2共同的部分,path1 = path1 ∩ path2
Path.Op.UNION 取path1與path2的並集,path1 = path1 ∪ path2
Path.Op.REVERSE_DIFFERENCE 與DIFFERENCE剛好相反,path1 = path2 - (path1 ∩ path2)
Path.Op.XOR 與INTERSECT剛好相反,path1 = (path1 ∪ path2) - (path1 ∩ path2)
public class PathView extends View {
    private Path mPath1 = new Path();
    private Path mPath2 = new Path();
    private Paint mPaint = new Paint();
    public PathView(Context context) {
        super(context);
        mPaint.setColor(Color.GREEN);
        mPaint.setStrokeWidth(20);
        mPaint.setStyle(Paint.Style.STROKE);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mPath1.addCircle(200, 200, 100, Path.Direction.CW); //繪製圓
        mPath2.addCircle(300, 300, 100, Path.Direction.CW);
         //mPath1.op(mPath2,Path.Op.DIFFERENCE);
        // mPath1.op(mPath2,Path.Op.INTERSECT);
        // mPath1.op(mPath2,Path.Op.UNION);
        // mPath1.op(mPath2,Path.Op.XOR);
        mPath1.op(mPath2, Path.Op.REVERSE_DIFFERENCE);
        canvas.drawPath(mPath1, mPaint);
        mPaint.setColor(Color.RED);
        mPaint.setStrokeWidth(4);
        canvas.drawPath(mPath2, mPaint);
    }
}

在這裏插入圖片描述

3、路徑上的移動連線和閉合

運算符 說明
void moveTo(float x, float y) 將路徑的繪製位置的起點定在(x,y)的位置(默認的起點位於屏幕的左上角)
void rMoveTo(float dx, float dy) 在前一個點的基礎上開始繪製,如果前面一個點是(x,y)rMoveTo(dx,dy)相當於moveTo(x+dx,y+dy),如果前面沒有調用moveTo,相當於從(dx,dy)開始繪製
void lineTo(float x, float y) 相當於是設置了終點位置,把起點和終點用直線連接起來
void rLineTo(float x, float y) 與rMoveTo類似,升級版的lineTo
void close() 使得路徑閉環

在這裏插入圖片描述

4、繪製2階、3階貝塞爾曲線

Path的系統API只提供了繪製2階和3階貝塞爾曲線的功能,要想繪製多階只能自己去實現。

  • quadTo(float x1, float y1, float x2, float y2) 繪製二階貝塞爾曲線,如果沒有執行moveTo方法設置起點,則(x1,y1)變爲控制點,起始點爲(0,0);而執行了moveTo之後起始點爲moveTo的座標,控制點爲(x1,y1),終點都爲(x2,y2)。
    在這裏插入圖片描述

其中的橙色線段和座標都是我自己手工繪製的,代碼只是繪製了綠色部分的。

  • void rQuadTo(float dx1, float dy1, float dx2, float dy2)

升級版的quadTo與上面的rMoveTo類似。

mPath1.moveTo(100, 100);
mPath1.quadTo(400, 200, 50, 500);//二階貝塞爾曲線
canvas.drawCircle(100, 100, 10, mPaint);
canvas.drawCircle(400, 200, 10, mPaint);
canvas.drawCircle(50, 500, 10, mPaint);
canvas.drawPath(mPath1, mPaint);
canvas.save();
mPaint.setStrokeWidth(4);
mPaint.setColor(Color.RED);
canvas.restore();
mPaint.setColor(Color.RED);
//rQuadTo方法是基於當前點座標系(偏移量)
mPath1.rQuadTo(300, 100, -90, 400);
canvas.drawPath(mPath1, mPaint);

在這裏插入圖片描述

  • cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 階繪製3階貝塞爾
mPath1.moveTo(100, 100);
mPath1.cubicTo(400, 200,10, 500,300, 700);
canvas.drawCircle(100, 100, 10, mPaint);
canvas.drawCircle(400, 200, 10, mPaint);
canvas.drawCircle(10, 500, 10, mPaint);
canvas.drawCircle(300, 700, 10, mPaint);
canvas.drawPath(mPath1, mPaint);

在這裏插入圖片描述

發佈了242 篇原創文章 · 獲贊 136 · 訪問量 54萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章