1. 程式人生 > >Android 高階UI解密 (五) :PathMeasure擷取片段 與 切線(新思路實現軌跡變換)

Android 高階UI解密 (五) :PathMeasure擷取片段 與 切線(新思路實現軌跡變換)

前面幾篇文章已經按照順序講解了Paint畫筆、Canvas畫布、Path相關內容了,也許沒有面面俱到,但特地強調了其重點內容。有關Path的內容只講解了貝塞爾曲線繪製,日後再做補充。此篇文章將介紹另外一個重點內容:PathMeasure。

PathMeasure類明顯是用來輔助Path類的,其API方法很少,但是有兩個王牌,即擷取片段getSegment方法和獲取指定長度的位置座標及該點切線值tanglegetPosTan方法。前者容易瞭解,擷取部分曲線或圖形片段處理,而後者的獲取指定點切線值,這個充滿數學魅力的API,

(此係列文章知識點相對獨立,可分開閱讀,不過筆者建議按照順序閱讀,理解更加深入清晰)

此篇涉及到的知識點如下:

  • PathMeasure基礎API介紹
  • PathMeasure實踐Loading效果和切線
  • 新思路實現軌跡變換動畫

一. PathMeasure基礎API介紹

顧名思義,PathMeasure是一個用來測量Path的類,它的方法比較少,以下先來介紹API基本使用。

1. 構造方法

方法名 釋義
PathMeasure() 建立一個空的PathMeasure
PathMeasure(Path path, boolean forceClosed) 建立 PathMeasure 並關聯一個指定的Path(Path需要已經建立完成)。

(1)無參建構函式

PathMeasure()

用這個建構函式可建立一個空的 PathMeasure,但是使用之前需要先呼叫 setPath 方法來與 Path 進行關聯。被關聯的 Path 必須是已經建立好的。如果關聯之後 Path 內容進行了更改,則需要使用 setPath 方法重新關聯。

(2)有參建構函式

PathMeasure (Path path, boolean forceClosed)
  • Path path:被關聯的 Path ;
  • boolean forceClosed:用來確保 Path 閉合,如果設定為 true, 則不論之前Path是否閉合,都會自動閉合該 Path(如果Path可以閉合的話);

用這個建構函式是建立一個 PathMeasure 並關聯一個 Path, 其實和建立一個空的 PathMeasure 後呼叫 setPath 進行關聯效果是一樣的。同樣,被關聯的 Path 也必須是已經建立好的。如果關聯之後 Path 內容進行了更改,則需要使用 setPath 方法重新關聯。

注意forceClosed 引數:

  • 不論 forceClosed 設定為何種狀態(true 或者 false), 都不會影響原有Path的狀態,即 Path 與 PathMeasure 關聯之後,之前的的 Path 不會有任何改變。
  • forceClosed 的狀態設定可能會影響測量結果。如果 Path 未閉合,例如繪製的是未閉合的矩形,但在與 PathMeasure 關聯的時候設定 forceClosed 為 true 時,測量結果可能會比 Path 實際長度稍長一點,即測量了矩形的四條邊而不是三條,獲取到到是該 Path 閉合時的狀態。

2. 公共方法

返回值 方法名 釋義
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) 獲取指定長度的位置座標及該點切線值tangle
boolean getMatrix(float distance, Matrix matrix, int flags) 設定距離為0 <= distance <= getLength(),然後計算相應的矩陣

(1)setPath方法

void setPath(Path path, boolean forceClosed)

作用:此方法是 PathMeasure 與 Path 關聯的重要方法,效果和建構函式中兩個引數的作用是一樣的。

(2)isClosed方法

boolean isClosed()

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

(3)getLength方法

float getLength()

作用:此方法用於獲取 Path 路徑的總長度。

(4)nextContour方法

boolean nextContour()

作用: Path 可以由多條曲線構成,但不論是 getLength 方法, 還是getgetSegment 或者其它方法,都只會在其中第一條線段上執行。此 nextContour方法 就是用於跳轉到下一條曲線到方法。如果跳轉成功,則返回 true, 如果跳轉失敗,則返回 false。

(5)getSegment方法

boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
  • 返回值boolean:判斷擷取是否成功(true 表示擷取成功,結果存入dst中,false 擷取失敗,不會改變dst中內容);
  • float startD:開始擷取位置距離 Path 起點的長度(取值範圍: 0 <= startD < stopD <= Path總長度);
  • float stopD:結束擷取位置距離 Path 起點的長度(取值範圍: 0 <= startD < stopD <= Path總長度);
  • Path dst:擷取的 Path 將會新增到 dst 中(注意: 是新增,而不是替換);
  • boolean startWithMoveTo:起始點是否使用 moveTo,用於保證擷取的 Path 第一個點位置不變(true表示保證擷取得到的 Path 片段不會發生形變,false表示保證儲存擷取片段的 Path(dst) 的連續性);

