自定義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方法裡新增即可