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

最后附上源码地址:https://github.com/2449983723/AndroidComponents

猜你喜欢

转载自blog.csdn.net/xiao_nian/article/details/83141422