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])
;
作用:用於獲取路徑上某點的座標以及該位置的正切值,即切線的座標。相當於是getPos
、getTan
兩個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動畫效果
- 在自定義View構造方法中呼叫Paint的
setStyle
、setStrokeWidth
方法初始化畫筆基本屬性。 - 建立Path路徑物件,繪製一個空心圓;建立PathMeasure物件,呼叫
setPath
方法關聯Path,並呼叫getLength
獲取路徑長度,建立Dst物件,後續會使用。 - 建立動畫ValueAnimator,呼叫
ofFloat(0, 1)
方法,此處的(0, 1)
範圍代表百分比例,即繪製圓的比例從0到100%。再設定線性插值器和迴圈播放,重點在於實現動畫的監聽事件中獲取變化的比例值賦值給成員變數,呼叫invalidate();
重新整理。 - 以上都是在構造方法中實現,準備就緒後,接下來在
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部落格中擷取的內容,可見:
- 首先呼叫PathMeasure的
getLength
方法獲取Path路徑的全長度length; - 接下來就是重點應用Dash風格效果:建立DashPathEffect,設定實線、虛線的長度都為length,而第三個引數則是起始偏移量偏移量;
- 最後將此效果設定到Paint畫筆中,canvas繪製即可;
- 後續我們再自己建立動畫,將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
方法。首先來看第一個效果實現步驟:
- 在構造方法中建立並設定Paint畫筆基本屬性;建立Path路徑添設定圓的軌跡;建立PathMeasure物件關聯Path;建立
getPosTan
方法中需要的Pos、T an陣列,留以後用; - 在構造方法中建立接著建立動畫,線性插值器,偏移量[0,1]變化,都是一些常規設定。
- 在
onDraw
方法中呼叫PathMeasure的getPosTan
方法,注意回顧此方法要求的三個引數資訊,分別是距離 Path 起點的長度(取值範圍[0, getLength]
)、座標值陣列、切點陣列,因此此處我們傳入的引數分別是:動畫偏移量百分比*length、兩個新建立的陣列。呼叫此方法後,後序繪製時可以利用Pos陣列,即沿著圓軌跡移動的座標值來繪製移動的小圓圈! - 先使用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;
}
}
}
若有錯誤,虛心指教~