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

(注:此水波左右移動是後來加的效果,具體實現點擊代碼查看)



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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章