自定義控件系列的讀書筆記,整理自下列資料,不代表博主個人觀點 :GcsSloop/AndroidNote
六、Path基本操作
6.1 Path常用方法表
不包括 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 | 矩陣變換 |
6.2 Path詳解
請關閉硬件加速,以免引起不必要的問題!
6.2.1 Path作用
Path在2D繪圖中是一個很重要的東西,使用Path不僅能夠繪制簡單圖形,也可以繪制這些比較復雜的圖形。另外,根據路徑繪制文本和剪裁畫布都會用到Path。
6.2.2 Path含義
Path封裝了由直線和曲線(二次,三次貝塞爾曲線)構成的幾何路徑。你能用Canvas中的drawPath來把這條路徑畫出來(同樣支持Paint的不同繪制模式),也可以用于剪裁畫布和根據路徑繪制文字。我們有時會用Path來描述一個圖像的輪廓,所以也會稱為輪廓線(輪廓線僅是Path的一種使用方法,兩者并不等價)
另外路徑有開放和封閉的區別。
圖像 | 名稱 | 備注 |
---|---|---|
封閉路徑 | 首尾相接形成了一個封閉區域 | |
開放路徑 | 沒有首位相接形成封閉區域 |
6.2.3 Path使用方法詳解
(1)第1組:moveTo、 setLastPoint、 lineTo 和 close
先創建一個通用的畫筆:
Paint mPaint = new Paint(); // 創建畫筆
mPaint.setColor(Color.BLACK); // 畫筆顏色 - 黑色
mPaint.setStyle(Paint.Style.STROKE); // 填充模式 - 描邊
mPaint.setStrokeWidth(10); // 邊框寬度 - 10
lineTo:
public void lineTo (float x, float y)
lineTo是指從某個點到參數坐標點之間連一條線,這里的某個點就是上次操作結束的點,如果沒有進行過操作則默認點為坐標原點:
canvas.translate(mWidth / 2, mHeight / 2); // 移動坐標系到屏幕中心(寬高數據在onSizeChanged中獲取)
Path path = new Path(); // 創建Path
path.lineTo(200, 200); // lineTo
path.lineTo(200,0);
canvas.drawPath(path, mPaint); // 繪制Path
在示例中我們調用了兩次lineTo,第一次由于之前沒有過操作,所以默認點就是坐標原點O,結果就是坐標原點O到A(200,200)之間連直線(用藍色圈1標注)。
第二次lineTo的時候,由于上次的結束位置是A(200,200),所以就是A(200,200)到B(200,0)之間的連線(用藍色圈2標注)。
moveTo 和 setLastPoint:
// moveTo
public void moveTo (float x, float y)
// setLastPoint
public void setLastPoint (float dx, float dy)
方法名 | 簡介 | 是否影響之前的操作 | 是否影響之后操作 |
---|---|---|---|
moveTo | 移動下一次操作的起點位置 | 否 | 是 |
setLastPoint | 設置之前操作的最后一個點位置 | 是 | 是 |
moveTo示例代碼:
canvas.translate(mWidth / 2, mHeight / 2); // 移動坐標系到屏幕中心
Path path = new Path(); // 創建Path
path.lineTo(200, 200); // lineTo
path.moveTo(200,100); // moveTo
path.lineTo(200,0); // lineTo
canvas.drawPath(path, mPaint); // 繪制Path
moveTo只改變下次操作的起點,在執行完第一次LineTo的時候,本來的默認點位置是A(200,200),但是moveTo將其改變成為了C(200,100),所以在第二次調用lineTo的時候就是連接C(200,100) 到 B(200,0) 之間的直線(用藍色圈2標注)。
下面是setLastPoint的示例:
canvas.translate(mWidth / 2, mHeight / 2); // 移動坐標系到屏幕中心
Path path = new Path(); // 創建Path
path.lineTo(200, 200); // lineTo
path.setLastPoint(200,100); // setLastPoint
path.lineTo(200,0); // lineTo
canvas.drawPath(path, mPaint); // 繪制Path
setLastPoint是重置上一次操作的最后一個點,在執行完第一次的lineTo的時候,最后一個點是A(200,200),而setLastPoint更改最后一個點為C(200,100),所以在實際執行的時候,第一次的lineTo就不是從原點O到A(200,200)的連線了,而變成了從原點O到C(200,100)之間的連線了。
在執行完第一次lineTo和setLastPoint后,最后一個點的位置是C(200,100),所以在第二次調用lineTo的時候就是C(200,100) 到 B(200,0) 之間的連線(用藍色圈2標注)。
close:
public void close ()
close方法用于連接當前最后一個點和最初的一個點(如果兩個點不重合的話),最終形成一個封閉的圖形。
canvas.translate(mWidth / 2, mHeight / 2); // 移動坐標系到屏幕中心
Path path = new Path(); // 創建Path
path.lineTo(200, 200); // lineTo
path.lineTo(200,0); // lineTo
path.close(); // close
canvas.drawPath(path, mPaint); // 繪制Path
很明顯,兩個lineTo分別代表第1和第2條線,而close在此處的作用就算連接了B(200,0)點和原點O之間的第3條線,使之形成一個封閉的圖形。
注意:close的作用是封閉路徑,與連接當前最后一個點和第一個點并不等價。如果連接了最后一個點和第一個點仍然無法形成封閉圖形,則close什么也不做。
(2)第2組:addXxx與arcTo
這次內容主要是在Path中添加基本圖形,重點區分addArc與arcTo。
第一類(基本形狀)
// 圓形
public void addCircle (float x, float y, float radius, Path.Direction dir)
// 橢圓
public void addOval (RectF oval, Path.Direction dir)
// 矩形
public void addRect (float left, float top, float right, float bottom, Path.Direction dir)
public void addRect (RectF rect, Path.Direction dir)
// 圓角矩形
public void addRoundRect (RectF rect, float[] radii, Path.Direction dir)
public void addRoundRect (RectF rect, float rx, float ry, Path.Direction dir)
這一類就是在path中添加一個基本形狀,基本形狀部分和前面所講的繪制基本形狀并無太大差別。
仔細觀察一下第一類的方法,無一例外,在最后都有一個Path.Direction。Direction的意思是方向,趨勢,是一個枚舉(Enum)類型,里面只有兩個枚舉常量,如下:
類型 | 解釋 | 翻譯 |
---|---|---|
CW | clockwise | 順時針 |
CCW | counter-clockwise | 逆時針 |
它們的作用如下有:一是在添加圖形時確定閉合順序(各個點的記錄順序);二是對圖形的渲染結果有影響(是判斷圖形渲染的重要條件) 。
先研究確定閉合順序的問題,添加一個矩形:
canvas.translate(mWidth / 2, mHeight / 2); // 移動坐標系到屏幕中心
Path path = new Path();
path.addRect(-200,-200,200,200, Path.Direction.CW);
canvas.drawPath(path,mPaint);
將上面代碼的CW改為CCW再運行一次,會發現兩次運行結果一模一樣!
想要讓它現出原形,就要用到剛剛學到的setLastPoint(重置當前最后一個點的位置)。
canvas.translate(mWidth / 2, mHeight / 2); // 移動坐標系到屏幕中心
Path path = new Path();
path.addRect(-200,-200,200,200, Path.Direction.CW);
path.setLastPoint(-300,300); // <-- 重置最后一個點的位置
canvas.drawPath(path,mPaint);
Path是使用四個點來記錄矩形,對于上面這個矩形來說,采用的是順時針(CW),所以記錄的點的順序是 A -> B -> C -> D. 最后一個點就是D,我們這里使用setLastPoint改變最后一個點的位置實際上是改變了D的位置。
理解了上面的原理之后,假設我們將順時針改為逆時針(CCW),則記錄點的順序應該就是 A - D -> C -> B, 再使用setLastPoint則改變的是B的位置,如下:
canvas.translate(mWidth / 2, mHeight / 2); // 移動坐標系到屏幕中心
Path path = new Path();
path.addRect(-200,-200,200,200, Path.Direction.CCW);
path.setLastPoint(-300,300); // <-- 重置最后一個點的位置
canvas.drawPath(path,mPaint);
參數中點的順序很重要!
第二類(Path)
// path
public void addPath (Path src)
public void addPath (Path src, float dx, float dy)
public void addPath (Path src, Matrix matrix)
這個相對比較簡單,也很容易理解,就是將兩個Path合并成為一個。
第三個方法是將src添加到當前path之前先使用Matrix進行變換。
第二個方法比第一個方法多出來的兩個參數是將src進行了位移之后再添加進當前path中。
示例:
canvas.translate(mWidth / 2, mHeight / 2); // 移動坐標系到屏幕中心
canvas.scale(1,-1); // <-- 注意 翻轉y坐標軸
Path path = new Path();
Path src = new Path();
path.addRect(-200,-200,200,200, Path.Direction.CW);
src.addCircle(0,0,100, Path.Direction.CW);
path.addPath(src,0,200);
mPaint.setColor(Color.BLACK); // 繪制合并后的路徑
canvas.drawPath(path,mPaint);
首先我們新建的兩個Path(矩形和圓形)中心都是坐標原點,我們在將包含圓形的path添加到包含矩形的path之前將其進行移動了一段距離,最終繪制出來的效果就如上面所示。
第三類(addArc與arcTo)
// addArc
public void addArc (RectF oval, float startAngle, float sweepAngle)
// arcTo
public void arcTo (RectF oval, float startAngle, float sweepAngle)
public void arcTo (RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo)
從名字就可以看出,這兩個方法都是與圓弧相關的,作用都是添加一個圓弧到path中,但既然存在兩個方法,兩者之間肯定是有區別的:
名稱 | 作用 | 區別 |
---|---|---|
addArc | 添加一個圓弧到path | 直接添加一個圓弧到path中 |
arcTo | 添加一個圓弧到path | 添加一個圓弧到path,如果圓弧的起點和上次最后一個坐標點不相同,就連接兩個點 |
可以看到addArc有1個方法(實際上是兩個的,但另一個重載方法是API21添加的), 而arcTo有2個方法,其中一個最后多了一個布爾類型的變量forceMoveTo。
forceMoveTo是什么作用呢?
這個變量意思為“是否強制使用moveTo”,也就是說,是否使用moveTo將變量移動到圓弧的起點位移,也就意味著:
forceMoveTo | 含義 | 等價方法 |
---|---|---|
true | 將最后一個點移動到圓弧起點,再開始繪制圓弧,即不連接最后一個點與圓弧起點 | public void addArc (RectF oval, float startAngle, float sweepAngle) |
false | 直接連接最后一個點與圓弧起點,然后開始繪制圓弧 | public void arcTo (RectF oval, float startAngle, float sweepAngle) |
示例(addArc):
canvas.translate(mWidth / 2, mHeight / 2); // 移動坐標系到屏幕中心
canvas.scale(1,-1); // <-- 注意 翻轉y坐標軸
Path path = new Path();
path.lineTo(100,100);
RectF oval = new RectF(0,0,300,300);
path.addArc(oval,0,270);
// path.arcTo(oval,0,270,true); // <-- 和上面一句作用等價
canvas.drawPath(path,mPaint);
示例(arcTo):
canvas.translate(mWidth / 2, mHeight / 2); // 移動坐標系到屏幕中心
canvas.scale(1,-1); // <-- 注意 翻轉y坐標軸
Path path = new Path();
path.lineTo(100,100);
RectF oval = new RectF(0,0,300,300);
path.arcTo(oval,0,270);
// path.arcTo(oval,0,270,false); // <-- 和上面一句作用等價
canvas.drawPath(path,mPaint);
(3)第3組:isEmpty、 isRect、isConvex、 set 和 offset
isEmpty:
public boolean isEmpty ()
判斷path中是否包含內容。
Path path = new Path();
Log.e("1",path.isEmpty()+"");
path.lineTo(100,100);
Log.e("2",path.isEmpty()+"");
log輸出結果:
03-02 14:22:54.770 12379-12379/com.sloop.canvas E/1: true
03-02 14:22:54.770 12379-12379/com.sloop.canvas E/2: false
isRect:
public boolean isRect (RectF rect)
判斷path是否是一個矩形,如果是一個矩形的話,會將矩形的信息存放進參數rect中。
path.lineTo(0,400);
path.lineTo(400,400);
path.lineTo(400,0);
path.lineTo(0,0);
RectF rect = new RectF();
boolean b = path.isRect(rect);
Log.e("Rect","isRect:"+b+"| left:"+rect.left+"| top:"+rect.top+"| right:"+rect.right+"| bottom:"+rect.bottom);
log 輸出結果:
03-02 16:48:39.669 24179-24179/com.sloop.canvas E/Rect: isRect:true| left:0.0| top:0.0| right:400.0| bottom:400.0
set:
public void set (Path src)
將新的path賦值到現有path。
canvas.translate(mWidth / 2, mHeight / 2); // 移動坐標系到屏幕中心
canvas.scale(1,-1); // <-- 注意 翻轉y坐標軸
Path path = new Path(); // path添加一個矩形
path.addRect(-200,-200,200,200, Path.Direction.CW);
Path src = new Path(); // src添加一個圓
src.addCircle(0,0,100, Path.Direction.CW);
path.set(src); // 大致相當于 path = src;
canvas.drawPath(path,mPaint);
offset:
public void offset (float dx, float dy)
public void offset (float dx, float dy, Path dst)
對path進行一段平移,和Canvas中的translate作用很像,但Canvas作用于整個畫布,而path的offset只作用于當前path。
第二個方法中最后的參數dst是存儲平移后的path的。
dst狀態 | 效果 |
---|---|
dst不為空 | 將當前path平移后的狀態存入dst中,不會影響當前path |
dst為空(null) | 平移將作用于當前path,相當于第一種方法 |
示例:
canvas.translate(mWidth / 2, mHeight / 2); // 移動坐標系到屏幕中心
canvas.scale(1,-1); // <-- 注意 翻轉y坐標軸
Path path = new Path(); // path中添加一個圓形(圓心在坐標原點)
path.addCircle(0,0,100, Path.Direction.CW);
Path dst = new Path(); // dst中添加一個矩形
dst.addRect(-200,-200,200,200, Path.Direction.CW);
path.offset(300,0,dst); // 平移
canvas.drawPath(path,mPaint); // 繪制path
mPaint.setColor(Color.BLUE); // 更改畫筆顏色
canvas.drawPath(dst,mPaint); // 繪制dst
從運行效果圖可以看出,雖然我們在dst中添加了一個矩形,但是并沒有表現出來,所以,當dst中存在內容時,dst中原有的內容會被清空,而存放平移后的path。
(4)rXxx方法
此類方法可以看到和前面的方法看起來很像,只是在前面多了一個r,r代表的是相對坐標。rXxx方法的坐標使用的是相對位置(基于當前點的位移),而之前方法的坐標是絕對位置(基于當前坐標系的坐標)。
舉個例子:
Path path = new Path();
path.moveTo(100,100);
path.lineTo(100,200);
canvas.drawPath(path,mDeafultPaint);
改成r方法后:
Path path = new Path();
path.moveTo(100,100);
path.rLineTo(100,200);
canvas.drawPath(path,mDeafultPaint);
6.3 PathMeasure
常用方法如下:
返回值 | 方法名 | 釋義 |
---|---|---|
void | setPath(Path path, boolean forceClosed) | 關聯一個Path |
boolean | isClosed() | 是否閉合 |
float | getLength() | 獲取Path的長度 |
boolean | nextContour() | 跳轉到下一個輪廓 |
boolean | getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) | 截取片段 |
boolean | getPosTan(float distance, float[] pos, float[] tan) | 獲取指定長度的位置坐標及該點切線值 |
boolean | getMatrix(float distance, Matrix matrix, int flags) | 獲取指定長度的位置坐標及該點Matrix |
6.3.1 構造函數
(1)無參構造函數
PathMeasure ()
用這個構造函數可創建一個空的 PathMeasure,但是使用之前需要先調用 setPath 方法來與 Path 進行關聯。被關聯的 Path 必須是已經創建好的,如果關聯之后 Path 內容進行了更改,則需要使用 setPath 方法重新關聯。
(2)有參構造函數
PathMeasure (Path path, boolean forceClosed)
用這個構造函數是創建一個 PathMeasure 并關聯一個 Path, 其實和創建一個空的 PathMeasure 后調用 setPath 進行關聯效果是一樣的,同樣,被關聯的 Path 也必須是已經創建好的,如果關聯之后 Path 內容進行了更改,則需要使用 setPath 方法重新關聯。
該方法有兩個參數,第二個參數是用來確保 Path 閉合,如果設置為 true, 則不論之前Path是否閉合,都會自動閉合該 Path(如果Path可以閉合的話)。
在這里有兩點需要明確:
- 1、不論 forceClosed 設置為何種狀態(true 或者 false), 都不會影響原有Path的狀態,即 Path 與 PathMeasure 關聯之后,之前的的 Path 不會有任何改變。
- 2、forceClosed 的設置狀態可能會影響測量結果,如果 Path 未閉合但在與 PathMeasure 關聯的時候設置 forceClosed 為 true 時,測量結果可能會比 Path 實際長度稍長一點,獲取到到是該 Path 閉合時的狀態。
用一個例子來驗證一下:
canvas.translate(mViewWidth/2,mViewHeight/2);
Path path = new Path();
path.lineTo(0,200);
path.lineTo(200,200);
path.lineTo(200,0);
PathMeasure measure1 = new PathMeasure(path,false);
PathMeasure measure2 = new PathMeasure(path,true);
Log.e("TAG", "forceClosed=false---->"+measure1.getLength());
Log.e("TAG", "forceClosed=true----->"+measure2.getLength());
canvas.drawPath(path,mDeafultPaint);
log如下:
forceClosed=false---->600.0
forceClosed=true----->800.0
繪制在界面上的效果如下:
通過上面的示例能驗證以上兩個問題,另外還有:
- 1、將 Path 與兩個的 PathMeasure 進行關聯,并給 forceClosed 設置了不同的狀態,之后繪制再繪制出來的 Path 沒有任何變化,所以與 Path 與 PathMeasure進行關聯并不會影響 Path 狀態。
- 2、可以看到,設置 forceClosed 為 true 的方法比設置為 false 的方法測量出來的長度要長一點,這是由于 Path 沒有閉合的緣故,多出來的距離正是 Path 最后一個點與最開始一個點之間點距離。forceClosed 為 false 測量的是當前 Path 狀態的長度, forceClosed 為 true,則不論Path是否閉合測量的都是 Path 的閉合長度。
6.3.2 setPath、 isClosed 和 getLength
setPath 是 PathMeasure 與 Path 關聯的重要方法,效果和 構造函數 中兩個參數的作用是一樣的。
isClosed 用于判斷 Path 是否閉合,但是如果你在關聯 Path 的時候設置 forceClosed 為 true 的話,這個方法的返回值則一定為true。
getLength 用于獲取 Path 的總長度,在之前的測試中已經用過了。
6.3.3 getSegment
getSegment 用于獲取Path的一個片段,方法如下:
boolean getSegment (float startD, float stopD, Path dst, boolean startWithMoveTo)
方法各參數釋義:
參數 | 作用 | 備注 |
---|---|---|
返回值(boolean) | 判斷截取是否成功 | true 表示截取成功,結果存入dst中,false 截取失敗,不會改變dst中內容 |
startD | 開始截取位置距離 Path 起點的長度 | 取值范圍: 0 <= startD < stopD <= Path總長度 |
stopD | 結束截取位置距離 Path 起點的長度 | 取值范圍: 0 <= startD < stopD <= Path總長度 |
dst | 截取的 Path 將會添加到 dst 中 | 注意: 是添加,而不是替換 |
startWithMoveTo | 起始點是否使用 moveTo | 用于保證截取的 Path 第一個點位置不變 |
- 如果 startD、stopD 的數值不在取值范圍 [0, getLength] 內,或者 startD == stopD 則返回值為 false,不會改變 dst 內容。
- 如果在安卓4.4或者之前的版本,在默認開啟硬件加速的情況下,更改 dst 的內容后可能繪制會出現問題,請關閉硬件加速或者給 dst 添加一個單個操作,例如: dst.rLineTo(0, 0)
看看這個方法如何使用:
創建了一個 Path, 并在其中添加了一個矩形,現在想截取矩形中的一部分,就是下圖中紅色的部分。
代碼如下:
canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐標系
Path path = new Path(); // 創建Path并添加了一個矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
Path dst = new Path(); // 創建用于存儲截取后內容的 Path
PathMeasure measure = new PathMeasure(path, false); // 將 Path 與 PathMeasure 關聯
// 截取一部分存入dst中,并使用 moveTo 保持截取得到的 Path 第一個點的位置不變
measure.getSegment(200, 600, dst, true);
canvas.drawPath(dst, mDeafultPaint); // 繪制 dst
結果如下:
從上圖可以看到我們成功到將需要到片段截取了出來,然而當 dst 中有內容時會怎樣呢?
canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐標系
Path path = new Path(); // 創建Path并添加了一個矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
Path dst = new Path(); // 創建用于存儲截取后內容的 Path
dst.lineTo(-300, -300); // <--- 在 dst 中添加一條線段
PathMeasure measure = new PathMeasure(path, false); // 將 Path 與 PathMeasure 關聯
measure.getSegment(200, 600, dst, true); // 截取一部分 并使用 moveTo 保持截取得到的 Path 第一個點的位置不變
canvas.drawPath(dst, mDeafultPaint); // 繪制 Path
結果如下:
從上面的示例可以看到 dst 中的線段保留了下來,可以得到結論:被截取的 Path 片段會添加到 dst 中,而不是替換 dst 中到內容。
前面兩個例子中 startWithMoveTo 均為 true, 如果設置為false會怎樣呢?
canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐標系
Path path = new Path(); // 創建Path并添加了一個矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
Path dst = new Path(); // 創建用于存儲截取后內容的 Path
dst.lineTo(-300, -300); // 在 dst 中添加一條線段
PathMeasure measure = new PathMeasure(path, false); // 將 Path 與 PathMeasure 關聯
measure.getSegment(200, 600, dst, false); // <--- 截取一部分 不使用 startMoveTo, 保持 dst 的連續性
canvas.drawPath(dst, mDeafultPaint); // 繪制 Path
結果如下:
從該示例我們又可以得到一條結論:如果 startWithMoveTo 為 true, 則被截取出來到Path片段保持原狀,如果 startWithMoveTo 為 false,則會將截取出來的 Path 片段的起始點移動到 dst 的最后一個點,以保證 dst 的連續性。
6.3.4 nextContour
我們知道 Path 可以由多條曲線構成,但不論是 getLength , getSegment 或者是其它方法,都只會在其中第一條線段上運行,而這個 nextContour
就是用于跳轉到下一條曲線的方法,如果跳轉成功,則返回 true, 如果跳轉失敗,則返回 false。
如下,我們創建了一個 Path 并使其中包含了兩個閉合的曲線,內部的邊長是200,外面的邊長是400,現在我們使用 PathMeasure 分別測量兩條曲線的總長度。
代碼:
canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐標系
Path path = new Path();
path.addRect(-100, -100, 100, 100, Path.Direction.CW); // 添加小矩形
path.addRect(-200, -200, 200, 200, Path.Direction.CW); // 添加大矩形
canvas.drawPath(path,mDeafultPaint); // 繪制 Path
PathMeasure measure = new PathMeasure(path, false); // 將Path與PathMeasure關聯
float len1 = measure.getLength(); // 獲得第一條路徑的長度
measure.nextContour(); // 跳轉到下一條路徑
float len2 = measure.getLength(); // 獲得第二條路徑的長度
Log.i("LEN","len1="+len1); // 輸出兩條路徑的長度
Log.i("LEN","len2="+len2);
log輸出結果:
len1=800.0
len2=1600.0
通過測試,我們可以得到以下內容:
- 1、曲線的順序與 Path 中添加的順序有關。
- 2、getLength 獲取到到是當前一條曲線分長度,而不是整個 Path 的長度。
- 3、getLength 等方法是針對當前的曲線(其它方法請自行驗證)。
6.3.5 getPosTan
這個方法是用于得到路徑上某一長度的位置以及該位置的正切值:
boolean getPosTan (float distance, float[] pos, float[] tan)
各參數釋義:
參數 | 作用 | 備注 |
---|---|---|
返回值(boolean) | 判斷獲取是否成功 | true表示成功,數據會存入 pos 和 tan 中, false 表示失敗,pos 和 tan 不會改變 |
distance | 距離 Path 起點的長度 | 取值范圍: 0 <= distance <= getLength |
pos | 該點的坐標值 | 當前點在畫布上的位置,有兩個數值,分別為x,y坐標。 |
tan | 該點的正切值 | 當前點在曲線上的方向,使用 Math.atan2(tan[1], tan[0]) 獲取到正切角的弧度值。 |
tan
是用來判斷 Path 上趨勢的,即在這個位置上曲線的走向,請看下圖示例,注意箭頭的方向:
可以看到 上圖中箭頭在沿著 Path 運動時,方向始終與 Path 走向保持一致,保持方向主要就是依靠 tan
。
下面來看看代碼是如何實現的,首先需要定義幾個必要的變量:
private float currentValue = 0; // 用于紀錄當前的位置,取值范圍[0,1]映射Path的整個長度
private float[] pos; // 當前點的實際位置
private float[] tan; // 當前點的tangent值,用于計算圖片所需旋轉的角度
private Bitmap mBitmap; // 箭頭圖片
private Matrix mMatrix; // 矩陣,用于對圖片進行一些操作
初始化這些變量(在構造函數中調用):
private void init(Context context) {
pos = new float[2];
tan = new float[2];
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2; // 縮放圖片
mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.arrow, options);
mMatrix = new Matrix();
}
具體繪制:
canvas.translate(mViewWidth / 2, mViewHeight / 2); // 平移坐標系
Path path = new Path(); // 創建 Path
path.addCircle(0, 0, 200, Path.Direction.CW); // 添加一個圓形
PathMeasure measure = new PathMeasure(path, false); // 創建 PathMeasure
currentValue += 0.005; // 計算當前的位置在總長度上的比例[0,1]
if (currentValue >= 1) {
currentValue = 0;
}
measure.getPosTan(measure.getLength() * currentValue, pos, tan); // 獲取當前位置的坐標以及趨勢
mMatrix.reset(); // 重置Matrix
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI); // 計算圖片旋轉角度
mMatrix.postRotate(degrees, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2); // 旋轉圖片
mMatrix.postTranslate(pos[0] - mBitmap.getWidth() / 2, pos[1] - mBitmap.getHeight() / 2); // 將圖片繪制中心調整到與當前點重合
canvas.drawPath(path, mDeafultPaint); // 繪制 Path
canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint); // 繪制箭頭
invalidate(); // 重繪頁面
核心要點:
- 1、圖片需要旋轉的角度應該跟圓上改點的切線斜率有關,而切線夾角的tan值可以通過getPosTan得到,而Math中
atan2
方法是根據正切是數值計算出該角度的大小,得到的單位是弧度(取值范圍是 -pi 到 pi),所以上面又將弧度轉為了角度。 - 2、通過
Matrix
來設置圖片對旋轉角度和位移,這里使用的方法與前面講解過對 canvas操作有些類似。 - 3、頁面刷新,頁面刷新此處是在 onDraw 里面調用了 invalidate 方法來保持界面不斷循環刷新,但并不提倡這么做,正確對做法應該是使用 線程 或者 ValueAnimator 來控制界面的刷新。
6.3.6 getMatrix
這個方法是用于得到路徑上某一長度的位置以及該位置的正切值的矩陣:
boolean getMatrix (float distance, Matrix matrix, int flags)
各參數釋義:
參數 | 作用 | 備注 |
---|---|---|
返回值(boolean) | 判斷獲取是否成功 | true表示成功,數據會存入matrix中,false 失敗,matrix內容不會改變 |
distance | 距離 Path 起點的長度 | 取值范圍: 0 <= distance <= getLength |
matrix | 根據 falgs 封裝好的matrix | 會根據 flags 的設置而存入不同的內容 |
flags | 規定哪些內容會存入到matrix中 | 可選擇POSITION_MATRIX_FLAG(位置)、ANGENT_MATRIX_FLAG(正切) |
其實這個方法就相當于在前一個例子中封裝 matrix
的過程,上面的過程由 getMatrix
來做了,可以直接得到一個封裝好到 matrix
。
最后到 flags
選項可以選擇 位置
或者 正切
,如果兩個選項都想選擇,可以將兩個選項之間用 |
連接起來,如下:
measure.getMatrix(distance, matrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
試試將上面都例子中 getPosTan
替換為 getMatrix
, 這樣一來就會顯得簡單很多:
具體繪制:
Path path = new Path(); // 創建 Path
path.addCircle(0, 0, 200, Path.Direction.CW); // 添加一個圓形
PathMeasure measure = new PathMeasure(path, false); // 創建 PathMeasure
currentValue += 0.005; // 計算當前的位置在總長度上的比例[0,1]
if (currentValue >= 1) {
currentValue = 0;
}
// 獲取當前位置的坐標以及趨勢的矩陣
measure.getMatrix(measure.getLength() * currentValue, mMatrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2); // <-- 將圖片繪制中心調整到與當前點重合(注意:此處是前乘pre)
canvas.drawPath(path, mDeafultPaint); // 繪制 Path
canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint); // 繪制箭頭
invalidate(); // 重繪頁面
使用 getMatrix 方法的確可以節省一些代碼,不過這里依舊需要注意一些內容:
- 1、對
matrix
的操作必須要在getMatrix
之后進行,否則會被getMatrix
重置而導致無效。 - 2、矩陣對旋轉角度默認為圖片的左上角,此處需要使用
preTranslate
調整為圖片中心。 - 3、pre(矩陣前乘) 與 post(矩陣后乘) 的區別,后續文章講解。
6.4 Path & SVG
當圖形過于復雜時,用代碼寫就不現實了,在繪制復雜的圖形時一般是將 SVG 圖像轉換為 Path。
SVG 是一種矢量圖,內部用的是 xml 格式化存儲方式存儲這操作和數據,可以將 SVG 看作是 Path 的各項操作簡化書寫后的存儲格式。
Path 和 SVG 結合通常能誕生出一些奇妙的東西,如下:
該圖片來自這個開源庫 :PathView
SVG 轉 Path 的解析可以用這個庫: AndroidSVG
6.5 Path使用技巧
先放一個效果圖,然后分析一下實現過程:
這是一個搜索的動效圖,通過分析可以得到它應該有四種狀態,分別如下:
狀態 | 概述 |
---|---|
初始狀態 | 初始狀態,沒有任何動效,只顯示一個搜索標志 ?? |
準備搜索 | 放大鏡圖標逐漸變化為一個點 |
正在搜索 | 圍繞這一個圓環運動,并且線段長度會周期性變化 |
準備結束 | 從一個點逐漸變化成為放大鏡圖標 |
這些狀態是有序轉換的,轉換流程以及轉換條件如下:
6.5.1 Path 劃分
為了制作對方便,此處整個動效用了兩個 Path, 一個是中間對放大鏡, 另一個則是外側的圓環,將兩者全部畫出來是這樣的。
其中 Path 的走向要把握好,如下(只是一個放大鏡,并不是♂):
其中圓形上面的點可以用 PathMeasure 測量,無需計算。
6.5.2 動畫狀態與時間關聯
此處使用的是 ValueAnimator,它可以將一段時間映射到一段數值上,隨著時間變化不斷的更新數值,并且可以使用插值器來控制數值變化規律。
6.5.3 具體繪制
繪制部分是根據 當前狀態以及從 ValueAnimator 獲得的數值來截取 Path 中合適的部分繪制出來。