1. 程式人生 > >自定義SurfaceView之音訊錄製圓形進度條

自定義SurfaceView之音訊錄製圓形進度條

本篇文章介紹自定義SurfaceView來實現如下的效果

這裡寫圖片描述

由於對於SurfaceView不是很熟練,這次拿它來練手

SurfaceView用途:

一般View可以滿足大部分的繪圖需求,但如果需要併發執行復雜耗時的邏輯的時候,就會不斷阻塞主執行緒,導致畫面卡頓,為了避免這種問題的發生,我們應該使用SurfaceView來解決這個問題

SurfaceView使用介紹可以參考另外一篇部落格:Android繪圖機制與處理技巧(一)SurfaceView

總共由以下幾個部分組成:

  • 按鈕按下效果
  • 灰色總進度條
  • 綠色圓形進度條
  • 綠色小圓圈

具體實現過程分析:

建立自定義SurfaceView需要繼承自SurfaceView,並實現SurfaceHolder.Callback, Runnable介面

public class CircleRecordSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {

然後需要重寫三個方法:surfaceCreated(),surfaceChanged(),surfaceDestroyed()和run()

定義的成員變數,具體用途可以看註釋

//選擇按鈕圖示
    private boolean isChangeCenterBitmap = true;
    //持續畫圖
    private boolean isSustainedDraw = false
; private boolean isStart = true; //是否畫小圓點,預設為true private boolean isDrawSmallCircle = true; //小圓點顏色 private int smallCircleColor; //是否畫圓弧,預設為true private boolean isDrawArc = true; //圓弧顏色 private int arcColor; private CompleteTimeCallBack completeTimeCallBack; private
SurfaceHolder holder = null; //繪圖屬性--------- private Canvas canvas; //錄音按鈕 private Paint pPaint; private int px;//座標x位置 private int py;//座標y位置 //radius = defaultRadius * dp private int radius;//半徑 //defaultRadius 預設值為40 private int defaultRadius = 40; //起始角度 private float startAngle = 270; //進度 private float sweepAngle; //小球起始角度預設等於進度條起始角度 private float angle, duration = 20; private int startBitmap; private int stopBitmap; //中心圖片的範圍,預設為10,值越大圖片越小 private int centerBitmap_margin = 10; private int dp; private Bitmap bitmap; private boolean isGetBitmap = false; long a, b, calculateTime, sleepTime = 60, correctSleepTime;

然後我們需要對SurfaceHolder以及一些其他的屬性初始化

public void init() {

        //獲取mSurfaceHolder
        holder = getHolder();
        holder.addCallback(this);
        //背景設為透明
        if (!isInEditMode()) {
            setZOrderOnTop(true);
        }
        holder.setFormat(PixelFormat.TRANSLUCENT);

        //設定進度條
        pPaint = new Paint();
        pPaint.setAntiAlias(true);
        pPaint.setStrokeWidth(4);
        pPaint.setStyle(Paint.Style.STROKE);
        angle = startAngle;
        sweepAngle = 0;

        dp = Resources.getSystem().getDisplayMetrics().densityDpi / 160;

    }

重寫SurfaceView的surfaceCreated()方法

@Override
    public void surfaceCreated(SurfaceHolder holder) {
        if (!isStart) {
            reset();
        }
        radius = defaultRadius * dp;
        isChangeCenterBitmap = true;
        px = this.getWidth() / 2;
        py = this.getHeight() / 2;
        new Thread(this).start();
    }

在這裡根據子執行緒標誌位做初始化,以及計算半徑,中心點座標等等,並開啟執行緒

由於我們不用改變SurfaceView大小因此無需在surfaceChanged()方法中寫邏輯

@Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

重寫SurfaceView的surfaceDestroyed()方法,在這裡將標誌位設為false

@Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        isStart = false;
    }

然後重寫run()方法,該方法是一個子執行緒,在這裡通過不停迴圈才進行繪製介面

