Android自定義View之分貝儀

xiao_nian發表於2018-10-18

一、說明

       最近在整理自定義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

相關文章