[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>