作用:用於獲取Path路徑的一個片段。(如果 startD、stopD 的數值不在取值範圍 [0, getLength] 內,或者 startD == stopD 則返回值為 false,不會改變 dst 內容)。

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

(6)getPosTan方法

boolean getPosTan(float distance, float[] pos, float[] tan)
  • 返回值(boolean):判斷獲取是否成功(true表示成功,資料會存入 pos 和 tan 中,false 表示失敗,pos 和 tan 不會改變);
  • float distance:距離 Path 起點的長度 取值範圍: 0 <= distance <= getLength
  • float[] pos:該點的座標值,座標值: (x==[0], y==[1])
  • float[] tan:該點的正切值,正切值: (x==[0], y==[1])

作用:用於獲取路徑上某點的座標以及該位置的正切值,即切線的座標。相當於是getPosgetTan兩個API的集合。

//用於獲取路徑上某點的切線角度
(math.atan2(tan[1], tan[0])*180.0 / math.PI)

上面程式碼是常用的一個公式,用於獲取路徑上某點的切線角度通過 tan 得值計算出圖片旋轉的角度,tan 是 tangent 的縮寫,即中學中常見的正切, 其中tan0是鄰邊邊長,tan1是對邊邊長,而Math中 atan2 方法是根據正切是數值計算出該角度的大小,得到的單位是弧度,所以上面又將弧度轉為了角度

(7)getMatrix方法

boolean getMatrix(float distance, Matrix matrix, int flags) 
  • 返回值(boolean):判斷獲取是否成功(true表示成功,資料會存入matrix中,false 失敗,matrix內容不會改變);
  • float distance:距離 Path 起點的長度(取值範圍: 0 <= distance <= getLength);
  • Matrix matrix:根據 falgs 封裝好的matrix,會根據 flags 的設定而存入不同的內容;
  • int flags:規定哪些內容會存入到matrix中(可選擇POSITION_MATRIX_FLAG位置 、ANGENT_MATRIX_FLAG正切 );

作用:用於得到路徑上某一長度的位置以及該位置的正切值的矩陣。

二. PathMeasure實踐

1. 實現Loading動畫效果

  1. 在自定義View構造方法中呼叫Paint的setStylesetStrokeWidth方法初始化畫筆基本屬性。
  2. 建立Path路徑物件,繪製一個空心圓;建立PathMeasure物件,呼叫setPath方法關聯Path,並呼叫getLength獲取路徑長度,建立Dst物件,後續會使用。
  3. 建立動畫ValueAnimator,呼叫ofFloat(0, 1)方法,此處的(0, 1)範圍代表百分比例,即繪製圓的比例從0到100%。再設定線性插值器和迴圈播放,重點在於實現動畫的監聽事件中獲取變化的比例值賦值給成員變數,呼叫invalidate();重新整理。
  4. 以上都是在構造方法中實現,準備就緒後,接下來在onDraw方法中進行繪製,繪製圓的起點當然是0,終點則是隨著動畫漸變成圓,為mLength * mAnimValue;,即圓比例值*繪製路徑總長度。有了這兩個float值後,可使用PathMeasure的getSegment(start, stop, mDst, true)方法獲取到對應路徑,接下來再呼叫熟悉的canvas 繪製drawPath(mDst, mPaint)即可。

注意,在onDraw方法中一開始除了需要重置mDst外,還需要呼叫Dst.lineTo(0, 0)方法,這是Android硬體加速的一個小bug,若不呼叫則getSegment(start, stop, mDst, true)方法可能不起作用。

public class PathTracingView extends View {
    private Path mDst;
    private Path mPath;
    private Paint mPaint;
    private float mLength;
    private float mAnimValue;

    private PathMeasure mPathMeasure;
    ......

    public PathTracingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //設定Paint畫筆基本屬性
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);

        mPath = new Path();
        mDst = new Path();

        mPath.addCircle(400, 400, 100, Path.Direction.CW);
        mPathMeasure = new PathMeasure();
        mPathMeasure.setPath(mPath, true);

        mLength = mPathMeasure.getLength();

        ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
        animator.setDuration(1000);
        animator.setInterpolator(new LinearInterpolator());
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mAnimValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        animator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mDst.reset();
        mDst.lineTo(0, 0);

        float stop = mLength * mAnimValue;
        float start = 0//float start = (float) (stop - ((0.5 - Math.abs(mAnimValue - 0.5)) * mLength));

        mPathMeasure.getSegment(0, stop, mDst, true);
        //mPathMeasure.getSegment(start, stop, mDst, true);
        canvas.drawPath(mDst, mPaint);
    }
}

