最近在整理自定义View方面的知识,偶尔看到meizu工具箱的分贝仪效果,感觉它的实效效果还挺好的,遂想自己模拟实现练练手,废话不多说,直接开撸。
首先看一下效果图:
看效果还挺炫酷的,随着分贝的变化,界面会显示出对应的分贝,并且上面的指针也会转动,同时,指针的颜色也会变化。两个小三角分贝指向当前最小分贝和最大分贝的位置。
要实现上面分贝仪的效果,首先我们需要实时采集外界的声音,然后根据声音大小的变化来改变自定义View的显示,所以需要两方面的知识,采集声音和自定义View,在Android中,采集声音使用的是MediaRecorder这个类,而分贝数据的显示则需要用到自定义View方面的知识。
1、权限申请
首先使用录音是需要申请权限的,在Android6.0以前,权限申请很简单,只需要在Manifest文件中声明即可,但是在后面的Android版本中,Android对权限的控制比较严格,为了方便权限的申请,这里使用了权限申请框架EasyPermissions,参考:Android EasyPermissions官方库,高效处理权限
首先,需要在Manifest文件中声明要使用的权限:
然后需要在代码中动态检查权限:
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 perms) {
Log.i("liunianprint:", "onPermissionsGranted");
startRecord(); // 在onStart方法中开始录音
}
// 申请权限失败
@Override
public void onPermissionsDenied(int requestCode, List 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 perms) {
Log.i("liunianprint:", "onPermissionsGranted");
startRecord(); // 在onStart方法中开始录音
}
// 申请权限失败
@Override
public void onPermissionsDenied(int requestCode, List perms) {
Log.i("liunianprint:", "onPermissionsDenied");
if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
showAppSettingsDialog();
}
}
private void showAppSettingsDialog() {
new AppSettingsDialog.Builder(this).setTitle("该功能需要麦克风权限").setRationale("该功能需要麦克风权限,请在设置里面开启!").build().show();
}
}
五、自定义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文件中,声明自定义属性
在这个自定义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
最后附上源码地址:https://github.com/2449983723/AndroidComponents