安卓自定義View進階-PathMeasure
可以看到,在經過 Path之基本操作 Path之貝塞爾曲線 和 Path之完結篇 後, Path中各類方法基本上都講完了,表格中還沒有講解到到方法就是矩陣變換了,難道本篇終於要講矩陣了? 非也,矩陣這一部分仍在後面單獨講解,本篇主要講解 PathMeasure 這個類與 Path 的一些使用技巧。
PS:不要問我為什麼不講 PathEffect,因為這個方法在後面的Paint系列中。
先放一個圖鎮樓,省的下面無聊的內容把你們都嚇跑了Σ( ̄。 ̄ノ)ノ
Path & PathMeasure
顧名思義,PathMeasure是一個用來測量Path的類,主要有以下方法:
構造方法
方法名 | 釋義 |
---|---|
PathMeasure() | 建立一個空的PathMeasure |
PathMeasure(Path path, boolean forceClosed) | 建立 PathMeasure 並關聯一個指定的Path(Path需要已經建立完成)。 |
公共方法
返回值 | 方法名 | 釋義 |
---|---|---|
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 |
PathMeasure的方法也不多,接下來我們就逐一的講解一下。
1.建構函式
建構函式有兩個。
無參建構函式:
PathMeasure ()
用這個建構函式可建立一個空的 PathMeasure,但是使用之前需要先呼叫 setPath 方法來與 Path 進行關聯。被關聯的 Path 必須是已經建立好的,如果關聯之後 Path 內容進行了更改,則需要使用 setPath 方法重新關聯。
有參建構函式:
PathMeasure (Path path, boolean forceClosed)
用這個建構函式是建立一個 PathMeasure 並關聯一個 Path, 其實和建立一個空的 PathMeasure 後呼叫 setPath 進行關聯效果是一樣的,同樣,被關聯的 Path 也必須是已經建立好的,如果關聯之後 Path 內容進行了更改,則需要使用 setPath 方法重新關聯。
該方法有兩個引數,第一個引數自然就是被關聯的 Path 了,第二個引數是用來確保 Path 閉合,如果設定為 true, 則不論之前Path是否閉合,都會自動閉合該 Path(如果Path可以閉合的話)。
在這裡有兩點需要明確:
- 不論 forceClosed 設定為何種狀態(true 或者 false), 都不會影響原有Path的狀態,即 Path 與 PathMeasure 關聯之後,之前的的 Path 不會有任何改變。
- 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如下:
com.gcssloop.canvas E/TAG: forceClosed=false---->600.0
com.gcssloop.canvas E/TAG: forceClosed=true----->800.0
繪製在介面上的效果如下:
我們所建立的 Path 實際上是一個邊長為 200 的正方形的三條邊,通過上面的示例就能驗證以上兩個問題。
- 1.我們將 Path 與兩個的 PathMeasure 進行關聯,並給 forceClosed 設定了不同的狀態,之後繪製再繪製出來的 Path 沒有任何變化,所以與 Path 與 PathMeasure進行關聯並不會影響 Path 狀態。
- 2.我們可以看到,設定 forceClosed 為 true 的方法比設定為 false 的方法測量出來的長度要長一點,這是由於 Path 沒有閉合的緣故,多出來的距離正是 Path 最後一個點與最開始一個點之間點距離。forceClosed 為 false 測量的是當前 Path 狀態的長度, forceClosed 為 true,則不論Path是否閉合測量的都是 Path 的閉合長度。
2.setPath、 isClosed 和 getLength
這三個方法都如字面意思一樣,非常簡單,這裡就簡單是敘述一下,不再過多講解。
setPath 是 PathMeasure 與 Path 關聯的重要方法,效果和 建構函式 中兩個引數的作用是一樣的。
isClosed 用於判斷 Path 是否閉合,但是如果你在關聯 Path 的時候設定 forceClosed 為 true 的話,這個方法的返回值則一定為true。
getLength 用於獲取 Path 的總長度,在之前的測試中已經用過了。
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, 並在其中添加了一個矩形,現在我們想擷取矩形中的一部分,就是下圖中紅色的部分。
矩形邊長400dp,起始點在左上角,順時針
程式碼:
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 的連續性。
從而我們可以用以下規則來判斷 startWithMoveTo 的取值:
取值 | 主要功用 |
---|---|
true | 保證擷取得到的 Path 片段不會發生形變 |
false | 保證儲存擷取片段的 Path(dst) 的連續性 |
4.nextContour
我們知道 Path 可以由多條曲線構成,但不論是 getLength , getgetSegment 或者是其它方法,都只會在其中第一條線段上執行,而這個 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輸出結果:
com.gcssloop.canvas I/LEN: len1=800.0
com.gcssloop.canvas I/LEN: len2=1600.0
通過測試,我們可以得到以下內容:
- 1.曲線的順序與 Path 中新增的順序有關。
- 2.getLength 獲取到到是當前一條曲線分長度,而不是整個 Path 的長度。
- 3.getLength 等方法是針對當前的曲線(其它方法請自行驗證)。
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
這個東東,這個東西是幹什麼的呢?
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
得值計算出圖片旋轉的角度,tan 是 tangent 的縮寫,即中學中常見的正切, 其中tan[0]是鄰邊邊長,tan[1]是對邊邊長,而Math中atan2
方法是根據正切是數值計算出該角度的大小,得到的單位是弧度(取值範圍是 -pi 到 pi),所以上面又將弧度轉為了角度。- 2.通過
Matrix
來設定圖片對旋轉角度和位移,這裡使用的方法與前面講解過對 canvas操作 有些類似,對於Matrix
會在後面專一進行講解,敬請期待。- 3.頁面重新整理,頁面重新整理此處是在 onDraw 裡面呼叫了 invalidate 方法來保持介面不斷重新整理,但並不提倡這麼做,正確對做法應該是使用 執行緒 或者 ValueAnimator 來控制介面的重新整理,關於控制頁面重新整理這一部分會在後續的 動畫部分 詳細講解,同樣敬請期待。
關於tan
這個引數有很多魔法師不理解,特此拉出來詳述一下,tan
在數學中被稱為正切,在直角三角形中,一個銳角的正切定義為它的對邊(Opposite side)與鄰邊(Adjacent side)的比值(來自維基百科):
我們此處用 tan
來描述 Path 上某一點的切線方向,主要用了兩個數值 tan[0] 和 tan[1] 來描述這個切線的方向(切線方向與x軸夾角) ,看上面公式可知 tan
既可以用 對邊/鄰邊
來表述,也可以用 sin/cos
來表述,此處用兩種理解方式均可以(注意下面等價關係):
tan[0] = cos = 鄰邊(單位圓x座標)
tan[1] = sin = 對邊(單位圓y座標)
以 sin/cos
理解:
在圓上最右側點的切線方向向下(動圖中小飛機朝向和切線朝向一致),切線角度為90度.
sin90 = 1,cos90 = 0
tan[0] = cos = 0
tan[1] = sin = 1
以 對邊/鄰邊
理解(單位圓上座標):
按照這種理解方式需要藉助一個單位圓,單位圓上任意一點到圓心到距離均為 1,以下圖30度為例:
tan30 = 對邊/鄰邊 = AB/OA = B點y座標/B點x座標
另外根據單位圓性質同樣可以證得:
sin30 = 對邊/斜邊 = AB/OB = AB = B點y座標 (單位圓邊上任意一點距離圓心距離均為1,故OB = 1)
cos30 = 鄰邊/斜邊 = OA/OB = OA = B點x座標化為通用公式即為:
sin = 該角度在單位圓上對應點的y座標
cos = 該角度在單位圓上對應點的x座標即 tan = sin/cos = y/x
tan[0] = x
tan[1] = y另外注意,這個單位圓與小飛機路徑沒有半毛錢關係,例如上一個例子中的90度切線,不要在單位圓上找對應位置,要找對應角度的位置,90度對應的位置是(0,1),所以:
tan[0] = x = 0
tan[1] = y = 1其實繞來繞去全是等價的 (╯°Д°)╯︵ ┻━┻
PS: 使用 Math.atan2(tan[1], tan[0]) 將 tan
轉化為角(單位為弧度)的時候要注意引數順序。
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(矩陣後乘) 的區別,此處請等待後續的文章或者自行搜尋。
Path & SVG
我們知道,用Path可以創建出各種個樣的圖形,但如果圖形過於複雜時,用程式碼寫就不現實了,不僅麻煩,而且容易出錯,所以在繪製複雜的圖形時我們一般是將 SVG 影象轉換為 Path。
你說什麼是 SVG?
SVG 是一種向量圖,內部用的是 xml 格式化儲存方式儲存這操作和資料,你完全可以將 SVG 看作是 Path 的各項操作簡化書寫後的儲存格式。
Path 和 SVG 結合通常能誕生出一些奇妙的東西,如下:
該圖片來自這個開源庫 ->PathView
SVG 轉 Path 的解析可以用這個庫 -> AndroidSVG
限於篇幅以及本人精力,這一部分就暫不詳解了,感興趣的可以直接看原始碼,或者搜尋一些相關的解析文章。
Path使用技巧
話說本篇文章的名字不是叫 玩出花樣麼?怎麼只見前面囉囉嗦嗦的扯了一大堆不明所以的東西,花樣在哪裡?
前面的內容雖然囉嗦繁雜,但卻是重中之重的基礎,如果在修仙界,這叫根基,而下面講述的內容的是招式,有了根基才能演化出千變萬化的招式,而沒有根基只學招式則是徒有其表,只能學一樣會一樣,很難適應千變萬化的需求。
先放一個效果圖,然後分析一下實現過程:
這是一個搜尋的動效圖,通過分析可以得到它應該有四種狀態,分別如下:
狀態 | 概述 |
---|---|
初始狀態 | 初始狀態,沒有任何動效,只顯示一個搜尋標誌 �� |