此部分的實現重點在於對PathMeasure的運用,首先獲取動畫實時變化的圓比例,呼叫getSegment方法獲取圓的指定路徑,canvas將其繪製出來。效果如下:

這裡寫圖片描述

在見識到PathMeasure的精彩之處後,發現上面這個Loading繪製太普通了,怎麼著也要來點特效~只需要改變兩行程式碼就可以實現Windows的開機Loading效果圖。

這裡寫圖片描述

效果如上,比起第一個要酷炫不少吧~只需要將onDraw方法中將float start = 0;改成

//修改成Windows的Loading效果
float start = (float) (stop - ((0.5 - Math.abs(mAnimValue - 0.5)) * mLength));
mPathMeasure.getSegment(start, stop, mDst, true);

可以發現stop的值沒有修改,仍舊是從[0, 圓周長長度] 之間的變化,可是start值看似有些複雜,決定於stop、mAnimValue的值。先來分析動畫效果,可把它分成上半圓、下半圓效果來看。這意味著:

  • 當mAnimValue小於0.5時,即繪製不到半圓時,start還是0,繪製下半圓效果跟第一個相同。
  • 當mAnimValue大於0.5時,即可以繪製整圓時,經過運算的start越趨近於stop,因此其效果出現的是上半圓。

因此可見各種絢麗的動畫效果,對座標進行簡單的數學計算就可以實現。

2. 實現軌跡動畫的新思路

關於軌跡動畫的實現,通常是使用VectorDrawable或者Path來實現,但一位Android大神Romain Guy提出了一種新的實現思路:Path Tracing Trick,此小節結合新的思路來實現軌跡動畫效果。

這裡寫圖片描述

如上圖所示這幾種不同的線條效果,通過設定畫筆Paint屬性即可完成。重點檢視第三種Dash風格,實質是由實線、虛線組合而成,在程式碼設定Dash風格時需要傳入兩個引數:實線長度和虛線長度。

那麼舉一反三,如果要實現一個佈景的繪製動畫,通過設定畫筆Paint的Dash風格,將實線和虛線的長度都設定為佈景的長度,那麼佈景初始時的顯示是一條實線或一條虛線,通過最後一個引數偏移量的設定,令全部都是虛線(即空白)的圖形不斷的被虛線所填充,從而可以實現軌跡動畫的效果。

Romain Guy提出的如上思路的確令人耳目一新,以Paint畫筆特有的Dash實、虛線風格(即DashPathEffect),再借助動畫的偏移量位移,從而可以實現軌跡偏移的動畫效果,接下來學習實現這個抽象的思路。

這裡寫圖片描述

上圖中程式碼演示是Romain Guy部落格中擷取的內容,可見:

  1. 首先呼叫PathMeasure的getLength方法獲取Path路徑的全長度length;
  2. 接下來就是重點應用Dash風格效果:建立DashPathEffect,設定實線、虛線的長度都為length,而第三個引數則是起始偏移量偏移量;
  3. 最後將此效果設定到Paint畫筆中,canvas繪製即可;
  4. 後續我們再自己建立動畫,將DashPathEffect第三個引數偏移量改成動畫指定的偏移量,即可完成實線、虛線交錯(路徑軌跡)的動畫效果。

完整程式碼如下,配上註釋並不難理解:

public class PathPaintView extends View {

    private Path mPath;
    private Paint mPaint;
    private float mLength;
    private float mAnimValue;
    private PathEffect mEffect;

    private PathMeasure mPathMeasure;

    public PathPaintView(Context context) {
        super(context);
    }

    public PathPaintView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);

        mPath = new Path();

        //繪製三角形
        mPath.moveTo(100, 100);
        mPath.lineTo(100, 500);
        mPath.lineTo(400, 300);
        mPath.close();

        //設定PathMeasure
        mPathMeasure = new PathMeasure();
        mPathMeasure.setPath(mPath, true);

        //獲取軌跡路徑全長度
        mLength = mPathMeasure.getLength();

        //設定動畫,線性插值器數值從百分比[0,1]變化
        ValueAnimator animator = ValueAnimator.ofFloat(1, 0);
        animator.setDuration(2000);
        animator.setInterpolator(new LinearInterpolator());
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                //獲取動畫偏移量
                mAnimValue = (float) valueAnimator.getAnimatedValue();
                //建立Paint畫筆的DashPathEffect效果,三個引數分別為:實線、虛線長度、起始偏移量(通過變化的百分比乘以路徑長度)
                mEffect = new DashPathEffect(new float[]{mLength, mLength}, mLength * mAnimValue);
                mPaint.setPathEffect(mEffect);
                //重新整理UI
                invalidate();
            }
        });
        animator.start();
    }

    public PathPaintView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mPath, mPaint);
    }
}

