目錄
1、前言
2、API講解
3、實戰
4、更多案例
5、寫在最後
java
2019年了,然而2017計劃寫的東西還沒開始😂,此次的拖延症來的比日常早卻去的比日常晚。今天進行分享的是UI中的PathMeasure,同時記錄本身在使用過程當中的幾個疑惑點。話很少說,開始進入正題。android
這一小節主要是對PathMeasure的構造方法和公有方法進行講解git
public PathMeasure() 複製代碼
方法描述: 建立一個空的PathMeasure,可是使用以前須要先調用 setPath 方法來與 Path 進行關聯。被關聯的 Path 必須是已經建立好的,若是關聯以後 Path 內容進行了更改,則須要使用 setPath 方法從新關聯。github
public PathMeasure(Path path, boolean forceClosed) 複製代碼
方法描述: 建立 PathMeasure 並關聯一個指定的Path,且Path須要已經建立完成。 這個構造方法其實 和 使用 PathMeasure() 後調用 setPath方法 進行關聯一個Path的效果是同樣的;固然,被關聯的 Path 也必須是已經建立好的,若是關聯以後 Path 內容進行了更改,則須要使用 setPath 方法從新關聯。canvas
參數解析: 第一個參數 path: 被關聯的 Path,也就是須要測量的Path; 第二個參數 forceClosed: 是否要閉合Path。 設置爲true:則不論Path是否閉合,都會自動閉合該 Path(若是Path能夠閉合的話),而後進行測量; 設置爲false:則Path保持原來的樣子,進行測量;數組
值得注意的兩個小點:(敲黑板了!!!)
一、不論 forceClosed 設置爲什麼種狀態(true 或者 false),都不會影響原有Path的狀態,即 Path 與 PathMeasure 關聯以後,以前的Path 不會有任何改變
二、forceClosed 的設置狀態可能會影響測量結果,若是 Path 未閉合但在與 PathMeasure 關聯的時候設置 forceClosed 爲 true 時,測量結果可能會比 Path 實際長度稍長一點,具體請看下面的例子。微信
舉個栗子🌰ide
完整代碼請看這裏,傳送門函數
代碼主要畫了以下圖的路徑,而後對使用PathMeasure與該path進行關聯,一個對forceClosed設置爲true,一個爲false,而後進行日誌打印。 post
Path mPath;
Paint mPaint;
int width;
int height;
boolean isInit = false;
PathMeasure closePathMeasure;
PathMeasure noClosePathMeasure;
@Override
protected void init(Context context) {
mPath = new Path();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(ContextCompat.getColor(context, R.color.color_blue));
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!isInit) {
isInit = true;
width = getMeasuredWidth() / 2;
height = getMeasuredHeight() / 2;
mPath.lineTo(0, 100);
mPath.lineTo(100, 100);
mPath.lineTo(100, -100);
mPath.lineTo(200, -100);
mPath.lineTo(200, 0);
closePathMeasure = new PathMeasure(mPath, true);
float closeLength = closePathMeasure.getLength();
noClosePathMeasure = new PathMeasure(mPath, false);
float noCloseLength = noClosePathMeasure.getLength();
Log.i(TAG, "[closeLength:" + closeLength +
"; noCloseLength:" + noCloseLength + "]");
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(width, height);
canvas.drawPath(mPath, mPaint);
}
複製代碼
日誌輸出:
public void setPath(Path path, boolean forceClosed) 複製代碼
方法描述: 關聯一個Path,該方法的做用是:當路徑Path變更後,PathMeasure須要從新關聯,不然從PathMeasure獲得的數據仍是以前關聯的Path數據,而並不是新的Path數據。
參數解析: 第一個參數 path: 被關聯的 Path,也就是須要測量的Path; 第二個參數 forceClosed: 是否要閉合Path。 設置爲true:則不論Path是否閉合,都會自動閉合該 Path(若是Path能夠閉合的話),而後進行測量; 設置爲false:則Path保持原來的樣子,進行測量;
值得注意的兩個小點:(此處和構造方法PathMeasure(Path path, boolean forceClosed)的描述是同樣)
一、不論 forceClosed 設置爲什麼種狀態(true 或者 false),都不會影響原有Path的狀態,即 Path 與 PathMeasure 關聯以後,以前的Path 不會有任何改變
二、forceClosed 的設置狀態可能會影響測量結果,若是 Path 未閉合但在與 PathMeasure 關聯的時候設置 forceClosed 爲 true 時,測量結果可能會比 Path 實際長度稍長一點,具體可看PathMeasure(Path path, boolean forceClosed)方法講解中的例子。
public float getLength() 複製代碼
方法描述: 返回當前關聯路徑輪廓的總長度,或者若是沒有路徑,則返回0。
public boolean isClosed() 複製代碼
方法描述: 測量的路徑是否閉合。ture爲閉合,false爲不閉合。
值得注意 這裏的閉合取決於兩點: 一、Path 原本就是閉合的,則isClosed返回的就是true。 二、若是 Path 不是閉合的,但在與PathMeasure關聯時(經過構造方法關聯或是經過setPath關聯),將forceClosed設置爲true。此時,isClosed返回true。
public boolean nextContour() 複製代碼
方法描述: 獲取在路徑中下一個輪廓,若是有下一個輪廓,則返回true,且PathMeasure切至下一個輪廓的數據;若是沒有下一個輪廓則返回false。至於怎麼纔算一個輪廓,且看下面例子:
舉個栗子🌰 這段代碼主要是畫了三次,即moveTo了三次,因此即便在圖中看起來是兩個正方形,但在PathMeasure中能夠得出三段輪廓。每次調用nextContour,都按咱們畫的順序給咱們切換,直至最後一個輪廓在調用nextContour時返回false,則中斷循環。
Path mNextContourPath;
PathMeasure nextContourPathMeasure;
int width;
int height;
boolean isInit = false;
Paint mPaint;
@Override
protected void init(Context context) {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(ContextCompat.getColor(context, R.color.color_purple));
mPaint.setStyle(Paint.Style.STROKE);
mNextContourPath = new Path();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!isInit) {
isInit = true;
width = getMeasuredWidth() / 2;
height = getMeasuredHeight() / 2;
// 第一個輪廓
mNextContourPath.moveTo(-100, -100);
mNextContourPath.lineTo(-100, 100);
mNextContourPath.lineTo(100, 100);
mNextContourPath.lineTo(100, -100);
mNextContourPath.lineTo(-100, -100);
// 第二個輪廓
mNextContourPath.moveTo(-50, -50);
mNextContourPath.lineTo(-50, 50);
mNextContourPath.lineTo(50, 50);
mNextContourPath.lineTo(50, -50);
// 第三個輪廓
mNextContourPath.moveTo(50, -50);
mNextContourPath.lineTo(-50, -50);
nextContourPathMeasure = new PathMeasure(mNextContourPath, false);
int i = 0;
while (nextContourPathMeasure.nextContour()) {
++i;
Log.i(TAG, "第" + i + "個輪廓的 Length:" + nextContourPathMeasure.getLength());
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(width, height);
canvas.drawPath(mNextContourPath, mPaint);
}
複製代碼
效果圖:
public boolean getMatrix(float distance, Matrix matrix, int flags) 複製代碼
方法描述: 用於獲取關聯的Path上距離起始點長度( 即傳入的distance,範圍0<=distance<=getLength() )的點的座標和正切值(二者可選,由flags決定)。
返回值: 一、爲true時,說明獲取成功,數據存進matrix; 二、爲false時,說明獲取失敗,matrix不變更;
參數解析: 第一個參數 distance: 即須要的測量點與當前path起始位置的距離,取值範圍:0<=distance<=getLength() ; 第二個參數 matrix: 測量點的矩陣,能夠選擇包含點的座標和正切值,所包含的數據由flags決定; 第三個參數 flags: 決定matrix中包含的數據,能夠選擇的值有:POSITION_MATRIX_FLAG(位置) 和 ANGENT_MATRIX_FLAG(正切) 若是須要兩個值時,能夠用或「|」將其拼湊後傳入,例如:
pathMeasure.getMatrix(distance, matrix, PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
複製代碼
知識點拓展:若是對 POSITION_MATRIX_FLAG|ANGENT_MATRIX_FLAG 這種傳值不太理解的童鞋能夠查看我寫的另一篇文章《android位運算簡單講解》
public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 複製代碼
方法描述: 獲取關聯的path的片斷路徑,添加至dst路徑中(並不是替換,是增長)
返回值: 一、爲true時,說明截取成功,添加至dst路徑中; 二、爲false時,說明截取失敗,dst路徑不變更;
參數解析: 第一個參數 startD: 截取的路徑的起始點距離path起始點的長度,取值範圍:0<=startD<stopD<=Path.getLength(); 第二個參數 stopD: 截取的路徑的終止點距離path起始點的長度,取值範圍:0<=startD<stopD<=Path.getLength(); 第三個參數 dst: 截取的路徑保存的地方,此處特別注意截取的路徑是添加到dst中,而非替換; 第四個參數 startWithMoveTo: 截取的片斷的第一個點是否保持不變; 設置爲true:保持截取的片斷不變,添加至dst路徑中; 設置爲false:會將截取的片斷的起始點移至dst路徑中的最後一個點,讓dst路徑保持連續
值得一提 若是你在4.4或更早的版本使用在使用這個函數時,須要先調用一下 mDst.lineTo(0, 0); 這句代碼,這是由於硬件加速致使的問題;如不調用,會致使沒有任何效果。
舉個例子🌰 咱們以屏幕中心爲原點,先畫一條從 (0,0) 到 (200,200) 的直線,而後從一個順時針畫的圓中截取 0.25 到 0.5 距離的圓弧放置dst中,先將startWithMoveTo設置爲true,具體代碼以下:
mGetSegmentPathMeasure = new PathMeasure();
// 順時針畫 半徑爲400px的圓
mPath.addCircle(0, 0, 400, Path.Direction.CW);
mGetSegmentPathMeasure.setPath(mPath, false);
// 畫直線
mDst.moveTo(0, 0);
mDst.lineTo(200, 200);
// 截取 0.25 到 0.5 距離的圓弧放置dst中
mGetSegmentPathMeasure.getSegment(mGetSegmentPathMeasure.getLength() * 0.25f,
mGetSegmentPathMeasure.getLength() * 0.5f,
mDst,
true);
canvas.drawPath(mDst, mPaint);
複製代碼
代碼只是截取主要部門,須要查看完整代碼的童鞋,請入傳送門
效果圖
若是將 startWithMoveTo 參數值改成 false,則效果不一樣,代碼以下:
mGetSegmentPathMeasure = new PathMeasure();
// 順時針畫 半徑爲400px的圓
mPath.addCircle(0, 0, 400, Path.Direction.CW);
mGetSegmentPathMeasure.setPath(mPath, false);
// 畫直線
mDst.moveTo(0, 0);
mDst.lineTo(200, 200);
// 截取 0.25 到 0.5 距離的圓弧放置dst中
mGetSegmentPathMeasure.getSegment(mGetSegmentPathMeasure.getLength() * 0.25f,
mGetSegmentPathMeasure.getLength() * 0.5f,
mDst,
false);
canvas.drawPath(mDst, mPaint);
複製代碼
效果圖
從兩個效果圖,可看出startWithMoveTo參數設置爲true和false,會致使dst路徑的不一樣。爲true時,保持 截取的片斷路徑 的原樣將其添加至 dst路徑 中;爲false時,會將截取的片斷的起始點移至dst路徑中的最後一個點,讓dst路徑保持連續。
值得注意 在寫這篇博客時,將startWithMoveTo參數設置爲false,在兩臺測試機(Mate10 Android 8.1.0和oppo A57 Android 6.0.1)上運行,效果有些許不一樣。 Demo使用的是px做爲單位,兩臺手機的分辨率不一樣,因此在 A57 機型上按比例縮小了一倍進行繪製 (即圓半徑從400px變爲200px,斜線從(0,0)->(200,200)變爲(0,0)->(100,100) ),從下面👇的OPPO A57的效果圖能夠很明顯的看出,圓弧的路徑已經受到dst中最後一個點的影響,改變了形狀。(Mate10的效果圖請翻閱上面👆)
OPPO A57的效果圖
public boolean getPosTan(float distance, float pos[], float tan[]) 複製代碼
方法描述: 獲取關聯的Path距離起始點長度(distance)的點的 座標(pos) 和 餘弦(tan[0],即cos)與正弦(tan[1],即sin)。
返回值 一、爲true時,說明獲取成功,該點的 座標 以及 正餘弦 將各自存進pos和tan參數 二、爲false時,說明獲取失敗,pos與tan沒有變更
參數解析: 第一個參數 distance: 即須要的測量點與當前path起始位置的距離,取值範圍:0<=distance<=getLength() ; 第二個參數 pos: 測量點的座標,pos[0]爲x座標,pos[1]爲y座標; 第三個參數 tan: 測量點的正餘弦值,tan[0]爲cos,即餘弦值或稱爲單位圓的x座標;tan[1]爲sin,即正弦值或稱爲單位圓的y座標;
數學小課堂: 單位圓指的是平面直角座標系上,圓心爲原點,半徑爲1的圓。 cos = 鄰邊/斜邊 = OB/OA = OB(由於OA長度爲1)= x sin = 對邊/斜邊 = AB/OA = AB (由於OA長度爲1) = y
按照國際慣例,先上效果圖
動畫解析 讓箭頭繞着紅色圓轉圈,同時須要改變箭頭的方向,使其朝向當前位置的切線方向
實現思路與代碼解析 先進行初始化對象,主要是初始化畫筆、圖片、路徑、PathMeasure、裝載變量、估值器,具體爲每一個對象設置的屬性請看下面代碼,此處比較簡單,就再也不贅述
// 初始化 畫筆 [抗鋸齒、不填充、紅色、線條2px]
mCirclePaint = new Paint();
mCirclePaint.setAntiAlias(true);
mCirclePaint.setStyle(Paint.Style.STROKE);
mCirclePaint.setColor(Color.RED);
mCirclePaint.setStrokeWidth(2);
// 獲取圖片
mArrowBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.arrow, null);
// 初始化 圓路徑 [圓心(0,0)、半徑200px、順時針畫]
mCirclePath = new Path();
mCirclePath.addCircle(0, 0, 200, Path.Direction.CW);
// 初始化 裝載 座標 和 正餘弦 的數組
mPos = new float[2];
mTan = new float[2];
// 初始化 PathMeasure 而且關聯 圓路徑
mPathMeasure = new PathMeasure();
mPathMeasure.setPath(mCirclePath, false);
// 初始化矩陣
mMatrix = new Matrix();
// 初始化 估值器 [區間0-一、時長5秒、線性增加、無限次循環]
valueAnimator = ValueAnimator.ofFloat(0, 1f);
valueAnimator.setDuration(5000);
// 勻速增加
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 第一種作法:經過本身控制,是箭頭在原來的位置繼續運行
mCurrentValue += DELAY;
if (mCurrentValue >= 1) {
mCurrentValue -= 1;
}
// 第二種作法:直接獲取能夠經過估值器,改變其變更規律
//mCurrentValue = (float) animation.getAnimatedValue();
invalidate();
}
});
複製代碼
初始化工做完成後,接下來就是進行繪製工做,咱們按照步驟來說解: 第一步,將屏幕的中心點做爲原點,方便操做和繪製
// 移至canvas中間
canvas.translate(mWidth / 2, mHeight / 2);
複製代碼
第二步,繪製圓,即箭頭走的軌跡,PathMeasure所關聯的Path就是此處的mCirclePath,在上面的初始化代碼能夠清晰的看到
// 畫圓路徑
canvas.drawPath(mCirclePath, mCirclePaint);
複製代碼
第三步,獲取當前點的座標以及正餘弦的值,存放至mPos和mTan變量中
// 測量 pos(座標) 和 tan(正切)
mPathMeasure.getPosTan(mPathMeasure.getLength() * mCurrentValue, mPos, mTan);
複製代碼
第四步,經過反正弦atan2計算出角度(單位爲弧度),因此須要進行將單位在轉爲度。
// 計算角度
float degree = (float) (Math.atan2(mTan[1], mTan[0]) * 180 / Math.PI);
複製代碼
數學小課堂 咱們來拆分下這個公式,先看Math.atan2(mTan[1], mTan[0]) 這段,這裏關係到的是直角座標系與極座標系的轉換,因此咱們先來重拾下忘記的第一個知識點 (1)直角座標系與極座標系的轉換(圖片是本身手寫的,字跡粗糙勿噴😄)
因此只需對(y/x)進行求反正切即可,但這裏有存在一個問題,也就是咱們剛剛提到的 θ 由 y/x 計算結果的符號和該點存在的象限決定,若是使用 Math.atan(double a) 方法進行求反正切,其結果範圍爲開區間的 (-pi/2,pi/2),然而一個圓的的範圍是(-pi,pi),這顯然直接使用是不能知足的。幸虧Math類提供了一個讓咱們省事的API atan2(double y, double x),其返回值的範圍正是 (-pi,pi)。
到這裏已經能經過atan2函數獲得該點的角度,可是其單位是弧度,並不能在直角座標系中直接拿來使用,須要進行轉換。因此咱們須要引出第二個被遺忘的知識點
(2)弧度制 弧度制是什麼這裏就不作過多解釋。這裏涉及到一個公式就是 1° = π/180 rad ,看到這裏你們應該就明白爲何要 乘以 180 / Math.PI,由於求出的反正切的值單位爲弧度,須要轉爲咱們一般使用的角度制中的度。
第五步,重置矩陣,避免矩陣內有以前遺留的操做。
// 重置矩
mMatrix.reset();
複製代碼
第六步,根據第四步計算得出的角度而且以圖片的中心點進行旋轉
// 設置旋轉角度
mMatrix.postRotate(degree, mArrowBitmap.getWidth() / 2, mArrowBitmap.getHeight() / 2);
複製代碼
第七部,進行偏移,由於直接繪製的話,箭頭會在軌道以外,須要挪動箭頭的寬和高各一半
// 設置偏移量
mMatrix.postTranslate(mPos[0] - mArrowBitmap.getWidth() / 2,
mPos[1] - mArrowBitmap.getHeight() / 2);
複製代碼
第八步,使用矩陣將箭頭繪製至畫布中
// 畫箭頭,使用矩陣旋轉
canvas.drawBitmap(mArrowBitmap, mMatrix, mCirclePaint);
複製代碼
至此,效果已完成。
須要查看完整代碼的童鞋,請進傳送門
效果圖
代碼傳送門 完整代碼請進
效果圖
代碼傳送門 完整代碼請進
PathMeasure能夠說是自定義UI的利器之一,熟練的掌握能讓咱們斬獲更多的產品😈。若是各位童鞋在閱讀中發現有錯誤或是晦澀難懂的地方請與我聯繫,我會及時修改,讓咱們共同進步。一樣若是你喜歡的話,請給個贊並關注我吧😄。
高級UI系列的Github地址:請進入傳送門,若是喜歡的話給我一個star吧😄
若是須要更多的交流與探討,能夠經過如下微信二維碼加小盆友好友。