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的值是不同的。
然后我们就可以看到一大排自定义的按钮了,想要什么图案,自己画哦!
到这里,其实没完!
我发现有一个副作用。。。。
因为我们界面上有进度条嘛,所以其实界面一直在刷刷刷,那么我们自定义的按钮,也就一直在刷刷刷。。。
好了,睡觉了。源代码请再等等,慢慢讲,我还会慢慢改,然后最后会放上来的。