繪製出的路徑效果如下,可見這就是實線在不斷替代虛線的過程,即虛線到實線的一個變化效果,這也就對應了以上程式碼中對動畫值的變化設定是[1,0],如果設定成[0,1],則是實線到虛線的變化效果。由此可見,藉助Paint的Dash實虛線變化效果,再結合 PathMeasure的輔助方法獲取路徑長度計算偏移量,即可以新的思路完成路徑軌跡的效果動畫。

這裡寫圖片描述

3. getPosTan繪製切線實踐

在介紹PathMeasure的基本方法中介紹過了getPosTan重點方法,通過一個簡單的切線繪製demo來深入瞭解學習。

這裡寫圖片描述

這裡先給出效果,如上,以繪製的圓形作為輔助更容易理解切線的概念,將以上效果實現分成兩個部分:小圓圈沿著圓的軌跡移動,切線沿著圓的軌跡移動,這些實現都要依賴getPosTan方法。首先來看第一個效果實現步驟:

  1. 在構造方法中建立並設定Paint畫筆基本屬性;建立Path路徑添設定圓的軌跡;建立PathMeasure物件關聯Path;建立getPosTan方法中需要的Pos、T an陣列,留以後用;
  2. 在構造方法中建立接著建立動畫,線性插值器,偏移量[0,1]變化,都是一些常規設定。
  3. onDraw方法中呼叫PathMeasure的getPosTan方法,注意回顧此方法要求的三個引數資訊,分別是距離 Path 起點的長度(取值範圍[0, getLength])、座標值陣列、切點陣列,因此此處我們傳入的引數分別是:動畫偏移量百分比*length、兩個新建立的陣列。呼叫此方法後,後序繪製時可以利用Pos陣列,即沿著圓軌跡移動的座標值來繪製移動的小圓圈!
  4. 先使用canvas的drawPath繪製出大圓,接著呼叫drawCircle繪製沿著圓軌跡移動的小圓圈,而此方法傳入的圓心座標就是Pos陣列!

這裡寫圖片描述

繪製效果如上,接下來就是重頭戲,繪製移動小圓圈相對於大圓的切線,此處需要用到講解該API時的公式:

//用於獲取路徑上某點的切線角度
(math.atan2(tan[1], tan[0])*180.0 / math.PI)

通過以上公式可以獲取到沿著圓軌跡移動的小圓圈的切線角度,有此角度後便可繪製不斷變化的切線,此處有個小技巧,不需要多次重複繪製變化的切線,既然已經知曉變化的角度,直接呼叫canvas的rotate方法變化圓的形狀即可,因為圓即使改變了角度也無任何變化,而其切線則會產生變化。

完整程式碼如下:

public class PathPosTanView extends View  implements View.OnClickListener{

    private Path mPath;
    private float[] mPos;
    private float[] mTan;
    private Paint mPaint;
    private PathMeasure mPathMeasure;
    private ValueAnimator mAnimator;
    private float mCurrentValue;

    public PathPosTanView(Context context) {
        super(context);
    }

    public PathPosTanView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPath = new Path();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);

        mPath.addCircle(0, 0, 200, Path.Direction.CW);

        mPathMeasure = new PathMeasure();
        mPathMeasure.setPath(mPath, false);

        mPos = new float[2];
        mTan = new float[2];

        setOnClickListener(this);

        mAnimator = ValueAnimator.ofFloat(0, 1);
        mAnimator.setDuration(3000);
        mAnimator.setInterpolator(new LinearInterpolator());
        mAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mCurrentValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
    }

    public PathPosTanView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

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

        mPathMeasure.getPosTan(mCurrentValue * mPathMeasure.getLength(), mPos, mTan);
        float degree = (float) (Math.atan2(mTan[1], mTan[0]) * 180 / Math.PI);

        canvas.save();
        canvas.translate(400, 400);
        canvas.drawPath(mPath, mPaint);
        canvas.drawCircle(mPos[0], mPos[1], 10, mPaint);
        canvas.rotate(degree);
        //相對座標
        canvas.drawLine(0, -200, 300, -200, mPaint);
        canvas.restore();
    }

    @Override
    public void onClick(View view) {
        mAnimator.start();
    }
}

三. 綜合例項 —— 搜尋View

最後留一個常見的自定義View供讀者自己奇思妙想去實現,除了用VectorDrawable實現,閱讀過此篇文章可以輕鬆使用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;
        }
    }
}

若有錯誤,虛心指教~