Android 音樂播放器的實現(一)自定義按鈕的實現

Android 系統提供瞭MediaPlayer控件,讓我們能夠利用它實現音頻的播放。

而從學Android開始,在看教程的時候,我就想,我要自己做一個音樂播放器,因為一個完整的音樂播放器是有很多功能的,它涉及到很多方面的知識,可以幫助我們更好地學習和掌握關於Android的點點滴滴的知識。

既然我們現在是來學習怎麼用代碼打出我們自己的音樂播放器,我們就別著急,心急吃不瞭熱豆腐,一口吃不成大胖子。

一步一步地,來實現我們的音樂播放器吧。

那麼思路是怎麼樣的呢?我當時是這樣想的,先做一部分功能,能夠看到音樂,控制音樂就可以瞭,所以目前的功能實現如下:

1)要拿出本地的音樂文件,然後將它展現在一個列表上。

1.1)利用ContentResolver 獲取本地數據,關於怎麼獲取本地的音樂文件或者圖片文件,請看:
Android中利用ContentResolver獲取本地音樂和相片

1.2)利用ListView 展現數據,每個Listitem會顯示歌曲名,歌手,播放時間,還有如果有唱片的圖片,還要把唱片圖片展示出來。

2)要有一排按鈕,能夠實現播放,前一首,後一首,退出,模式選擇(順序播放,循環播放,單曲循環,隨機播放等),放在最下面

3)要有一條進度條,隨著音樂的播放,一步一步地向前刷刷刷,

4)既然有進度條,那也要有兩個展示時間的控件,一個展示音樂有多長,一個展示播到哪瞭。這個跟進度條都要放在按鈕的上面。

5)一個展示當前播放歌曲的TextView,放在最上面。

所以一開始就有瞭下面的界面:

因為我不會美工啊,所以一開始我就用按鈕來做播放,停止等控制功能,我們是在學習嘛,美化的東西慢慢來。(其實看上去也不算很醜,對吧?)

但是後來一想,既然是學習啊,又不會美工,那麼我就來實現一排自定義的Button吧,於是就有瞭下面的界面。

看到下面一排醜醜的按鈕沒瞭,哈哈,我畫的!

既然說到瞭這個,我們這篇文件就先說說自定義按鈕是怎麼實現的吧。

我在之前寫過一篇關於自定義View的文章:Android學習小demo(1)自定義View,vc3Ryb25nPsbkyrXUrcDtysfSu9H5tcSjrM7Sw8e+zdaxvdPAtL+0tcS0+sLrsMmjujwvcD4KPHA+MaOpz8jU2nJlcy92YWx1ZXMv1tC0tL2o0ru49mF0dHJzLnhtbM7EvP6jrNTawO/D5tfUtqjS5cr00NSjujwvcD4KPHA+PHByZSBjbGFzcz0=”brush:java;”>

我們定義瞭兩個屬性,其中type是一個枚舉類型,分別有start, forward, backward, exit, mode等類型。

雖然我們有五種類型的按鈕要展現,但是我們隻要實現一個自定義的類,然後根據傳入的不同 type 的值來畫出不同的圖形就好。

下面我們看看自定義按鈕的代碼:

public class CustomAudioIcon extends View implements OnTouchListener {

//	private static final String TAG = "com.example.nature.CustoAudioIcon";

	private static final int defaultType = -1;
	private static final int start = 0;
	private static final int forward = 2;
	private static final int backward = 3;
	private static final int exit = 4;
	private static final int mode = 5;

	private int type;
	private int color;

	private Paint upPaint;
	private Paint pressPaint;
	private Paint boxPaint;
	private Paint paint;
	
	private int width,height;	

	private boolean pressed = false;
	
	//Only for StartStopButton
	private boolean flagStart = true;
	
	//Only for ModeButton
	public static final int MODE_ONE_LOOP = 0;
	public static final int MODE_ALL_LOOP = 1;
	public static final int MODE_RANDOM = 2;
	public static final int MODE_SEQUENCE = 3; 
	private int currentMode = 3;

	public CustomAudioIcon(Context context, AttributeSet attrs) {
		super(context, attrs);
		TypedArray typedArray = context.obtainStyledAttributes(attrs,
				R.styleable.CustomAudioIcon);
		type = typedArray.getInt(R.styleable.CustomAudioIcon_type, defaultType);
		color = typedArray.getColor(R.styleable.CustomAudioIcon_color,
				Color.BLACK);
		typedArray.recycle();

		init();
		setClickable(true);//In order to make this view can accept the OnClickListener
		setOnTouchListener(this);
	}

