1. 程式人生 > >自定義View實現圓形水波進度條(下)

自定義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,到這裡,自定義的水波形狀的進度條就完成了,再次上效果圖

(注:此水波左右移動是後來加的效果,具體實現點選程式碼檢視)

由於本人目前水平有限,文字若有不足的地方,歡迎指正,謝謝。