@Override
    public void run() {
        while (isStart) {
            if (isSustainedDraw) {
                canvas = holder.lockCanvas(); // 獲得畫布物件,開始對畫布畫畫
                if (canvas == null) {
                    continue;
                }
                canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
                //                canvas.drawColor(canvasColor); // 把畫布填充指定顏色
                drawCenterBitmap();
                drawCircle();
                holder.unlockCanvasAndPost(canvas); // 完成畫畫,把畫布顯示在螢幕上

                calculateTime = calculateTime + sleepTime;
                try {
                    b = a;
                    a = System.currentTimeMillis();

                    if (b == 0) {
                        correctSleepTime = sleepTime;

                    } else {
                        if ((a - b) >= sleepTime && (a - b) < 2 * sleepTime) {
                            correctSleepTime = sleepTime - (a - b - correctSleepTime);
                        } else if ((a - b) > 2 * sleepTime) {
                            correctSleepTime = 0;
                            //不睡眠
                        } else if ((a - b) < sleepTime) {
                            correctSleepTime = sleepTime - (a - b - correctSleepTime);
                        }
                    }
                    if (correctSleepTime > 0) {
                        Thread.sleep(correctSleepTime);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else if (isChangeCenterBitmap) {
                canvas = holder.lockCanvas(); // 獲得畫布物件,開始對畫布畫畫
                if (canvas == null) {
                    continue;
                }
                canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
                //                canvas.drawColor(canvasColor); // 把畫布填充指定顏色
                drawCenterBitmap();
                drawCircle();
                holder.unlockCanvasAndPost(canvas); // 完成畫畫,把畫布顯示在螢幕上
            }
        }
    }

有幾個要注意的地方:

  • lockCanvas()用來獲取當前canvas繪圖物件,繪製完畢後通過holder.unlockCanvasAndPost(canvas)方法來提交畫布內容
  • 由於每次迴圈都是通過lockCanvas()來獲取的canvas物件,因此會保留上一次的繪圖操作,所以我們每次回之前都需要通過drawcolor()來進行清屏操作
  • isSustainedDraw標記位來判斷手指持續按住螢幕的狀態,在按下的狀態下才會重新整理
  • isChangeCenterBitmap標記位用來在手指不觸碰螢幕的情況下限制螢幕只重新整理一次,節省資源
  • 使用Thread.sleep(correctSleepTime)儘可能的節省系統資源

畫按鈕圖形的方法

private void drawCenterBitmap() {
        int area = radius - centerBitmap_margin * dp;
        RectF imageRect = new RectF(px - area, py - area, px + area, py + area);

        if (isChangeCenterBitmap) {
            if (!isGetBitmap) {
                if (isSustainedDraw) {
                    bitmap = BitmapFactory.decodeResource(getResources(), stopBitmap);
                } else {
                    bitmap = BitmapFactory.decodeResource(getResources(), startBitmap);
                }
            }
            isChangeCenterBitmap = false;
        }
        canvas.drawBitmap(bitmap, null, imageRect, pPaint);
    }

通過isSustainedDraw判斷後使用canvas.drawBitmap()方法繪製相應的圖片

通過這個方法繪製灰色大圓、綠色圓點和圓弧

public void drawCircle() {

        //繪製大圓
        pPaint.setColor(Color.LTGRAY);
        pPaint.setStyle(Paint.Style.STROKE);
        canvas.drawCircle(px, py, radius, pPaint);

        //繪製原點
        if (isDrawSmallCircle) {
            pPaint.setColor(smallCircleColor);
            pPaint.setStyle(Paint.Style.FILL);

            //radians=angle * Math.PI / 180 角度轉弧度公式
            //Math.cos(radians) * defaultRadius cos計算x軸偏移量,sin計算y軸偏移量
            float ballX = (float) (px + radius * Math.cos(angle * Math.PI / 180));
            float ballY = (float) (py + radius * Math.sin(angle * Math.PI / 180));
            canvas.drawCircle(ballX, ballY, 4 * dp, pPaint);
        }

        //繪製圓弧
        if (isDrawArc) {
            pPaint.setStyle(Paint.Style.STROKE);
            //            pPaint.setColor(Color.parseColor(arcColor));
            pPaint.setColor(arcColor);
            RectF rect = new RectF(px - radius, py - radius, px + radius, py + radius);
            canvas.drawArc(rect, startAngle, sweepAngle, false, pPaint);//畫弧形
        }

        float speed = 360 / (duration * (1000 / sleepTime));
        //        Log.i(TAG, "drawCircle: speed:"+speed);
        angle = angle + speed;
        if (angle > 360) {
            angle = 0;
        }
        sweepAngle = sweepAngle + speed;
        if (sweepAngle > 360) {
            reset();
            if (completeTimeCallBack != null) {
                completeTimeCallBack.stop();
            }
            isSustainedDraw = false;
            isChangeCenterBitmap = true;
        }
    }

畫大圓較簡單,設定好paint屬性後使用drawCircle()方法即可
畫圓弧也比較容易,設定好RectF類的座標和paint屬性後,使用drawArc()方法即可
需要注意的是畫圓點,重點在於怎麼計算圓點圍繞中心旋轉時的座標:
我們可以使用Math.cos(radians) * radius來計算X軸偏移量,Math.sin(radians) * radius來計算Y軸偏移量,radians表示弧度,可以通過angle * Math.PI / 180來計算

用到的包裝方法,可以設定一些屬性

 public void startDraw() {
        isChangeCenterBitmap = true;
        isSustainedDraw = true;
    }

    public void stopDraw() {
        isChangeCenterBitmap = true;
        isSustainedDraw = false;
    }

    public void reset() {
        isStart = true;
        angle = startAngle;
        sweepAngle = 0;
    }

    //設定圓弧顏色,用#RRGGBB 或者 #AARRGGBB
    public void setArcColor(int arcColor) {
        this.arcColor = arcColor;
    }

    //設定小圓點顏色,用#RRGGBB 或者 #AARRGGBB
    public void setSmallCircleColor(int smallCircleColor) {
        this.smallCircleColor = smallCircleColor;
    }

    public void setDefaultRadius(int defaultRadius) {
        this.defaultRadius = defaultRadius;
    }

    public void setStartBitmap(int startBitmap) {
        this.startBitmap = startBitmap;
    }

    public void setStopBitmap(int stopBitmap) {
        this.stopBitmap = stopBitmap;
    }

    public void setDuration(float duration) {
        this.duration = duration;
    }

xml佈局

<com.gavinandre.customviewsamples.view.CircleRecordSurfaceView
        android:id="@+id/circle_record_view"
        android:layout_width="110dp"
        android:layout_height="110dp"
        android:layout_centerInParent="true"/>

使用方法

mCircleRecordView.setDuration(6);
        mCircleRecordView.setStartBitmap(R.mipmap.audio_record_mic_btn);
        mCircleRecordView.setStopBitmap(R.mipmap.audio_record_mic_btn_press);
        mCircleRecordView.setArcColor(ContextCompat.getColor(this, R.color.record_green));
        mCircleRecordView.setSmallCircleColor(ContextCompat.getColor(this, R.color.record_green));
        mCircleRecordView.setDefaultRadius(50);
        mCircleRecordView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        mCircleRecordView.startDraw();
                        break;
                    case MotionEvent.ACTION_UP:
                        mCircleRecordView.reset();
                        mCircleRecordView.stopDraw();
                        break;
                    default:
                        break;
                }
                return true;
            }
        });

如果要寫其他邏輯的話在onTouch方法裡新增即可