	private void init() {
		boxPaint = new Paint();
		boxPaint.setColor(color);
		boxPaint.setAntiAlias(true);
		boxPaint.setStrokeWidth(1);

		upPaint = new Paint();
		upPaint.setColor(Color.BLACK);
		upPaint.setAntiAlias(true);
		upPaint.setStrokeWidth(1);

		pressPaint = new Paint();
		pressPaint.setColor(Color.GREEN);
		pressPaint.setAntiAlias(true);
		pressPaint.setStrokeWidth(1);
	}

	public void onDraw(Canvas canvas) {		
		paint = pressed ? pressPaint : upPaint;
		width = getMeasuredWidth();
		height = getMeasuredHeight();	
		
		if(pressed){
			canvas.drawColor(Color.parseColor("#447744"));
		}
		switch (type) {
		case start:
			if(flagStart){
				drawStart(canvas, pressed);
			}else{
				drawStop(canvas, pressed);
			}
			break;	
		case forward:
			drawForward(canvas, pressed);
			break;
		case backward:
			drawBackward(canvas, pressed);
			break;
		case exit:
			drawExit(canvas, pressed);
			break;
		case mode:
			drawMode(canvas, pressed);
			break;
		}

		boxPaint.setStyle(Style.STROKE);
		Rect rect = canvas.getClipBounds();
		rect.bottom--;
		rect.right--;
		canvas.drawRect(rect, boxPaint);
	}

	// public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
	// setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
	// MeasureSpec.getSize(heightMeasureSpec));
	// }

	private void drawStart(Canvas canvas, boolean pressed) {	
		float scaleWidth = width < height ? width : height;
		// calculate the vertexes.
		float[] verticles = { (float) (0.21 * scaleWidth),
				(float) (0.1 * scaleWidth), (float) (0.21 * scaleWidth),
				(float) (0.9 * scaleWidth), (float) (0.9 * scaleWidth),
				(float) (0.5 * scaleWidth) };
		canvas.drawLine(verticles[0], verticles[1], verticles[2], verticles[3],paint);
		canvas.drawLine(verticles[0], verticles[1], verticles[4], verticles[5],paint);
		canvas.drawLine(verticles[2], verticles[3], verticles[4], verticles[5],paint);
	}
	
	private void drawStop(Canvas canvas, boolean pressed) {			
		float scaleWidth = width < height ? width : height;
		// calculate the vertexes.
		float[] verticles = { (float) (0.4 * scaleWidth), (float) (0.1 * scaleWidth), 
				(float) (0.4 * scaleWidth), (float) (0.9 * scaleWidth), 
				(float) (0.6 * scaleWidth), (float) (0.1 * scaleWidth),
				(float) (0.6 * scaleWidth), (float) (0.9 * scaleWidth)};
		canvas.drawLine(verticles[0], verticles[1], verticles[2], verticles[3],paint);
		canvas.drawLine(verticles[4], verticles[5], verticles[6], verticles[7],paint);		
	}

	private void drawForward(Canvas canvas, boolean pressed) {		
		// get the shorter width or height
		int minWH = width < height ? width : height;
		float scaleWidth = (float) (minWH * 0.8);
		// calculte the vertexes.
		float[] verticles = { (float) (0.21 * scaleWidth),
				(float) (0.1 * scaleWidth), (float) (0.21 * scaleWidth),
				(float) (0.9 * scaleWidth), (float) (0.9 * scaleWidth),
				(float) (0.5 * scaleWidth), (float) (0.9 * scaleWidth),
				(float) (0.1 * scaleWidth), (float) (0.9 * scaleWidth),
				(float) (0.9 * scaleWidth) };
		canvas.save();
		canvas.translate((float) (0.1 * minWH), (float) (0.1 * minWH));
		// draw the triangle
		canvas.drawLine(verticles[0], verticles[1], verticles[2], verticles[3],paint);
		canvas.drawLine(verticles[0], verticles[1], verticles[4], verticles[5],paint);
		canvas.drawLine(verticles[2], verticles[3], verticles[4], verticles[5],paint);
		// draw the vertical line
		canvas.drawLine(verticles[6], verticles[7], verticles[8], verticles[9],paint);
		canvas.restore();
	}

	private void drawBackward(Canvas canvas, boolean pressed) {
		// get the shorter width or height
		int minWH = width < height ? width : height;
		float scaleWidth = (float) (minWH * 0.8);
		// calculte the vertexes.
		float[] verticles = { (float) (0.79 * scaleWidth),
				(float) (0.1 * scaleWidth), (float) (0.79 * scaleWidth),
				(float) (0.9 * scaleWidth), (float) (0.1 * scaleWidth),
				(float) (0.5 * scaleWidth), (float) (0.1 * scaleWidth),
				(float) (0.1 * scaleWidth), (float) (0.1 * scaleWidth),
				(float) (0.9 * scaleWidth) };

		canvas.save();
		canvas.translate((float) (0.1 * minWH), (float) (0.1 * minWH));
		// draw the triangle
		canvas.drawLine(verticles[0], verticles[1], verticles[2], verticles[3],paint);
		canvas.drawLine(verticles[0], verticles[1], verticles[4], verticles[5],paint);
		canvas.drawLine(verticles[2], verticles[3], verticles[4], verticles[5],paint);
		// draw the vertical line
		canvas.drawLine(verticles[6], verticles[7], verticles[8], verticles[9],paint);
		canvas.restore();
	}
	
