1. 程式人生 > >[Android]自定義繪製一個簡易的音訊條形圖,附上對MP3音訊波形資料的採集與展現

[Android]自定義繪製一個簡易的音訊條形圖,附上對MP3音訊波形資料的採集與展現

  在專案中需要到資料統計的地方,往往都需要到一些圖的展示,比如曲線圖、折線圖、餅狀圖、圓形圖、條形圖等等。在本文中我們來實現一個簡易的條形圖的繪製。

  首先,我們建立一個BarGraphView類,讓這個類繼承自View,一般重寫View都必須重寫View的一參構造方法和二參構造方法,如下:

public class BarGraphView extends View {
public BarGraphView(Context context) {
        super(context);
    }

    public BarGraphView(Context context, @Nullable AttributeSet attrs) {
        super
(context, attrs); } }

  其次,繪製的過程在於onDraw方法的回撥,我們重寫這個方法,來繪製條形圖:

 @Override
    protected void onDraw(Canvas canvas) {
        Log.i("bar","onDraw");
        super.onDraw(canvas);
        Random random = new Random();
        rectWidth = canvas.getWidth()/barCount;
        for (int i = 0;i<barCount;i++){
            //生成0-99的隨機數,作為高度的百分比,得出一個隨機高度
int currentHeight = (random.nextInt(10))*canvas.getHeight()/100; Log.i("bar","currentHeight:"+currentHeight); canvas.drawRect(rectWidth*i+offset,currentHeight,rectWidth*(i+1),getHeight(),mPaint); } postInvalidateDelayed(speed); }

  上文程式碼中,我們先通過canvas.getWidth()/barCount;計算出每個條形的寬度,而高度則是隨機取出總高度的百分比。
最後呼叫canvas.drawRect(rectWidth*i+offset,currentHeight,rectWidth*(i+1),getHeight(),mPaint);

  Canvas代表一個畫板,他可以畫出很圖案,比如條形圖其實就是一個個矩形組成的,那麼我們利用drawRect可以畫出一個矩形來作為一個條形。

  在drawRect方法中五個引數分別代表著:left、top,right、bottom,畫筆。

  其實也就是左上頂點座標(left,top)和一個右下頂點的座標來確定一個矩形(right,bottom)
我們用rectWidth*i+offset,來定義矩形的left,用隨機數來定義矩形的top,用計算出的寬度來定義矩形的right,用總體高度來定義矩形的bottm。

  而第五個引數Paint,代表著一個畫筆,有畫板了,也知道要畫什麼,但也得有個筆來畫才能展現出來是吧,所以我們增加一個init方法來定義一個Paint變數,讓構造方法呼叫這個init方法:

    private void init(){
        mPaint = new Paint();
        mPaint.setColor(getResources().getColor(R.color.colorAccent));
        mPaint.setStyle(Paint.Style.FILL);
    }

  這裡為了簡單,我們只是把畫筆定義為預設的colorAccent顏色。

  在onDraw方法的最後呼叫postInvalidateDelayed(speed);來重新整理,模擬一個條形圖動態變化的效果,呼叫postInvalidateDelayed時View會再回調onDraw方法。

  然後,我們提供幾個設定的方法:

    public void setBarCount(int barCount) {
        this.barCount = barCount;
    }


    public void setOffset(int offset) {
        this.offset = offset;

    }

    public void setSpeed(int speed) {
        this.speed = speed;
    }

  好了,一個簡易的條形圖就已經初具規模了。通常我們為了更為直觀的看到一個效果,會把條形圖的條形設定成一個顏色漸變的效果,怎麼做呢?

  可以重寫View的onSizeChanged方法,在該方法中,我們利用LinearGradient這個顏色漸變的類來裝入畫筆:

@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        LinearGradient linearGradient = new LinearGradient(0,0,rectWidth,getHeight(),Color.RED,Color.GREEN, Shader.TileMode.CLAMP);
        mPaint.setShader(linearGradient);

    }

  程式碼很簡單,我們只是建立一個LinearGradient物件,物件的前面四個引數代表著兩個座標,也就是從(x1,y1)到(x2,y2)的漸變過程,我們讓起點座標都定位0,然後讓終點座標設定為條形圖的最高點。第五個引數表示起始顏色,第六個顏色表示終止顏色,我們分別用紅和綠,來表示一個綠到紅的漸變過程。最後一個引數Shader.TileMode.CLAMP表示如果著色器超出原始邊界範圍,會複製邊緣顏色。最後我們把創建出來的LinearGradient物件裝入畫筆,就實現一個條形圖的漸變效果了。

  到這裡,我們就已經完成了一個簡易條形圖的製作了,另外一個不得不說的點是,由於我們是繼承自View,預設View的warp_content模式是填充父佈局,也就是跟match_content一樣的效果了,那麼我們可以來設定一個值,使得如果定義屬性為warp_content的話,則有一個預設的值。

  我們重寫onMeasure方法,而其實onMeasure方法中,預設就是呼叫setMeasuredDimension方法,所以我們可以直接把自定義好的寬高值傳遞給setMeasuredDimension。

  我們定義一個寬度的測量:

  private int measureWidth(int widthMeasureSpec){
        int width;
        int spacMode = MeasureSpec.getMode(widthMeasureSpec);
        int size = MeasureSpec.getSize(widthMeasureSpec);
        if(spacMode==MeasureSpec.EXACTLY){
            width = size;
        }else {
            width = 300;
            if(spacMode==MeasureSpec.AT_MOST){
                width = Math.min(width,size);
            }
        }
        Log.i("bar","width:"+width);
        return width;
    }

  在Android中,採用了一個int數值來代表一個測量值,用高二位來代表測量的模式,其餘位數代表測量的數值。利用MeasureSpec的getMode和getSize方法我們很容易得到這兩個數值。

  當我們定義寬或高是match_content或者給定了一個確切的數值的話,則模式就是MeasureSpec.EXACTLY,否則就是MeasureSpec.AT_MOST。其實還有個MeasureSpec.UNSPECIFIED,表示空間不受限制,一般View裡面不用到這個屬性。如果我們設定為warp_content的話則模式就是MeasureSpec.AT_MOST,我們進行判斷,取出一個最小值作為預設包裹的大小。
  另外再定義一個高度的測量,寫法幾乎一致:

private int measureHeight(int heightMeasureSpec){
        int height;
        int spacMode = MeasureSpec.getMode(heightMeasureSpec);
        int size = MeasureSpec.getSize(heightMeasureSpec);
        if(spacMode==MeasureSpec.EXACTLY){
            height = size;
        }else {
            height = 300;
            if(spacMode==MeasureSpec.AT_MOST){
                height = Math.min(height,size);
            }
        }
        Log.i("bar","height:"+height);
        return height;
    }

  最後我們在onMeasure方法中,這樣子寫:

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
    }

好了,我們來測試一下,建立一個Activity:

public class BarGraphActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bar_graph);
        BarGraphView view = (BarGraphView) findViewById(R.id.bargraph);
        view.setOffset(10);
        view.setSpeed(300);//設定間隔重新整理速度
        view.setBarCount(20);//設定條形圖的數量
        }
  }

  在佈局裡面定義:

<?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:layout_margin="15dp"
    tools:context="com.example.qyz.BarGraphActivity">
<com.example.qyz.BarGraphView
    android:id="@+id/bargraph"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />


</LinearLayout>

  然後執行,截圖如下:

條形圖

  這樣子我們的一個簡易條形圖就製作完畢了。
  最後貼出BarGraphView的程式碼:

