標籤:
Path之完結篇(偽)作者微博: @GcsSloop【本系列相關文章】
經曆過前兩篇 Path之基本操作 和 Path之貝茲路徑 的講解,本篇終於進入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不保留內部資料結構,但會保留FillType. rewind會保留內部的資料結構,但不保留FillType |
| 矩陣操作 |
transform |
矩陣變換 |
二、Path方法詳解rXxx方法
此類方法可以看到和前面的一些方法看起來很像,只是在前面多了一個r,那麼這個rXxx和前面的一些方法有什麼區別呢?
rXxx方法的座標使用的是相對位置(基於當前點的位移),而之前方法的座標是絕對位置(基於當前座標系的座標)。
舉個例子:
Path path = new Path(); path.moveTo(100,100); path.lineTo(100,200); canvas.drawPath(path,mDeafultPaint);
在這個例子中,先移動點到座標(100,100)處,之後再串連 點(100,100) 到 (100,200) 之間點直線,非常簡單,畫出來就是一條豎直的線,那接下來看下一個例子:
Path path = new Path(); path.moveTo(100,100); path.rLineTo(100,200); canvas.drawPath(path,mDeafultPaint);
這個例子中,將 lineTo 換成了 rLineTo 可以看到在螢幕上原本是豎直的線變成了傾斜的線。這是因為最終我們串連的是 (100,100) 和 (200, 300) 之間的線段。
在使用rLineTo之前,當前點的位置在 (100,100) , 使用了 rLineTo(100,200) 之後,下一個點的位置是在當前點的基礎上加上位移量得到的,即 (100+100, 100+200) 這個位置,故最終結果如上所示。
PS: 此處僅以 rLineTo 為例,只要理解 “絕對座標” 和 “相對座標” 的區別,其他方法類比即可。
填充模式
我們在之前的文章中瞭解到,Paint有三種樣式,“描邊” “填充” 以及 “描邊加填充”,我們這裡所瞭解到就是在Paint設定為後兩種樣式時不同的填充模式對圖形渲染效果的影響。
我們要給一個圖形內部填充顏色,首先需要分清哪一部分是外部,哪一部分是內部,機器不像我們人那麼聰明,機器是如何判斷內外呢?
機器判斷圖形內外,一般有以下兩種方法:
PS:此處所有的圖形均為封閉圖形,不包括圖形不封閉這種情況。
| 方法 |
判定條件 |
解釋 |
| 奇偶規則 |
奇數表示在圖形內,偶數表示在圖形外 |
從任意位置p作一條射線, 若與該射線相交的圖形邊的數目為奇數,則p是圖形內部點,否則是外部點。 |
| 非零環繞數規則 |
若環繞數為0表示在圖形內,非零表示在圖形外 |
首先使圖形的邊變為向量。將環繞數初始化為零。再從任意位置p作一條射線。當從p點沿射線方向移動時,對在每個方向上穿過射線的邊計數,每當圖形的邊從右至左穿過射線時,環繞數加1,從左至右時,環繞數減1。處理完圖形的所有相關邊之後,若環繞數為非零,則p為內部點,否則,p是外部點。 |
接下來我們先瞭解一下兩種判斷方法是如何工作的。
奇偶規則(Even-Odd Rule)
這一個比較簡單,也容易理解,直接用一個簡單樣本來說明。
在中有一個四邊形,我們選取了三個點來判斷這些點是否在圖形內部。
>
P1: 從P1發出一條射線,發現圖形與該射線相交邊數為0,偶數,故P1點在圖形外部。
P2: 從P2發出一條射線,發現圖形與該射線相交邊數為1,奇數,故P2點在圖形內部。
P3: 從P3發出一條射線,發現圖形與該射線相交邊數為2,偶數,故P3點在圖形外部。
非零環繞數規則(Non-Zero Winding Number Rule)
非零環繞數規則相對來說比較難以理解一點。
我們在之前的文章 Path之基本操作 中我們瞭解到,在給Path中添加圖形時需要指定圖形的添加方式,是用順時針還是逆時針,另外我們不論是使用lineTo,quadTo,cubicTo還是其他連接線的方法,都是從一個點串連到另一個點,換言之,Path中任何線段都是有方向性的,這也是使用非零環繞數規則的基礎。
我們依舊用一個簡單的例子來說明非零環繞數規則的用法:
PS: 注意圖形中線段的方向性!
>
P1: 從P1點發出一條射線,沿射線防線移動,並沒有與邊相交點部分,環繞數為0,故P1在圖形外邊。
P2: 從P2點發出一條射線,沿射線方向移動,與圖形點左側邊相交,該邊從左至右穿過穿過射線,環繞數-1,最終環繞數為-1,故P2在圖形內部。
P3: 從P3點發出一條射線,沿射線方向移動,在第一個交點處,底邊從右至左穿過射線,環繞數+1,在第二個交點處,右側邊從左至右穿過射線,環繞數-1,最終環繞數為0,故P3在圖形外部。
通常,這兩種方法的判斷結果是相同的,但也存在兩種方法判斷結果不同的情況,如下面這種情況:
注意圖形線段的方向,就不詳細解釋了,用上面的方法進行判斷即可。
自相交圖形
自相交圖形定義:多邊形在平面內除頂點外還有其他公用點。
簡單的提一下自相交圖形,瞭解概念即可,就是一個簡單的自相交圖形:
Android中的填充模式
Android中的填充模式有四種,是封裝在Path中的一個枚舉。
| 模式 |
簡介 |
| EVEN_ODD |
奇偶規則 |
| INVERSE_EVEN_ODD |
反奇偶規則 |
| WINDING |
非零環繞數規則 |
| INVERSE_WINDING |
反非零環繞數規則 |
我們可以看到上面有四種模式,分成兩對,例如 “奇偶規則” 與 “反奇偶規則” 是一對,它們之間有什麼關係呢?
Inverse 和含義是“相反,對立”,說明反奇偶規則剛好與奇偶規則相反,例如對於一個矩形而言,使用奇偶規則會填充矩形內部,而使用反奇偶規則會填充矩形外部,這個會在後面樣本中代碼展示兩者對區別。
Android與填充模式相關的方法
這些都是Path中的方法。
| 方法 |
作用 |
| setFillType |
設定填充規則 |
| getFillType |
擷取當前填充規則 |
| isInverseFillType |
判斷是否是反向(INVERSE)規則 |
| toggleInverseFillType |
切換填充規則(即原有規則與反向規則之間相互切換) |
樣本示範:
本示範著重於協助理解填充模式中的一些痛點和易混淆的問題,對於一些比較簡單的問題,讀者可自行驗證,本文中不會過多贅述。
奇偶規則與反奇偶規則
mDeafultPaint.setStyle(Paint.Style.FILL); // 設定畫布模式為填充 canvas.translate(mViewWidth / 2, mViewHeight / 2); // 移動畫布(座標系) Path path = new Path(); // 建立Path //path.setFillType(Path.FillType.EVEN_ODD); // 設定Path填充模式為 奇偶規則 path.setFillType(Path.FillType.INVERSE_WINDING); // 反奇偶規則 path.addRect(-200,-200,200,200, Path.Direction.CW); // 給Path中添加一個矩形
下面兩張圖片分別是在奇偶規則於反奇偶規則的情況下繪製的結果,可以看出其填充的地區剛好相反:
PS: 白色為背景色,黑色為填充色。
圖形邊的方向對非零奇偶環繞數規則填充結果的影響
我們之前討論過給Path添加圖形時順時針與逆時針的作用,除了上次講述的方便記錄外,就是本文所涉及的另外一個重要作用了: “作為非零環繞數規則的判斷依據。”
通過前面我們已經大致瞭解了在圖形邊的方向會如何影響到填充效果,我們這裡驗證一下:
mDeafultPaint.setStyle(Paint.Style.FILL); // 設定畫筆模式為填充 canvas.translate(mViewWidth / 2, mViewHeight / 2); // 移動畫布(坐系) Path path = new Path(); // 建立Path // 添加小正方形 (通過這兩行代碼來控制小正方形邊的方向,從而示範不同的效果) // path.addRect(-200, -200, 200, 200, Path.Direction.CW); path.addRect(-200, -200, 200, 200, Path.Direction.CCW); // 添加大正方形 path.addRect(-400, -400, 400, 400, Path.Direction.CCW); path.setFillType(Path.FillType.WINDING); // 設定Path填充模式為非零環繞規則 canvas.drawPath(path, mDeafultPaint); // 繪製Path
布爾操作(API19)
布爾操作與我們中學所學的集合操作非常像,只要知道集合操作中等交集,並集,差集等操作,那麼理解布爾操作也是很容易的。
布爾操作是兩個Path之間的運算,主要作用是用一些簡單的圖形通過一些規則合成一些相對比較複雜,或難以直接得到的圖形。
如太極中的陰陽魚,如果用貝茲路徑製作的話,可能需要六段貝茲路徑才行,而在這裡我們可以用四個Path通過布爾運算得到,而且會相對來說更容易理解一點。
canvas.translate(mViewWidth / 2, mViewHeight / 2); Path path1 = new Path(); Path path2 = new Path(); Path path3 = new Path(); Path path4 = new Path(); path1.addCircle(0, 0, 200, Path.Direction.CW); path2.addRect(0, -200, 200, 200, Path.Direction.CW); path3.addCircle(0, -100, 100, Path.Direction.CW); path4.addCircle(0, 100, 100, Path.Direction.CCW); path1.op(path2, Path.Op.DIFFERENCE); path1.op(path3, Path.Op.UNION); path1.op(path4, Path.Op.DIFFERENCE); canvas.drawPath(path1, mDeafultPaint);
前面示範了布爾運算的作用,接下來我們瞭解一下布爾運算的核心:布爾邏輯。
Path的布爾運算有五種邏輯,如下:
| 邏輯名稱 |
類比 |
說明 |
|
| DIFFERENCE |
差集 |
Path1中減去Path2後剩下的部分 |
|
| REVERSE_DIFFERENCE |
差集 |
Path2中減去Path1後剩下的部分 |
|
| INTERSECT |
交集 |
Path1與Path2相交的部分 |
|
| UNION |
並集 |
包含全部Path1和Path2 |
|
| XOR |
異或 |
包含Path1與Path2但不包括兩者相交的部分 |
|
布爾運算方法
通過前面到理論知識鋪墊,相信大家對布爾運算已經有了基本的認識和理解,下面我們用代碼示範一下布爾運算:
在Path中的布爾運算有兩個方法
boolean op (Path path, Path.Op op) boolean op (Path path1, Path path2, Path.Op op)
兩個方法中的傳回值用於判斷布爾運算是否成功,它們使用方法如下:
``java
// 對 path1 和 path2 執行布爾運算,運算方式由第二個參數指定,運算結果存入到path1中。
path1.op(path2, Path.Op.DIFFERENCE);
// 對 path1 和 path2 執行布爾運算,運算方式由第三個參數指定,運算結果存入到path3中。path3.op(path1, path2, Path.Op.DIFFERENCE)
#### 布爾運算樣本代碼:``` java int x = 80; int r = 100; canvas.translate(250,0); Path path1 = new Path(); Path path2 = new Path(); Path pathOpResult = new Path(); path1.addCircle(-x, 0, r, Path.Direction.CW); path2.addCircle(x, 0, r, Path.Direction.CW); pathOpResult.op(path1,path2, Path.Op.DIFFERENCE); canvas.translate(0, 200); canvas.drawText("DIFFERENCE", 240,0,mDeafultPaint); canvas.drawPath(pathOpResult,mDeafultPaint); pathOpResult.op(path1,path2, Path.Op.REVERSE_DIFFERENCE); canvas.translate(0, 300); canvas.drawText("REVERSE_DIFFERENCE", 240,0,mDeafultPaint); canvas.drawPath(pathOpResult,mDeafultPaint); pathOpResult.op(path1,path2, Path.Op.INTERSECT); canvas.translate(0, 300); canvas.drawText("INTERSECT", 240,0,mDeafultPaint); canvas.drawPath(pathOpResult,mDeafultPaint); pathOpResult.op(path1,path2, Path.Op.UNION); canvas.translate(0, 300); canvas.drawText("UNION", 240,0,mDeafultPaint); canvas.drawPath(pathOpResult,mDeafultPaint); pathOpResult.op(path1,path2, Path.Op.XOR); canvas.translate(0, 300); canvas.drawText("XOR", 240,0,mDeafultPaint); canvas.drawPath(pathOpResult,mDeafultPaint);<div class="se-preview-section-delimiter"></div>
計算邊界
這個方法主要作用是計算Path所佔用的空間以及所在位置,方法如下:
void computeBounds (RectF bounds, boolean exact)<div class="se-preview-section-delimiter"></div>
它有兩個參數:
| 參數 |
作用 |
| bounds |
測量結果會放入這個矩形 |
| exact |
是否精確測量,目前這一個參數作用已經廢棄,一般寫true即可。 |
關於exact如有疑問可參見Google官方的提交記錄Path.computeBounds()
計算邊界樣本
計算path邊界的一個簡單樣本.
代碼:
// 移動canvas,mViewWidth與mViewHeight在 onSizeChanged 方法中獲得 canvas.translate(mViewWidth/2,mViewHeight/2); RectF rect1 = new RectF(); // 存放測量結果的矩形 Path path = new Path(); // 建立Path並添加一些內容 path.lineTo(100,-50); path.lineTo(100,50); path.close(); path.addCircle(-100,0,100, Path.Direction.CW); path.computeBounds(rect1,true); // 測量Path canvas.drawPath(path,mDeafultPaint); // 繪製Path mDeafultPaint.setStyle(Paint.Style.STROKE); mDeafultPaint.setColor(Color.RED); canvas.drawRect(rect1,mDeafultPaint); // 繪製邊界
重設路徑
重設Path有兩個方法,分別是reset和rewind,兩者區別主要有一下兩點:
| 方法 |
是否保留FillType設定 |
是否保留原有資料結構 |
| reset |
是 |
否 |
| rewind |
否 |
是 |
這個兩個方法應該何時選擇呢?
選擇權重: FillType > 資料結構
因為“FillType”影響的是顯示效果,而“資料結構”影響的是重建速度。
總結
Path中常用的方法到此已經結束,希望能夠協助大家加深對Path對理解運用,讓大家能夠用Path愉快的玩耍。( ̄▽ ̄)
(,,? ? ?,,)
PS: 由於本人水平有限,某些地方可能存在誤解或不準確,如果你對此有疑問可以提交Issues進行反饋。About Me作者微博: @GcsSloop
參考資料
Path
維基百科-Nonzero-rule
android繪圖之Path總結
布爾邏輯
GoogleCode-Path.computeBounds()
Path.reset vs Path.rewind
安卓自訂View進階 - Path之完結篇(偽)