	private void drawExit(Canvas canvas, boolean pressed) {		
		paint.setStyle(Style.STROKE);		
		// get the shorter width or height
		int minWH = width < height ? width : height;
		float scaleWidth = (float) (minWH * 0.8);
		canvas.save();
		canvas.translate((float) (0.1 * minWH), (float) (0.1 * minWH));
		canvas.drawCircle((float)(0.5 * scaleWidth), (float)(0.5 * scaleWidth), (float)(0.4 * scaleWidth), paint);
		canvas.restore();
	}
	
	private void drawMode(Canvas canvas, boolean pressed) {				
		paint.setStyle(Style.STROKE);		
		// get the shorter width or height
		int minWH = width < height ? width : height;
		float scaleWidth = (float) (minWH * 0.8);
		canvas.save();		
		canvas.translate((float) (0.1 * minWH), (float) (0.1 * minWH));
		switch(currentMode){		
		case MODE_ONE_LOOP:
			canvas.drawCircle((float)(0.5 * scaleWidth), (float)(0.5 * scaleWidth), (float)(0.1 * scaleWidth), paint);
			break;
		case MODE_ALL_LOOP:
			canvas.drawCircle((float)(0.4 * scaleWidth), (float)(0.5 * scaleWidth), (float)(0.1 * scaleWidth), paint);
			canvas.drawCircle((float)(0.6 * scaleWidth), (float)(0.5 * scaleWidth), (float)(0.1 * scaleWidth), paint);
			break;
		case MODE_RANDOM:
			canvas.drawCircle((float)(0.3 * scaleWidth), (float)(0.5 * scaleWidth), (float)(0.1 * scaleWidth), paint);
			canvas.drawCircle((float)(0.5 * scaleWidth), (float)(0.5 * scaleWidth), (float)(0.1 * scaleWidth), paint);
			canvas.drawCircle((float)(0.7 * scaleWidth), (float)(0.5 * scaleWidth), (float)(0.1 * scaleWidth), paint);
			break;
		case MODE_SEQUENCE:
			canvas.drawCircle((float)(0.2 * scaleWidth), (float)(0.5 * scaleWidth), (float)(0.1 * scaleWidth), paint);
			canvas.drawCircle((float)(0.4 * scaleWidth), (float)(0.5 * scaleWidth), (float)(0.1 * scaleWidth), paint);
			canvas.drawCircle((float)(0.6 * scaleWidth), (float)(0.5 * scaleWidth), (float)(0.1 * scaleWidth), paint);
			canvas.drawCircle((float)(0.8 * scaleWidth), (float)(0.5 * scaleWidth), (float)(0.1 * scaleWidth), paint);
			break;			
		}
		canvas.restore();
	}
	
	

	@Override
	public boolean onTouch(View v, MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			pressed = true;
			invalidate();
			break;
		case MotionEvent.ACTION_UP:
			pressed = false;			
			invalidate();
			if(type == start){
				flagStart = !flagStart;
			}
			if(type == mode){
				currentMode = (currentMode + 1) % 4;
			}
			break;
		}
		return false;
	}

	/**
	 * If showing the start triangle, returns true, otherwise returns false
	 * @return
	 */
	public boolean isStartStatus() {
		return flagStart;
	}

	/**
	 * Change the flag outside
	 * @param flagStart
	 */
	public void setFlagStart(boolean flagStart) {
		this.flagStart = flagStart;
		invalidate();
	}
}

在類中,我們首先還是通過typedArray來獲取到我們的type值,然後根據type值來畫不同的內容。

因為這幾個控件我都是在佈局文件中定義好長寬的,所以不需要在這裡面重寫onMeasure函數,我們隻要關心如何在 Ondraw() 裡面畫圖形就好瞭。

可以看到在 onDraw() 方法裡面,根據不同的type,我們是會畫不同的按鈕,比如 start 按鈕,它有兩個狀態,當我們點擊start的時候,它是會變成stop(或者pause,在這裡我沒有實現pause,下一次實現)的狀態。

case start:
	if(flagStart){
		drawStart(canvas, pressed);
	}else{
		drawStop(canvas, pressed);
	}
break;	

在drawStart 裡面,我們是畫瞭一個向右的等邊三角形,

