自定義View實現圓形水波進度條(下)
來源:伯樂線上專欄作者 - Code4Android
連結:http://android.jobbole.com/84776/
接上文
通過效果圖,我們看到實現此效果就是不斷的更新進度值,然後重繪,,那麼我們只需開啟一個執行緒實現更新進度值,為了更好的控制我們再加點選事件,當單機時開始增大進度,雙擊時暫停進度,並彈出Snackbar,其中有一個重置按鈕,點選重置時將進度設定為0,重繪介面。
響應點選事件
因為要實現雙擊事件,我們可以直接用GestureDetector(手勢檢測),通過這個類我們可以識別很多的手勢,主要是通過他的onTouchEvent(event)方法完成了不同手勢的識別GestureDetector裡有一個內部類 SimpleOnGestureListener。SimpleOnGestureListener類是GestureDetector提供給我們的一個更方便的響應不同手勢的類,這個類實現了上述兩個介面(OnGestureListener, OnDoubleTapListener,但是所有的方法體都是空的),該類是static class,也就是說它實際上是一個外部類。程式設計師可以在外部繼承這個類,重寫裡面的手勢處理方法
public static class SimpleOnGestureListener implements OnGestureListener, OnDoubleTapListener, OnContextClickListener { //單擊擡起 public boolean onSingleTapUp(MotionEvent e) { return false; } //長按 public void onLongPress(MotionEvent e) { } //滾動 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { return false; } //快速滑動 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; } // public void onShowPress(MotionEvent e) { } public boolean onDown(MotionEvent e) { return false; } public boolean onDoubleTap(MotionEvent e) { return false; } public boolean onDoubleTapEvent(MotionEvent e) { return false; } public boolean onSingleTapConfirmed(MotionEvent e) { return false; } public boolean onContextClick(MotionEvent e) { return false; } }
下面是我們自定繼承SimpleOnGestureListener,由於我們只要響應單擊和雙擊事件,那麼我們只需要重寫onDoubleTap雙擊(),onSingleTapConfirmed(單擊)方法即可,
public class MyGestureDetector extends GestureDetector.SimpleOnGestureListener { @Override public boolean onDoubleTap(MotionEvent e) { getHandler().removeCallbacks(singleTapThread); singleTapThread=null; Snackbar.make(CustomBall.this, "暫停進度,是否重置進度?", Snackbar.LENGTH_LONG).setAction("重置", new OnClickListener() { @Override public void onClick(View v) { currentProgress=0; invalidate(); } }).show(); return super.onDoubleTap(e); } @Override public boolean onSingleTapConfirmed(MotionEvent e) { Snackbar.make(CustomBall.this, "單機了", Snackbar.LENGTH_LONG).setAction("Action", null).show(); startProgressAnimation(); return super.onSingleTapConfirmed(e); } }
當點選時Snackbar做個提醒單擊了View,然後呼叫startProgressAnimation()方法初始化一個執行緒,通過postDelayed將執行緒加入的訊息佇列,延遲100ms執行,通過singleTapThread == null判斷條件,避免過多的建立物件
private void startProgressAnimation() { if (singleTapThread == null) { singleTapThread = new SingleTapThread(); getHandler().postDelayed(singleTapThread, 100); } }
我們將SingleTapThread 實現Runnable介面,在run方法裡書寫我們的處理邏輯,其實很簡單,先判斷當前進度值是不是大於最大進度(100),如果小於最大的值,我們就將currentProgress(當前進度值)加1的操作,然後呼叫invalidate()方法重繪介面,之後還需要再次將執行緒加入訊息佇列,依然延遲100ms執行。對於當如果當前進度已經載入到100%,此時我們將此執行緒從訊息佇列移除。
private class SingleTapThread implements Runnable { @Override public void run() { if (currentProgress < maxProgress) { currentProgress++; invalidate(); getHandler().postDelayed(singleTapThread, 100); } else { getHandler().removeCallbacks(singleTapThread); } } }
接下來還需要註冊事件,我們可以在onDraw()方法中通過GestureDetector的構造方法可以將自定義的MyGestureDetector物件傳遞進去,然後通setOnTouchListener設定監聽器,這樣GestureDetector能處理不同的手勢了
if (detector==null){ detector = new GestureDetector(new MyGestureDetector()); setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return detector.onTouchEvent(event); } }); }
還有最重要的一點是,View預設是不可點選的,所以我們需要 setClickable(true)設定View可點選的,OK,到這裡我們就完成的中心進度值得更新,接下來就開始繪製裡面的波浪形狀,效果圖如下
實現水波浪效果
水波紋效果是通過二階貝塞爾曲線實現的,先簡單看下什麼是貝塞爾曲線
在數學的數值分析領域中,貝塞爾曲線(英語:Bézier curve)是電腦圖形學中相當重要的引數曲線。更高維度的廣泛化貝塞爾曲線就稱作貝塞爾曲面,其中貝塞爾三角是一種特殊的例項。
貝塞爾曲線於1962年,由法國工程師皮埃爾·貝塞爾(Pierre Bézier)所廣泛發表,他運用貝塞爾曲線來為汽車的主體進行設計。貝塞爾曲線最初由Paul de Casteljau於1959年運用de Casteljau演算法開發,以穩定數值的方法求出貝塞爾曲線 – – – – -維基百科
-
線性貝塞爾曲線
給定點P0、P1,線性貝塞爾曲線只是一條兩點之間的直線。這條線由下式給出:
繪製效果為
-
二次方貝塞爾曲線
二次方貝塞爾曲線的路徑由給定點P0、P1、P2的函式B(t)追蹤:
-
三次方貝塞爾曲線
P0、P1、P2、P3四個點在平面或在三維空間中定義了三次方貝塞爾曲線。曲線起始於P0走向P1,並從P2的方向來到P3。一般不會經過P1或P2;這兩個點只是在那裡提供方向資訊。P0和P1之間的間距,決定了曲線在轉而趨進P2之前,走向P1方向的“長度有多長”。
曲線的引數形式為:
當然貝塞爾曲線是一個很複雜的東西,他可以延伸N階貝塞爾曲線,如果想要真正搞明白,想自定義比較複雜或者比較酷炫的動畫,那高等數學知識必須要搞明白,很多時候,我們只需要瞭解二次貝塞爾曲線就可以了,或者說,即使貝塞爾曲線不是那麼熟悉,也不用怕,android API 封裝了常用的貝塞爾曲線,我們只需要傳入座標就可以實現很多動畫。
首先我們需要初始化貝塞爾曲線區域的畫筆設定。其中重要的一點就是setXfermode()方法,此方法可以設定與其他繪製圖形的交集,合集,補集等運算,在這個專案中,我們使用了交集(繪製貝塞爾曲線區域和圓區域的交集)
progressPaint = new Paint(); progressPaint.setAntiAlias(true); progressPaint.setColor(progressColor); //取兩層繪製交集。顯示上層 progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
初始化畫筆後,就開始繪製我們的圖形,先初始化一個
寬和高都為radius * 2的正方形畫布作為緩衝區畫布,我們可以先在緩衝區畫布繪製,繪製完成後一次再繪製到畫布上。
bitmap = Bitmap.createBitmap((int)radius * 2,(int)radius * 2,Bitmap.Config.ARGB_8888); bitmapCanvas = newCanvas(bitmap);
然後繪製圓心(width / 2, height / 2)半徑為radius的圓
bitmapCanvas.drawCircle(width / 2,height / 2,radius,roundPaint);
水波從圓的最下方開始(進度為0),到最上方(進度最大值maxProgress)結束,那麼我們需要根據當前進度值動態計算水波的高度
floaty = (1 - (float)currentProgress / maxProgress) * radius * 2
如圖,我們就可以先將path.lineTo將每個點連起來,可以先從(width,y)繪製,那麼需要呼叫path.moveTo(width, y);方法將操作點移動到該座標上,接下下就開始依次連線其餘三個點(width,height),(0,height),(0,y)。由於我們之前畫筆設定的是取交集(progressPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN))),所以此時會繪製與圓相交的部分,也就是圓內的部分。
下面就是繪製貝塞爾曲線
path.rQuadTo(space, -d,space * 2,0); path.rQuadTo(space,d,space * 2,0);
第一個是繪製向下彎曲,第二個是繪製向上彎曲。為了從左到右都繪製曲線,我們根據圓的直徑計算一下,需要幾次才能平鋪,然後迴圈執行上面兩句,直到平鋪圓形區域,為了展示當進度增大時將波紋幅度降低的效果(直到進度為100%,幅度降為0)我們根據當前進度值動態計算了幅度值,計算方法如下
floatd = (1 - (float)currentProgress / maxProgress) *space;
由於我們需要以實心的方式繪製區域,那麼我們呼叫
path.close();將所畫區域封閉,也就是實心的效果。
path.close(); bitmapCanvas.drawPath(path,progressPaint);
Ok,到這裡,自定義的水波形狀的進度條就完成了,再次上效果圖
(注:此水波左右移動是後來加的效果,具體實現點選程式碼檢視)
由於本人目前水平有限,文字若有不足的地方,歡迎指正,謝謝。