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其实原理是一样的,我们就直接来看的代码吧:

1)先在res/values/中创建一个attrs.xml文件,在里面自定义属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CustomAudioIcon">
        <attr name="type">
            <enum name="start" value="0" />            
            <enum name="forward" value="1" />
            <enum name="backward" value="2" />
            <enum name="exit" value="3" />
            <enum name="mode" value="4" />
        </attr>
        <attr name="color" format="color"/>
    </declare-styleable>
</resources>

我们定义了两个属性,其中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事件分发机制完全解析,带你从源码的角度彻底理解(下)

到这里,我们的控件就可以了,接下来就是在布局文件中,把它当作按钮用了。

  <com.example.nature.CustomAudioIcon
        android:id="@+id/btnMode"
        android:layout_width="64dip"
        android:layout_height="64dip"
        android:layout_alignParentBottom="true"
        custom:type="mode"
        custom:color="#66DD22" />

    <com.example.nature.CustomAudioIcon
        android:id="@+id/btnPrevious"
        android:layout_width="64dip"
        android:layout_height="64dip"
        android:layout_alignBaseline="@+id/btnMode"
        android:layout_alignParentBottom="true"
        android:layout_toRightOf="@+id/btnMode"
        custom:type="backward"
        custom:color="#66DD22" />

    <com.example.nature.CustomAudioIcon
        android:id="@+id/btnStartStop"
        android:layout_width="64dip"
        android:layout_height="64dip"
        android:layout_alignBaseline="@+id/btnMode"
        android:layout_alignParentBottom="true"
        android:layout_toRightOf="@+id/btnPrevious"
        custom:type="start"
        custom:color="#66DD22" />

    <com.example.nature.CustomAudioIcon
        android:id="@+id/btnNext"
        android:layout_width="64dip"
        android:layout_height="64dip"
        android:layout_alignBaseline="@+id/btnMode"
        android:layout_alignParentBottom="true"
        android:layout_toRightOf="@+id/btnStartStop"
        custom:type="forward"
        custom:color="#66DD22" />

    <com.example.nature.CustomAudioIcon
        android:id="@+id/btnExit"
        android:layout_width="64dip"
        android:layout_height="64dip"
        android:layout_alignBaseline="@+id/btnMode"
        android:layout_alignParentBottom="true"
        android:layout_toRightOf="@+id/btnNext"
        custom:type="exit"
        custom:color="#66DD22" />   

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

然后我们就可以看到一大排自定义的按钮了,想要什么图案,自己画哦!

到这里,其实没完!

我发现有一个副作用。。。。

因为我们界面上有进度条嘛,所以其实界面一直在刷刷刷,那么我们自定义的按钮,也就一直在刷刷刷。。。

好了,睡觉了。源代码请再等等,慢慢讲,我还会慢慢改,然后最后会放上来的。


郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。