PathMeasure的API講解與實戰——Android高級UI

目錄
1、前言
2、API講解
3、實戰
4、更多案例
5、寫在最後
java

1、前言

2019年了,然而2017計劃寫的東西還沒開始😂,此次的拖延症來的比日常早卻去的比日常晚。今天進行分享的是UI中的PathMeasure,同時記錄本身在使用過程當中的幾個疑惑點。話很少說,開始進入正題。android

2、API講解

這一小節主要是對PathMeasure的構造方法公有方法進行講解git

一、構造方法

(1)PathMeasure()

public PathMeasure() 複製代碼

方法描述: 建立一個空的PathMeasure,可是使用以前須要先調用 setPath 方法來與 Path 進行關聯。被關聯的 Path 必須是已經建立好的,若是關聯以後 Path 內容進行了更改,則須要使用 setPath 方法從新關聯。github

(2)PathMeasure(Path path, boolean forceClosed)

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

能夠清楚的設置爲true的路徑長度爲800(五段折線加起來是600,再加上頭尾相連的長度200,正好是800),而爲false的長度爲600(正好是五段折線加起來是600) 若是你的Path已是閉合的(即頭尾相連的),則此時forceClosed設置爲true或false,其長度結果是同樣的。

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);
}
複製代碼

日誌輸出:

二、共有方法

(1)setPath

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)方法講解中的例子。

(2)getLength

public float getLength() 複製代碼

方法描述: 返回當前關聯路徑輪廓的總長度,或者若是沒有路徑,則返回0。

(3)isClosed

public boolean isClosed() 複製代碼

方法描述: 測量的路徑是否閉合。ture爲閉合,false爲不閉合。

值得注意 這裏的閉合取決於兩點: 一、Path 原本就是閉合的,則isClosed返回的就是true。 二、若是 Path 不是閉合的,但在與PathMeasure關聯時(經過構造方法關聯或是經過setPath關聯),將forceClosed設置爲true。此時,isClosed返回true。

(4)nextContour

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);
}
複製代碼

效果圖:

日誌輸出:

(5)getMatrix

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位運算簡單講解》

(6)getSegment

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的效果圖

(6)getPosTan

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

在這裏插入圖片描述

3、實戰

轉圈的箭頭

按照國際慣例,先上效果圖

動畫解析 讓箭頭繞着紅色圓轉圈,同時須要改變箭頭的方向,使其朝向當前位置的切線方向

實現思路與代碼解析 先進行初始化對象,主要是初始化畫筆、圖片、路徑、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)直角座標系與極座標系的轉換(圖片是本身手寫的,字跡粗糙勿噴😄)

從圖中能夠知道 θ 的計算是經過該點的x和y座標得出,而且還要根據 y/x 計算結果的符號和該點存在的象限來共同決定。而經過getPosTan方法得到的 tan[]中的值即可以看做該點的x、y座標值(具體緣由能夠查看前面getPosTan方法中的數學小課堂)。

因此只需對(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);
複製代碼

至此,效果已完成。

須要查看完整代碼的童鞋,請進傳送門

4、更多案例

一、拖動的loading線條

效果圖

代碼傳送門 完整代碼請進

二、乘風破浪的小船

效果圖

代碼傳送門 完整代碼請進

5、寫在最後

PathMeasure能夠說是自定義UI的利器之一,熟練的掌握能讓咱們斬獲更多的產品😈。若是各位童鞋在閱讀中發現有錯誤或是晦澀難懂的地方請與我聯繫,我會及時修改,讓咱們共同進步。一樣若是你喜歡的話,請給個贊並關注我吧😄。

高級UI系列的Github地址:請進入傳送門,若是喜歡的話給我一個star吧😄

若是須要更多的交流與探討,能夠經過如下微信二維碼加小盆友好友。

相關文章
相關標籤/搜索