private void drawStart(Canvas canvas, boolean pressed) {	
		float scaleWidth = width < height ? width : height;
		// calculate the vertexes.
		float[] verticles = { (float) (0.21 * scaleWidth),
				(float) (0.1 * scaleWidth), (float) (0.21 * scaleWidth),
				(float) (0.9 * scaleWidth), (float) (0.9 * scaleWidth),
				(float) (0.5 * scaleWidth) };
		canvas.drawLine(verticles[0], verticles[1], verticles[2], verticles[3],paint);
		canvas.drawLine(verticles[0], verticles[1], verticles[4], verticles[5],paint);
		canvas.drawLine(verticles[2], verticles[3], verticles[4], verticles[5],paint);
	}

而當我們點擊start的時候,它就會變成stop瞭,就要畫stop瞭,就是一個豎起來的等號(||)瞭,

private void drawStop(Canvas canvas, boolean pressed) {			
		float scaleWidth = width < height ? width : height;
		// calculate the vertexes.
		float[] verticles = { (float) (0.4 * scaleWidth), (float) (0.1 * scaleWidth), 
				(float) (0.4 * scaleWidth), (float) (0.9 * scaleWidth), 
				(float) (0.6 * scaleWidth), (float) (0.1 * scaleWidth),
				(float) (0.6 * scaleWidth), (float) (0.9 * scaleWidth)};
		canvas.drawLine(verticles[0], verticles[1], verticles[2], verticles[3],paint);
		canvas.drawLine(verticles[4], verticles[5], verticles[6], verticles[7],paint);		
	}

p.s. ^_^,有意思吧,哈哈哈哈。

好瞭,那麼是如何實現按鈕點擊的效果的呢,那就是要實現OnTouchListener瞭,

@Override
	public boolean onTouch(View v, MotionEvent event) {
		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			pressed = true;
			invalidate();
			break;
		case MotionEvent.ACTION_UP:
			pressed = false;			
			invalidate();
			if(type == start){
				flagStart = !flagStart;
			}
			if(type == mode){
				currentMode = (currentMode + 1) % 4;
			}
			break;
		}
		return false;
	}

其實任何一個控件的點擊,都是由這兩個動作組成的,Down下去,Up上來,在這裡,獲取到touch事件,然後根據不同的狀態,設置pressed 的值,在onDraw函數中,會根據pressed的值去獲取不同的Paint,

	public void onDraw(Canvas canvas) {		
		paint = pressed ? pressPaint : upPaint;
		width = getMeasuredWidth();
		height = getMeasuredHeight();	
		
		if(pressed){
			canvas.drawColor(Color.parseColor("#447744"));
		}

然後再調用 Invaldiate() 函數重新刷新頁面,就達到點擊的效果瞭。
一般情況,我們如果調用OnTouch函數,我們都是在OnTouch函數中返回一個 true,表明touch事件已經被我們消費掉瞭,不用再繼續走下去瞭。

但是在這裡,我們不能這麼做,因為我們在Activity中要給這些自定義的View設置OnClickListener呢,才能來控制我們音樂的播放暫停啊,所以這裡必須返回false。

但是如果返回 false, Down事件被觸發之後,就不會再繼續觸發Up事件瞭,這是因為默認的View是不能點擊的,才會發生這樣的事情,所以我們隻需要在初始化的時候,將這個View 設置成可點擊的就好瞭,如下:

	public CustomAudioIcon(Context context, AttributeSet attrs) {
		super(context, attrs);
		TypedArray typedArray = context.obtainStyledAttributes(attrs,
				R.styleable.CustomAudioIcon);
		type = typedArray.getInt(R.styleable.CustomAudioIcon_type, defaultType);
		color = typedArray.getColor(R.styleable.CustomAudioIcon_color,
				Color.BLACK);
		typedArray.recycle();

		init();
		setClickable(true);//In order to make this view can accept the OnClickListener
		setOnTouchListener(this);
	}

關於這個Touch事件和Click事件,推薦大傢去看一下郭大俠的這兩篇文章:

Android事件分發機制完全解析,帶你從源碼的角度徹底理解(上)

Android事件分發機制完全解析,帶你從源碼的角度徹底理解(下)

到這裡,我們的控件就可以瞭,接下來就是在佈局文件中,把它當作按鈕用瞭。

  

    

    

    

       

可以看到我們自己設定的custom:type的值是不同的。

然後我們就可以看到一大排自定義的按鈕瞭,想要什麼圖案,自己畫哦!

到這裡,其實沒完!

我發現有一個副作用。。。。

因為我們界面上有進度條嘛,所以其實界面一直在刷刷刷,那麼我們自定義的按鈕,也就一直在刷刷刷。。。

好瞭,睡覺瞭。源代碼請再等等,慢慢講,我還會慢慢改,然後最後會放上來的。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *