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文件,在里面自定义属性:
我们定义了两个属性,其中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();
}
}
因为这几个控件我都是在布局文件中定义好长宽的,所以不需要在这里面重写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() 函数重新刷新页面,就达到点击的效果了。
但是在这里,我们不能这么做,因为我们在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事件分发机制完全解析,带你从源码的角度彻底理解(下)
到这里,我们的控件就可以了,接下来就是在布局文件中,把它当作按钮用了。
然后我们就可以看到一大排自定义的按钮了,想要什么图案,自己画哦!
到这里,其实没完!
我发现有一个副作用。。。。
因为我们界面上有进度条嘛,所以其实界面一直在刷刷刷,那么我们自定义的按钮,也就一直在刷刷刷。。。
好了,睡觉了。源代码请再等等,慢慢讲,我还会慢慢改,然后最后会放上来的。