Android PathMeasure詳解和應用

鋸齒流沙發表於2019-02-09

PathMeasure,顧名思義,就是一個用來測量Path的類,主要有以下方法:

PathMeasure.png

構造方法

無參構造方法:

PathMeasure()

建立一個空的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:

1)不論 forceClosed 設定為何種狀態(true 或者 false), 都不會影響原有Path的狀態,即 Path 與 PathMeasure 關聯之後,之前的的 Path 不會有任何改變。

2)forceClosed 的設定狀態可能會影響測量結果,如果 Path 沒有閉合但在與 PathMeasure 關聯的時候設定 forceClosed 為 true 時,測量結果可能會比 Path 實際長度稍長一點,獲取得到的是該 Path 閉合時的狀態。

setPath

setPath(Path path, boolean forceClosed)方法就是關聯一個Path,需要預先建立好。

isClosed

isClosed方法用於判斷 Path 是否閉合,但是如果你在關聯 Path 的時候設定 forceClosed 為 true 的話,這個方法的返回值則一定為true。

getLength

getLength()方法用於獲取Path的長度。

public class PathMeasureView extends View {

    private static final String TAG = "lwj";
    private int mViewHeight;
    private int mViewWidth;
    private Paint paint;

    public PathMeasureView(Context context) {
        super(context);
        init(context);
    }

    private void init(Context context) {
        paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(10);
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(mViewWidth/2, mViewHeight/2);

        Path path = new Path();
		path.lineTo(0, 300);
		path.lineTo(300, 300);
		path.lineTo(300, 0);

		PathMeasure measure = new PathMeasure(path, false);
		PathMeasure measure2 = new PathMeasure(path, true);
		Log.i(TAG, "length:"+measure.getLength());//900
		Log.i(TAG,"length:"+ measure2.getLength());//1200
        canvas.drawPath(path, paint);

    }

    //該方法在當前View尺寸變化時被呼叫
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mViewHeight = h;
        mViewWidth = w;
    }
}
複製程式碼

nextContour

我們知道 Path 可以由多條曲線構成,但不論是 getLength , getgetSegment 或者是其它方法,都只會在其中第一條線段上執行,而這個 nextContour 就是用於跳轉到下一條曲線到方法,如果跳轉成功,則返回 true, 如果跳轉失敗,則返回 false。
注意:使用多路徑的效果需要關閉硬體加速。

setLayerType(View.LAYER_TYPE_SOFTWARE, null);
Path path = new Path();
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
path.addRect(-100, -100, 100, 100, Path.Direction.CW);
PathMeasure measure = new PathMeasure(path, false);
float length = measure.getLength();
//獲取下一個路徑,有可能沒有多個路徑了,返回false
boolean nextContour = measure.nextContour();
float length2 = measure.getLength();
Log.i("damon", "length1:"+length);
Log.i("damon", "length2:"+length2);
canvas.drawPath(path, paint);
複製程式碼
PathMeasure.png
PathMeasure.png

getSegment

boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo):用於獲取Path的一個片段。

解析:

1)返回值(boolean):判斷擷取是否成功,true 表示擷取成功,結果存入dst中,false 擷取失敗,不會改變dst中內容。

2)startD:開始擷取位置距離 Path 起點的長度 取值範圍: 0 <= startD < stopD <= Path總長度;

3)stopD:結束擷取位置距離 Path 起點的長度 取值範圍: 0 <= startD < stopD <= Path總長度;

4)dst:擷取的 Path 將會新增到 dst 中 注意: 是新增,而不是替換;

5)startWithMoveTo:起始點是否使用 moveTo,用於保證擷取的 Path 第一個點位置不變。

注意:

• 如果 startD、stopD 的數值不在取值範圍 [0, getLength] 內,或者 startD == stopD 則返回值為 false,不會改變 dst 內容。

• 如果在安卓4.4或者之前的版本,在預設開啟硬體加速的情況下,更改 dst 的內容後可能繪製會出現問題,請關閉硬體加速或者給 dst 新增一個單個操作,例如: dst.rLineTo(0, 0)

• 可以用以下規則來判斷 startWithMoveTo 的取值:

true:保證擷取得到的 Path 片段不會發生形變;

false:保證儲存擷取片段的 Path(dst) 的連續性。