public class BarGraphView extends View {
    private  int barCount =30;//條形的數量
    private int rectWidth = 15;//條形的寬度
    private int offset = 10;
    private int speed = 300;
    Paint mPaint;
    public BarGraphView(Context context) {
        super(context);
        init();
    }
    public BarGraphView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    private void init(){
        mPaint = new Paint();
        mPaint.setColor(getResources().getColor(R.color.colorAccent));
        mPaint.setStyle(Paint.Style.FILL);
    }
    public void setBarCount(int barCount) {
        this.barCount = barCount;
    }
    public void setOffset(int offset) {
        this.offset = offset;

    }
    public void setSpeed(int speed) {
        this.speed = speed;
    }
    @Override
    protected void onDraw(Canvas canvas) {
        Log.i("bar","onDraw");
        super.onDraw(canvas);
        Random random = new Random();
        rectWidth = canvas.getWidth()/barCount;
        for (int i = 0;i<barCount;i++){
            //生成0-99的隨機數,作為高度的百分比,得出一個隨機高度
            int currentHeight = (random.nextInt(100))*canvas.getHeight()/100;
            Log.i("bar","currentHeight:"+currentHeight);
            canvas.drawRect(rectWidth*i+offset,currentHeight,rectWidth*(i+1),getHeight(),mPaint);
        }
      postInvalidateDelayed(speed);

    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
    }
    private int measureWidth(int widthMeasureSpec){
        int width;
        int spacMode = MeasureSpec.getMode(widthMeasureSpec);
        int size = MeasureSpec.getSize(widthMeasureSpec);
        if(spacMode==MeasureSpec.EXACTLY){
            width = size;
        }else {
            width = 300;
            if(spacMode==MeasureSpec.AT_MOST){
                width = Math.min(width,size);
            }
        }
        Log.i("bar","width:"+width);
        return width;
    }
    private int measureHeight(int heightMeasureSpec){
        int height;
        int spacMode = MeasureSpec.getMode(heightMeasureSpec);
        int size = MeasureSpec.getSize(heightMeasureSpec);
        if(spacMode==MeasureSpec.EXACTLY){
            height = size;
        }else {
            height = 300;
            if(spacMode==MeasureSpec.AT_MOST){
                height = Math.min(height,size);
            }
        }
        Log.i("bar","height:"+height);
        return height;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        LinearGradient linearGradient = new LinearGradient(0,0,rectWidth,getHeight(),Color.RED,Color.GREEN, Shader.TileMode.CLAMP);
        mPaint.setShader(linearGradient);
    }
}

  為了一個真實效果,我們來接入實際MP3的波形音訊,讓條形圖顯示MP3的波形數值,

  怎麼得到mp3的波形數值呢?可以利用Visualizer類來進行採集,這部分程式碼我直接貼出來,在相關程式碼處已經做了註釋:

public class BarGraphActivity extends AppCompatActivity {
    // 定義播放聲音的MediaPlayer
    private MediaPlayer mPlayer;
    Visualizer mVisualizer;
    WaveView waveView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bar_graph);
         waveView = (WaveView) findViewById(R.id.bargraph);
        waveView.setOffset(10);
        mPlayer = MediaPlayer.create(this, R.raw.demo);
        setupVisualizer();
        // 開發播放音樂
        mPlayer.start();
    }
    /**
     * 初始化頻譜
     */
    private void setupVisualizer()
    {
        // 以MediaPlayer的AudioSessionId建立Visualizer
        // 顯示該MediaPlayer播放的MP3音訊資料
        mVisualizer = new Visualizer(mPlayer.getAudioSessionId());
        //設定資料取樣值,一般為2的指數倍,如64,128,256,512,1024。
        mVisualizer.setCaptureSize(Visualizer.getCaptureSizeRange()[0]);
        /**
         * listener,監聽取樣過程
         *rate, 取樣的週期,即隔多久取樣一次
         *iswave,波形訊號
         *isfft,是FFT訊號
         */
        mVisualizer.setDataCaptureListener(
                new Visualizer.OnDataCaptureListener()
                {
                    //採集快速傅立葉變換有關的資料
                    @Override
                    public void onFftDataCapture(Visualizer visualizer,
                                                 byte[] fft, int samplingRate){
                    }
                    //採集波形資料
                    @Override
                    public void onWaveFormDataCapture(Visualizer visualizer,
                                                      byte[] waveform, int samplingRate)
                    {
                        waveView.update(waveform);
                        Log.i("bar","onWaveFormDataCapture length:"+waveform.length);
                    }
                }, Visualizer.getMaxCaptureRate() / 5, true, false);
        //必須設定為true後,採集工作才會開始
        mVisualizer.setEnabled(true);
    }
}

  因為我們在上文的數值都是寫死成隨機的,我們複製BarGraphView重新命名為:WaveView,把onDraw方法的程式碼改為如下:

    @Override
    protected void onDraw(Canvas canvas) {
        Log.i("bar","onDraw");
        super.onDraw(canvas);
        if(data==null)return;
        rectWidth = canvas.getWidth()*3/data.length;
        for (int i = 0,j=0;j<data.length;i++,j+=3){
            int currentHeight = (int) (getHeight()*((data[i]+128)/256.0));
            canvas.drawRect(rectWidth*i+offset,currentHeight,rectWidth*(i+1),getHeight(),mPaint);
        }
    }

  由於資料比較多,我們把採集到的byte陣列資料,128個採集值每3個顯示一個出來。

  最後,還要申請一下許可權:

<uses-permission android:name="android.permission.RECORD_AUDIO"></uses-permission>
 <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"></uses-permission>

音訊