Android自定義View之分貝儀
一、說明
最近在整理自定義View方面的知識,偶爾看到meizu工具箱的分貝儀效果,感覺它的實效效果還挺好的,遂想自己模擬實現練練手,廢話不多說,直接開擼。
二、效果圖
首先看一下效果圖:
看效果還挺炫酷的,隨著分貝的變化,介面會顯示出對應的分貝,並且上面的指標也會轉動,同時,指標的顏色也會變化。兩個小三角分貝指向當前最小分貝和最大分貝的位置。
三、分析
要實現上面分貝儀的效果,首先我們需要實時採集外界的聲音,然後根據聲音大小的變化來改變自定義View的顯示,所以需要兩方面的知識,採集聲音和自定義View,在Android中,採集聲音使用的是MediaRecorder這個類,而分貝資料的顯示則需要用到自定義View方面的知識。
四、程式碼編寫
1、許可權申請
首先使用錄音是需要申請許可權的,在Android6.0以前,許可權申請很簡單,只需要在Manifest檔案中宣告即可,但是在後面的Android版本中,Android對許可權的控制比較嚴格,為了方便許可權的申請,這裡使用了許可權申請框架EasyPermissions,參考:Android EasyPermissions官方庫,高效處理許可權
首先,需要在Manifest檔案中宣告要使用的許可權:
<uses-permission android:name="android.permission.RECORD_AUDIO" />
然後需要在程式碼中動態檢查許可權:
public class DecibelActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks{
@Override
protected void onStart() {
super.onStart();
checkPermission(); // 這裡在onStart方法中檢查許可權,如果有許可權則直接開始錄音
}
@Override
protected void onStop() {
super.onStop();
stopRecord(); // 在onStop方法中開始錄音
}
/**
* 檢查許可權
*/
private void checkPermission() {
String[] perms = {Manifest.permission.RECORD_AUDIO};
if (!EasyPermissions.hasPermissions(this, perms)) { // 如果沒有該許可權,彈出彈框提示使用者申請許可權
EasyPermissions.requestPermissions(this, "為了正常使用,請允許麥克風許可權!", RECORD_AUDIO_PERMISSION_CODE, perms);
} else { // 如果有許可權,直接開始錄音
startRecord();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
//將請求結果傳遞EasyPermission庫處理
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
// 申請許可權成功
@Override
public void onPermissionsGranted(int requestCode, List<String> perms) {
Log.i("liunianprint:", "onPermissionsGranted");
startRecord(); // 在onStart方法中開始錄音
}
// 申請許可權失敗
@Override
public void onPermissionsDenied(int requestCode, List<String> perms) {
Log.i("liunianprint:", "onPermissionsDenied");
if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
showAppSettingsDialog();
}
}
private void showAppSettingsDialog() {
new AppSettingsDialog.Builder(this).setTitle("該功能需要麥克風許可權").setRationale("該功能需要麥克風許可權,請在設定裡面開啟!").build().show();
}
}
2、開始錄音
// 開始錄音
private void startRecord() {
try {
// 建立MediaRecorder並初始化引數
if (this.mMediaRecorder == null) {
this.mMediaRecorder = new MediaRecorder();
}
this.mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
this.mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
this.mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
this.mMediaRecorder.setOutputFile(this.filePath);
this.mMediaRecorder.setMaxDuration(MAX_LENGTH);
// 開始錄音
this.mMediaRecorder.prepare();
this.mMediaRecorder.start();
updateMicStatus(); // 更新分貝值
} catch (Exception e) {
showAppSettingsDialog();
}
}
3、定時採集聲音分貝值並更新到介面
// 根據採集的聲音更新分貝值,並在500毫秒後再次更新
private void updateMicStatus() {
if (this.mMediaRecorder != null) {
double maxAmplitude = ((double) getMaxAmplitude()) / ((double) this.BASE); // 獲得這500毫秒內最大的聲壓值
if (maxAmplitude > 1.0d) {
db = Math.log10(maxAmplitude) * 20.0d; //將聲壓值轉為分貝值
if (this.minDb == 0) {
this.minDb = (int) db;
}
startAnimator(); // 開始動畫更新分貝值
this.lastDb = db;
}
this.handler.postDelayed(this.update, 500); // 通過handler向主執行緒傳送訊息,500毫秒後執行this.update,再次更新分貝值
}
}
// 用來更新分貝值的runnable
Runnable update = new Runnable() {
public void run() {
DecibelActivity.this.updateMicStatus();
}
};
// 獲得兩次呼叫該方法時間內的最大聲壓值
private float getMaxAmplitude() {
if (this.mMediaRecorder == null) {
return 5.0f;
}
try {
return (float) this.mMediaRecorder.getMaxAmplitude(); // 獲得兩次呼叫該方法時間內的最大聲壓值
} catch (IllegalArgumentException e) {
e.printStackTrace();
return 0.0f;
}
}
在updateMicStatus方法中,會每隔500毫秒採集一次這段時間內音量的最大值,並通過動畫來平滑的更新分貝值。
4、通過動畫來平滑的更新分貝值
// 動畫更新分貝值
private void startAnimator() {
cancelAnimator();
valueAnimator = ValueAnimator.ofFloat(new float[]{((float) this.lastDb), (float) (this.db)}); // 通過動畫來平滑的更新兩次分貝值的變化
valueAnimator.setDuration(400); // 設定動畫時長為400
valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float floatValue = ((Float) valueAnimator.getAnimatedValue()).floatValue();
DecibelActivity.this.decibelView.setDb(floatValue);
int intValue = (int) (floatValue);
// 更新到目前為止的最大音量
if (intValue > DecibelActivity.this.maxDb) {
DecibelActivity.this.maxDb = intValue;
DecibelActivity.this.decibelView.setMaxDb(DecibelActivity.this.maxDb);
}
// 更新到目前為止的最小音量
if (intValue < DecibelActivity.this.minDb) {
DecibelActivity.this.minDb = intValue;
DecibelActivity.this.decibelView.setMinDb(DecibelActivity.this.minDb);
}
}
});
valueAnimator.start();
}
5、回收資源,避免記憶體洩露
@Override
protected void onStop() {
super.onStop();
stopRecord(); // 在onStop方法中開始錄音
}
@Override
protected void onDestroy() {
this.handler.removeCallbacks(this.update); // 移除handler中未執行完的訊息,避免記憶體洩露
cancelAnimator(); // 取消動畫,避免記憶體洩露
super.onDestroy();
}
private void cancelAnimator() {
if (valueAnimator != null && valueAnimator.isRunning()) {
valueAnimator.cancel();
}
}
// 停止錄音
private void stopRecord() {
if (this.mMediaRecorder != null) {
try {
this.mMediaRecorder.stop();
this.mMediaRecorder.reset();
this.mMediaRecorder.release();
this.mMediaRecorder = null;
} catch (Exception e) {
this.mMediaRecorder = null;
e.printStackTrace();
}
}
}
我們首先只看Activity部分的程式碼,自定義View部分的程式碼後面再分析,Activity完整程式碼如下:
package com.liunian.androidbasic.decibe;
import android.Manifest;
import android.animation.ValueAnimator;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.WindowManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import com.liunian.androidbasic.R;
import java.util.List;
import pub.devrel.easypermissions.AppSettingsDialog;
import pub.devrel.easypermissions.EasyPermissions;
public class DecibelActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks{
public static final int MAX_LENGTH = 600000;
private double db = 0.0d;
private int BASE = 1;
DecibelView decibelView;
private String filePath;
Handler handler = new Handler();
double lastDb = 0.0d;
MediaRecorder mMediaRecorder;
ValueAnimator valueAnimator;
private int maxDb;
private int minDb = 0;
private static int RECORD_AUDIO_PERMISSION_CODE = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_decibe);
getWindow().setBackgroundDrawable(null);
getSupportActionBar().hide();
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
init();
}
private void init() {
this.decibelView = (DecibelView) findViewById(R.id.decibel_view);
this.filePath = "/dev/null";
}
/**
* 檢查許可權
*/
private void checkPermission() {
String[] perms = {Manifest.permission.RECORD_AUDIO};
if (!EasyPermissions.hasPermissions(this, perms)) { // 如果沒有該許可權,彈出彈框提示使用者申請許可權
EasyPermissions.requestPermissions(this, "為了正常使用,請允許麥克風許可權!", RECORD_AUDIO_PERMISSION_CODE, perms);
} else { // 如果有許可權,直接開始錄音
startRecord();
}
}
// 用來更新分貝值的runnable
Runnable update = new Runnable() {
public void run() {
DecibelActivity.this.updateMicStatus();
}
};
// 獲得兩次呼叫該方法時間內的最大聲壓值
private float getMaxAmplitude() {
if (this.mMediaRecorder == null) {
return 5.0f;
}
try {
return (float) this.mMediaRecorder.getMaxAmplitude(); // 獲得兩次呼叫該方法時間內的最大聲壓值
} catch (IllegalArgumentException e) {
e.printStackTrace();
return 0.0f;
}
}
// 根據採集的聲音更新分貝值,並在500毫秒後再次更新
private void updateMicStatus() {
if (this.mMediaRecorder != null) {
double maxAmplitude = ((double) getMaxAmplitude()) / ((double) this.BASE); // 獲得這500毫秒內最大的聲壓值
if (maxAmplitude > 1.0d) {
db = Math.log10(maxAmplitude) * 20.0d; //將聲壓值轉為分貝值
if (this.minDb == 0) {
this.minDb = (int) db;
}
startAnimator(); // 開始動畫更新分貝值
this.lastDb = db;
}
this.handler.postDelayed(this.update, 500); // 通過handler向主執行緒傳送訊息,500毫秒後執行this.update,再次更新分貝值
}
}
private void cancelAnimator() {
if (valueAnimator != null && valueAnimator.isRunning()) {
valueAnimator.cancel();
}
}
// 動畫更新分貝值
private void startAnimator() {
cancelAnimator();
valueAnimator = ValueAnimator.ofFloat(new float[]{((float) this.lastDb), (float) (this.db)});
valueAnimator.setDuration(400); // 設定動畫時長為400
valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float floatValue = ((Float) valueAnimator.getAnimatedValue()).floatValue();
DecibelActivity.this.decibelView.setDb(floatValue);
int intValue = (int) (floatValue);
// 更新到目前為止的最大音量
if (intValue > DecibelActivity.this.maxDb) {
DecibelActivity.this.maxDb = intValue;
DecibelActivity.this.decibelView.setMaxDb(DecibelActivity.this.maxDb);
}
// 更新到目前為止的最小音量
if (intValue < DecibelActivity.this.minDb) {
DecibelActivity.this.minDb = intValue;
DecibelActivity.this.decibelView.setMinDb(DecibelActivity.this.minDb);
}
}
});
valueAnimator.start();
}
// 開始錄音
private void startRecord() {
try {
// 建立MediaRecorder並初始化引數
if (this.mMediaRecorder == null) {
this.mMediaRecorder = new MediaRecorder();
}
this.mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
this.mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
this.mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
this.mMediaRecorder.setOutputFile(this.filePath);
this.mMediaRecorder.setMaxDuration(MAX_LENGTH);
// 開始錄音
this.mMediaRecorder.prepare();
this.mMediaRecorder.start();
updateMicStatus(); // 更新分貝值
} catch (Exception e) {
showAppSettingsDialog();
}
}
// 停止錄音
private void stopRecord() {
if (this.mMediaRecorder != null) {
try {
this.mMediaRecorder.stop();
this.mMediaRecorder.reset();
this.mMediaRecorder.release();
this.mMediaRecorder = null;
} catch (Exception e) {
this.mMediaRecorder = null;
e.printStackTrace();
}
}
}
@Override
protected void onStart() {
super.onStart();
checkPermission(); // 這裡在onStart方法中檢查許可權,如果有許可權則直接開始錄音
}
@Override
protected void onStop() {
super.onStop();
stopRecord(); // 在onStop方法中開始錄音
}
@Override
protected void onDestroy() {
this.handler.removeCallbacks(this.update); // 移除handler中未執行完的訊息,避免記憶體洩露
cancelAnimator(); // 取消動畫,避免記憶體洩露
super.onDestroy();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
//將請求結果傳遞EasyPermission庫處理
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
// 申請許可權成功
@Override
public void onPermissionsGranted(int requestCode, List<String> perms) {
Log.i("liunianprint:", "onPermissionsGranted");
startRecord(); // 在onStart方法中開始錄音
}
// 申請許可權失敗
@Override
public void onPermissionsDenied(int requestCode, List<String> perms) {
Log.i("liunianprint:", "onPermissionsDenied");
if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
showAppSettingsDialog();
}
}
private void showAppSettingsDialog() {
new AppSettingsDialog.Builder(this).setTitle("該功能需要麥克風許可權").setRationale("該功能需要麥克風許可權,請在設定裡面開啟!").build().show();
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/window_background"
tools:context="com.liunian.androidbasic.decibe.DecibelActivity">
<com.liunian.androidbasic.decibe.DecibelView
android:id="@+id/decibel_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:colorRoundRadius="95dp"
app:currentLineRoundLength="41dp"
app:currentValueTextSize="60sp"
app:lineRoundLength="30dp"
app:lineRoundRadiusBgColor="@color/decibel_bg_round"
app:lineRoundRadiusColor="@color/decibel_round"
app:lineRoundStart="103dp"
app:outsideRoundRadius="141dp"
app:startAngle="145" />
</LinearLayout>
五、自定義View
首先貼上完整程式碼,然後在分析核心部分
package com.liunian.androidbasic.decibe;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.RectF;
import android.graphics.SweepGradient;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import com.liunian.androidbasic.R;
public class DecibelView extends View {
private int maxDb;
static int minDb;
private float lineRoundStart;
private float startAngle;
private Matrix colorRoundMatrix = new Matrix();
private int[] colorRoundColors = new int[]{0xFF009AD6, 0xFF3CDF5F, 0xFFCDD513, 0xFFFF4639, 0xFF26282C};
private SweepGradient colorRoundSweepGradient;
Paint currentValuePaint;
Paint lineRoundRadiusPaint;
Paint lineRoundRadiusBgPaint;
Paint outsideRoundRadiusPaint;
Paint colorRoundPaint;
Paint currentLineRoundPaint;
Paint dbTextPaint;
RectF colorRoundRect;
float degrees = 0.0f;
int currentDb = 0;
int width;
int height;
int halfWidth;
int centerY;
private Bitmap triangleBitmap;
private float outsideRoundRadius;
private float colorRoundRadius;
private float lineRoundLength;
private float currentLineRoundLength;
private float currentValueTextSize;
private int lineRoundRadiusColor;
private int lineRoundRadiusBgColor;
private static float DB_VALUE_PER_SCALE = 1.22f; // 規定每一刻度等於1.22分貝
private static float DEGRESS_VALUE_PER_SCALE = 3.0f; // 規定每一刻度的大小為3度
private static float MAX_SCALE = 96; // 最大的刻度數
public DecibelView(Context context) {
this(context, null);
}
public DecibelView(Context context, AttributeSet attributeSet) {
this(context, attributeSet, 0);
}
public DecibelView(Context context, AttributeSet attributeSet, int i) {
super(context, attributeSet, i);
obtainStyledAttributes(context, attributeSet, i);
initView();
}
private void obtainStyledAttributes(Context context, AttributeSet attributeSet, int i) {
if (attributeSet != null) {
TypedArray obtainStyledAttributes = context.obtainStyledAttributes(attributeSet, R.styleable.DecibelView, i, 0);
this.outsideRoundRadius = obtainStyledAttributes.getDimension(R.styleable.DecibelView_outsideRoundRadius, 402.0f);
this.colorRoundRadius = obtainStyledAttributes.getDimension(R.styleable.DecibelView_colorRoundRadius, 325.0f);
this.currentLineRoundLength = obtainStyledAttributes.getDimension(R.styleable.DecibelView_currentLineRoundLength, 339.0f);
this.lineRoundLength = obtainStyledAttributes.getDimension(R.styleable.DecibelView_lineRoundLength, 342.0f);
this.lineRoundStart = obtainStyledAttributes.getDimension(R.styleable.DecibelView_lineRoundStart, 525.0f);
this.currentValueTextSize = obtainStyledAttributes.getDimension(R.styleable.DecibelView_currentValueTextSize, toSp(60.0f));
this.lineRoundRadiusColor = obtainStyledAttributes.getColor(R.styleable.DecibelView_lineRoundRadiusColor, 0x33FFFFFF);
this.lineRoundRadiusBgColor = obtainStyledAttributes.getColor(R.styleable.DecibelView_lineRoundRadiusBgColor, 0xBFFFFFF);
this.startAngle = obtainStyledAttributes.getFloat(R.styleable.DecibelView_startAngle, 125.0f);
this.triangleBitmap = ((BitmapDrawable) getResources().getDrawable(R.mipmap.decibel_max_value)).getBitmap();
obtainStyledAttributes.recycle();
}
}
private void initView() {
this.currentValuePaint = new Paint();
this.currentValuePaint.setTextSize(this.currentValueTextSize);
this.currentValuePaint.setAntiAlias(true);
this.currentValuePaint.setStyle(Style.STROKE);
this.currentValuePaint.setColor(-1);
this.currentValuePaint.setTypeface(Typeface.create("sans-serif-medium", 0));
this.lineRoundRadiusPaint = new Paint();
this.lineRoundRadiusPaint.setAntiAlias(true);
this.lineRoundRadiusPaint.setStyle(Style.STROKE);
this.lineRoundRadiusPaint.setColor(this.lineRoundRadiusColor);
this.lineRoundRadiusPaint.setStrokeWidth(5.0f);
this.lineRoundRadiusBgPaint = new Paint();
this.lineRoundRadiusBgPaint.setAntiAlias(true);
this.lineRoundRadiusBgPaint.setStyle(Style.STROKE);
this.lineRoundRadiusBgPaint.setColor(this.lineRoundRadiusBgColor);
this.lineRoundRadiusBgPaint.setStrokeWidth(5.0f);
this.outsideRoundRadiusPaint = new Paint();
this.outsideRoundRadiusPaint.setAntiAlias(true);
this.outsideRoundRadiusPaint.setStyle(Style.STROKE);
this.outsideRoundRadiusPaint.setColor(this.lineRoundRadiusBgColor);
this.outsideRoundRadiusPaint.setStrokeWidth(toDip(3.0f));
this.currentLineRoundPaint = new Paint();
this.currentLineRoundPaint.setAntiAlias(true);
this.currentLineRoundPaint.setStyle(Style.STROKE);
this.currentLineRoundPaint.setStrokeWidth(10.0f);
this.dbTextPaint = new Paint();
this.dbTextPaint.setAntiAlias(true);
this.dbTextPaint.setStyle(Style.STROKE);
this.dbTextPaint.setColor(-1);
this.dbTextPaint.setTextSize(toSp(20.0f));
this.dbTextPaint.setTypeface(Typeface.create("sans-serif-medium", 0));
this.colorRoundPaint = new Paint();
this.colorRoundPaint.setAntiAlias(true);
this.colorRoundPaint.setStyle(Style.STROKE);
this.colorRoundPaint.setStrokeWidth(toDip(4.0f));
}
protected void onSizeChanged(int i, int i2, int i3, int i4) {
super.onSizeChanged(i, i2, i3, i4);
// 在onSizeChanged中設定和控制元件大小相關的值,這個時候控制元件已經測量完成了
this.width = getWidth();
this.halfWidth = this.width / 2;
this.height = getHeight();
this.centerY = (int) (toDip(180.0f) + this.colorRoundRadius);
initColorRoundRect();
initColorRoundSweepGradient();
}
private void initColorRoundRect() {
this.colorRoundRect = new RectF();
this.colorRoundRect.left = ((float) this.halfWidth) - this.colorRoundRadius;
this.colorRoundRect.top = ((float) this.centerY) - this.colorRoundRadius;
this.colorRoundRect.right = ((float) this.halfWidth) + this.colorRoundRadius;
this.colorRoundRect.bottom = ((float) this.centerY) + this.colorRoundRadius;
}
private void initColorRoundSweepGradient() {
this.colorRoundSweepGradient = new SweepGradient((float) this.halfWidth, (float) this.centerY, this.colorRoundColors, null);
}
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪製最外面的圓環
canvas.drawCircle((float) this.halfWidth, (float) this.centerY, this.outsideRoundRadius, this.outsideRoundRadiusPaint);
canvas.save(); // 儲存畫布狀態
// 繪製刻度的背景線
canvas.rotate((-this.startAngle) + 1.0f, (float) this.halfWidth, (float) this.centerY); // 先將畫布旋轉至起始角度
for (int i = 0; i <= MAX_SCALE; i++) { // 迴圈繪製完所有背景線
canvas.drawLine((float) this.halfWidth, (((float) this.centerY) - this.lineRoundStart) - this.lineRoundLength, (float) this.halfWidth, ((float) this.centerY) - this.lineRoundStart, this.lineRoundRadiusBgPaint);
canvas.rotate(DEGRESS_VALUE_PER_SCALE, (float) this.halfWidth, (float) this.centerY); // 每次繪製完後將畫布旋轉一個刻度對應的角度
}
canvas.restore(); // 還原畫布狀態到上一次呼叫save方法時
canvas.save();
// 繪製分貝值達到部分的刻度線
canvas.rotate((-this.startAngle) + 1.0f, (float) this.halfWidth, (float) this.centerY); // 先將畫布旋轉至起始角度
float tmpDegress = 0;
while (tmpDegress <= ((int) this.degrees)) { // 迴圈繪製分貝值達到部分的刻度線
if (this.degrees - tmpDegress >= DEGRESS_VALUE_PER_SCALE / 2) { // 這裡判斷一下,如果差值超過一半才繪製當前的刻度線
canvas.drawLine((float) this.halfWidth, (((float) this.centerY) - this.lineRoundStart) - this.lineRoundLength, (float) this.halfWidth, ((float) this.centerY) - this.lineRoundStart, this.lineRoundRadiusPaint);
}
canvas.rotate(DEGRESS_VALUE_PER_SCALE, (float) this.halfWidth, (float) this.centerY);
tmpDegress = DEGRESS_VALUE_PER_SCALE + tmpDegress;
}
// 繪製顏色漸變的半圓環
canvas.restore();
canvas.save();
this.colorRoundMatrix.setRotate(270 - startAngle, (float) this.halfWidth, (float) this.centerY);
this.colorRoundSweepGradient.setLocalMatrix(this.colorRoundMatrix);
this.colorRoundPaint.setShader(this.colorRoundSweepGradient);
canvas.drawArc(this.colorRoundRect, 270 - startAngle, 2 * startAngle, false, this.colorRoundPaint);
this.colorRoundPaint.setShader(null);
// 繪製分貝標誌線,這根線的顏色會隨著分貝的變化而變化
canvas.restore();
canvas.save();
canvas.rotate((-this.startAngle) + 1.0f + this.degrees, (float) this.halfWidth, (float) this.centerY);
this.colorRoundMatrix.setRotate(270 * (1 - this.degrees / (2 * startAngle)), (float) this.halfWidth, (float) this.centerY);
this.colorRoundSweepGradient.setLocalMatrix(this.colorRoundMatrix);
this.currentLineRoundPaint.setShader(this.colorRoundSweepGradient);
canvas.drawLine((float) this.halfWidth, (((float) this.centerY) - this.lineRoundStart) - this.currentLineRoundLength, (float) this.halfWidth, ((float) this.centerY) - this.lineRoundStart, this.currentLineRoundPaint);
this.currentLineRoundPaint.setShader(null);
// 繪製半圓環的左邊角
canvas.restore();
canvas.save();
this.colorRoundPaint.setColor(0xFF009AD6);
canvas.rotate((float) (((double) ((-this.startAngle) + 1.0f)) + 0.2d), (float) this.halfWidth, (float) this.centerY);
canvas.drawLine((float) this.halfWidth, toDip(8.0f) + (((float) this.centerY) - toDip(95.0f)), (float) this.halfWidth, ((float) this.centerY) - toDip(95.0f), this.colorRoundPaint);
canvas.restore();
canvas.save();
// 繪製半圓環的右邊角
this.colorRoundPaint.setColor(0xFFCF4036);
canvas.rotate((float) (((double) (((-this.startAngle) + 1.0f) + 288.0f)) - 0.2d), (float) this.halfWidth, (float) this.centerY);
canvas.drawLine((float) this.halfWidth, toDip(8.0f) + (((float) this.centerY) - toDip(95.0f)), (float) this.halfWidth, ((float) this.centerY) - toDip(95.0f), this.colorRoundPaint);
canvas.restore();
canvas.save();
// 繪製標記最大分貝的小三角
canvas.rotate((-this.startAngle) + ((((float) maxDb) / DB_VALUE_PER_SCALE) * DEGRESS_VALUE_PER_SCALE), (float) this.halfWidth, (float) this.centerY);
canvas.drawBitmap(this.triangleBitmap, (float) this.halfWidth, (((float) this.centerY) - this.outsideRoundRadius) - toDip(10.0f), this.currentValuePaint);
canvas.restore();
canvas.save();
// 繪製標記最小分貝的小三角
canvas.rotate((-this.startAngle) + ((((float) minDb) / DB_VALUE_PER_SCALE) * DEGRESS_VALUE_PER_SCALE), (float) this.halfWidth, (float) this.centerY);
canvas.drawBitmap(this.triangleBitmap, (float) this.halfWidth, (((float) this.centerY) - this.outsideRoundRadius) - toDip(10.0f), this.currentValuePaint);
canvas.restore();
// 繪製描述分貝的文字
float descent = - this.currentValuePaint.ascent();
float measureText = this.currentValuePaint.measureText(this.currentDb + "");
canvas.drawText(this.currentDb + "", (((float) this.halfWidth) - (measureText / 2.0f)) - 10.0f, (((float) this.centerY) + (descent / 2.0f)) - 20.0f, this.currentValuePaint);
canvas.drawText("dB", (measureText / 2.0f) + ((float) this.halfWidth), ((descent / 2.0f) + ((float) this.centerY)) - 20.0f, this.dbTextPaint);
}
// 設定最大的分貝值
public void setMaxDb(int maxDb) {
this.maxDb = maxDb;
invalidate();
}
// 設定最小的分貝值
public void setMinDb(int minDb) {
this.minDb = minDb;
invalidate();
}
// 設定當前分貝值
public void setDb(float db) {
this.currentDb = (int) db;
this.degrees = ((db / DB_VALUE_PER_SCALE) * DEGRESS_VALUE_PER_SCALE); // 規定1刻度等於1.22分貝
invalidate();
}
private float toDip(float value) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, getResources().getDisplayMetrics());
}
private float toSp(float value) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, value, getResources().getDisplayMetrics());
}
}
在attr.xml檔案中,宣告自定義屬性
<declare-styleable name="DecibelView">
<attr name="colorRoundRadius" format="dimension">
</attr>
<attr name="currentLineRoundLength" format="dimension">
</attr>
<attr name="currentValueTextSize" format="dimension">
</attr>
<attr name="lineRoundLength" format="dimension">
</attr>
<attr name="lineRoundStart" format="dimension">
</attr>
<attr name="outsideRoundRadius" format="dimension">
</attr>
<attr name="startAngle" format="float">
</attr>
<attr name="lineRoundRadiusBgColor" format="color">
</attr>
<attr name="lineRoundRadiusColor" format="color">
</attr>
</declare-styleable>
在這個自定義View中,最重要的部分就是onDraw函式,該函式繪製了控制元件的內容,其中利用到了很多繪製方面的知識,這裡總結一下:
1、canvas.save()和cavas.restore()
這兩個方法一般是成對出現的,用來儲存畫布的狀態和恢復畫布的狀態,在我們需要對畫布進行旋轉、位移等對畫布狀態進行改變的操作前,首先,我們應該呼叫canvas.save()方法將畫布的狀態儲存起來,然後在繪製完成後,如果需要將畫布狀態恢復到改變之前的狀態,就需要呼叫canvas.restore()方法來對畫布狀態進行恢復,canvas.restore()方法會將畫布狀態恢復至上一次呼叫canvas.save()方法的狀態。
2、SweepGradient
我們可以使用SweepGradient來修飾畫筆來繪製顏色漸變的效果,SweepGradient繼承於Shader,表示著色器的意思,Shader具體說明可以參考:https://blog.csdn.net/aigestudio/article/details/41799811
3、Paint.ascent()
關於字型測量可以參考:https://blog.csdn.net/aigestudio/article/details/41447349
六、總結
自定義View是一門很深的學問,設計到的知識非常多,要想完全掌握自定義View,除了不斷鑽研系統原始碼外,還需要多加實踐,在原始碼方面,我們需要掌握Android View的繪製流程(View的layout、mearsure、draw)、Android的螢幕重新整理機制(每16ms觸發一次繪製螢幕的訊號)、Android事件機制以及大量和繪製相關的類(Paint、Canvas、Matrix等等),只有掌握了這些,我們才能說真正掌握了自定義View,才能做出即流暢又炫酷的自定義View,當然,我們也不可能一下就能掌握這麼多知識,需要日積月累,自定義View方面的知識推薦部落格:AigeStudio
相關文章
- Android自定義View:View(二)AndroidView
- Android 自定義viewAndroidView
- Android: 自定義ViewAndroidView
- Android自定義View整合AndroidView
- Android自定義view-自繪ViewAndroidView
- 從 0 到 1Android 自定義 View(四)貝塞爾曲線AndroidView
- 安卓自定義 View 進階:貝塞爾曲線安卓View
- android自定義view(自定義數字鍵盤)AndroidView
- android自定義View&自定義ViewGroup(下)AndroidView
- android自定義View&自定義ViewGroup(上)AndroidView
- 重拾Android自定義ViewAndroidView
- Android自定義view詳解AndroidView
- Android 自定義 view 詳解AndroidView
- 自定義View合輯(6)-波浪(貝塞爾曲線)View
- Android自定義View播放Gif動畫AndroidView動畫
- Android 自定義View基礎(一)AndroidView
- Android自定義View:ViewGroup(三)AndroidView
- android自定義View——座標系AndroidView
- Android自定義View之捲尺AndroidView
- Android 自定義View之下雨動畫AndroidView動畫
- Android自定義View注意事項AndroidView
- Android自定義View 水波氣泡AndroidView
- Android 自定義View 字型變色AndroidView
- Android 自定義View 點贊效果AndroidView
- Android自定義View-卷軸AndroidView
- Android自定義View 屬性新增AndroidView
- [原] Android 自定義View步驟AndroidView
- Android 自定義View:深入理解自定義屬性(七)AndroidView
- 自定義VIEWView
- Android自定義view系列:手擼一個帶點兒科技感的儀表盤!AndroidView
- Android 自定義貝塞爾曲線工具Android
- Android自定義View:MeasureSpec的真正意義與View大小控制AndroidView
- 自定義儀表盤
- Android 自定義 View 最少必要知識AndroidView
- Android 自定義 View 實戰之 PuzzleViewAndroidView
- Android 自定義View:屬性動畫(六)AndroidView動畫
- Android 自定義 View 之入門篇AndroidView
- Android 自定義 View 之 LeavesLoadingAndroidView