Path path = new Path();
//多路徑的效果需要關閉硬體加速!!
path.addRect(-200, -200, 200, 200, Path.Direction.CW);
PathMeasure measure = new PathMeasure(path, false);
float length = measure.getLength();
Log.i("damon", "length1:"+length);
canvas.drawPath(path, paint);

Path dst = new Path();
dst.lineTo(-300, -300);

//startWithMoveTo:false,代表該起始點是否位上一個的結束點(是否保持連續性)。
measure.getSegment(200, 600, dst , false);
paint.setColor(Color.RED);
canvas.drawPath(dst, paint);
複製程式碼
PathMeasure.png

getMatrix

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(正切)

getPosTan

getPosTan(float distance, float[] pos, float[] tan):獲取指定長度的位置座標及該點切線值tangle。

返回值(boolean):判斷獲取是否成功,true表示成功,資料會存入 pos 和 tan 中,
false 表示失敗,pos 和 tan 不會改變;

distance:距離 Path 起點的長度,取值範圍: 0 <= distance <= getLength;

pos:該點的座標值,座標值: (x==[0], y==[1]);

tan:該點的正切值,正切值: (x==[0], y==[1])。

通過 三角函式tan 得值計算出圖片旋轉的角度,tan 是 tangent 的縮寫, 其中tan0是鄰邊邊長,tan1是對邊邊長,而Math中 atan2 方法是根據正切是數值計算出該角度的大小,得到的單位是弧度,所以上面又將弧度轉為了角度。

Path path = new Path();
path.addCircle(0, 0, 300, Path.Direction.CW);

PathMeasure measure = new PathMeasure(path, false);
float[] pos = new float[2];
float[] tan = new float[2];//tan=y/x
measure.getPosTan(measure.getLength()/4, pos , tan );
Log.i("damon", "position:x-"+pos[0]+", y-"+pos[1]);
Log.i("damon", "tan:x-"+tan[0]+", y-"+tan[1]);
canvas.drawPath(path, paint);
複製程式碼
PathMeasure.png
PathMeasure.png

應用

繪製一個放大鏡,然後慢慢沿著放大鏡的路徑慢慢撤退消失,變成圓形搜尋的loading,接著loading完成之後,沿著路徑繪製出放大鏡。
如效果圖所示:

PathMeasure.png
PathMeasure.png
PathMeasure.png

這樣一個自定義View,需要用到PathMeasure,動畫等知識配合來做。

public class SearchView extends View {

    // 畫筆
    private Paint mPaint;

    // View 寬高
    private int mViewWidth;
    private int mViewHeight;

    // 這個檢視擁有的狀態
    public static enum State {
        NONE,
        STARTING,
        SEARCHING,
        ENDING
    }

    // 當前的狀態(非常重要)
    private State mCurrentState = State.NONE;

    // 放大鏡與外部圓環
    private Path path_srarch;
    private Path path_circle;

    // 測量Path 並擷取部分的工具
    private PathMeasure mMeasure;

    // 預設的動效週期 2s
    private int defaultDuration = 2000;

    // 控制各個過程的動畫
    private ValueAnimator mStartingAnimator;
    private ValueAnimator mSearchingAnimator;
    private ValueAnimator mEndingAnimator;

    // 動畫數值(用於控制動畫狀態,因為同一時間內只允許有一種狀態出現,具體數值處理取決於當前狀態)
    private float mAnimatorValue = 0;

    // 動效過程監聽器
    private ValueAnimator.AnimatorUpdateListener mUpdateListener;
    private Animator.AnimatorListener mAnimatorListener;

    // 用於控制動畫狀態轉換
    private Handler mAnimatorHandler;

    // 判斷是否已經搜尋結束
    private boolean isOver = false;

    private int count = 0;

    public SearchView(Context context) {
        super(context);

        initPaint();

        initPath();

        initListener();

        initHandler();

        initAnimator();

        // 進入開始動畫
        mCurrentState = State.STARTING;
        mStartingAnimator.start();

    }

    private void initPaint() {
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.WHITE);
        mPaint.setStrokeWidth(15);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setAntiAlias(true);
    }

    private void initPath() {
        path_srarch = new Path();
        path_circle = new Path();

        mMeasure = new PathMeasure();

        // 注意,不要到360度,否則內部會自動優化,測量不能取到需要的數值
        RectF oval1 = new RectF(-50, -50, 50, 50);          // 放大鏡圓環
        path_srarch.addArc(oval1, 45, 359.9f);

        RectF oval2 = new RectF(-100, -100, 100, 100);      // 外部圓環
        path_circle.addArc(oval2, 45, -359.9f);

        float[] pos = new float[2];

        mMeasure.setPath(path_circle, false);               // 放大鏡把手的位置
        mMeasure.getPosTan(0, pos, null);

        path_srarch.lineTo(pos[0], pos[1]);                 // 放大鏡把手

        Log.i("TAG", "pos=" + pos[0] + ":" + pos[1]);
    }

    private void initListener() {
        mUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mAnimatorValue = (float) animation.getAnimatedValue();
                invalidate();
            }
        };

        mAnimatorListener = new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                // getHandle發訊息通知動畫狀態更新
                mAnimatorHandler.sendEmptyMessage(0);
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        };
    }

    private void initHandler() {
        mAnimatorHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                switch (mCurrentState) {
                    case STARTING:
                        // 從開始動畫轉換好搜尋動畫
                        isOver = false;
                        mCurrentState = State.SEARCHING;
                        mStartingAnimator.removeAllListeners();
                        mSearchingAnimator.start();
                        break;
                    case SEARCHING:
                        if (!isOver) {  // 如果搜尋未結束 則繼續執行搜尋動畫
                            mSearchingAnimator.start();
                            Log.e("Update", "RESTART");

                            count++;
                            if (count>2){       // count大於2則進入結束狀態
                                isOver = true;
                            }
                        } else {        // 如果搜尋已經結束 則進入結束動畫
                            mCurrentState = State.ENDING;
                            mEndingAnimator.start();
                        }
                        break;
                    case ENDING:
                        // 從結束動畫轉變為無狀態
                        mCurrentState = State.NONE;
                        break;
                }
            }
        };
    }

    private void initAnimator() {
        mStartingAnimator = ValueAnimator.ofFloat(0, 1).setDuration(defaultDuration);
        mSearchingAnimator = ValueAnimator.ofFloat(0, 1).setDuration(defaultDuration);
        mEndingAnimator = ValueAnimator.ofFloat(1, 0).setDuration(defaultDuration);

        mStartingAnimator.addUpdateListener(mUpdateListener);
        mSearchingAnimator.addUpdateListener(mUpdateListener);
        mEndingAnimator.addUpdateListener(mUpdateListener);

        mStartingAnimator.addListener(mAnimatorListener);
        mSearchingAnimator.addListener(mAnimatorListener);
        mEndingAnimator.addListener(mAnimatorListener);
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mViewWidth = w;
        mViewHeight = h;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        drawSearch(canvas);
    }

    private void drawSearch(Canvas canvas) {

        mPaint.setColor(Color.WHITE);


        canvas.translate(mViewWidth / 2, mViewHeight / 2);

        canvas.drawColor(Color.parseColor("#0082D7"));

        switch (mCurrentState) {
            case NONE:
                canvas.drawPath(path_srarch, mPaint);
                break;
            case STARTING:
                mMeasure.setPath(path_srarch, false);
                Path dst = new Path();
                mMeasure.getSegment(mMeasure.getLength() * mAnimatorValue, mMeasure.getLength(), dst, true);
                canvas.drawPath(dst, mPaint);
                break;
            case SEARCHING:
                mMeasure.setPath(path_circle, false);
                Path dst2 = new Path();
                float stop = mMeasure.getLength() * mAnimatorValue;
                float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * 200f));
//                float start = stop-50;
                mMeasure.getSegment(start, stop, dst2, true);
                canvas.drawPath(dst2, mPaint);
                break;
            case ENDING:
                mMeasure.setPath(path_srarch, false);
                Path dst3 = new Path();
                mMeasure.getSegment(mMeasure.getLength() * mAnimatorValue, mMeasure.getLength(), dst3, true);
                canvas.drawPath(dst3, mPaint);
                break;
        }
    }
}
複製程式碼

以上就是關於PathMeasure的詳解和應用,需要讀者去多點動手才能理解其中的精髓的地方。

相關文章