在Android 实现简单音乐播放器(一)中,我介绍了MusicPlayer的页面设计。
现在,我简单总结一些功能实现过程中的要点和有趣的细节,结合MainActivity.java代码进行说明(写出来可能有点碎……一向不太会总结^·^)。
一、功能菜单
在MusicPlayer中,我添加了三个菜单:
search(搜索手机中的音乐文件,更新播放列表)、
clear(清除播放列表……这个功能是最初加进去的,后来改进之后,已经没什么实际意义)、
exit(退出)。
menu_main.xml
1 <menu xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:app="http://schemas.android.com/apk/res-auto" 3 xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity"> 4 <item android:id="@+id/action_search" android:title="search" 5 android:orderInCategory="100" app:showAsAction="never" /> 6 <item android:id="@+id/action_clear" android:title="clear" 7 android:orderInCategory="100" app:showAsAction="never" /> 8 <item android:id="@+id/action_exit" android:title="exit" 9 android:orderInCategory="100" app:showAsAction="never" /> 10 </menu>
关于菜单功能,直接上代码,很简单,就不做说明啦。重要的在后面。
1 @Override 2 public boolean onCreateOptionsMenu(Menu menu) { 3 // Inflate the menu; this adds items to the action bar if it is present. 4 getMenuInflater().inflate(R.menu.menu_main, menu); 5 return true; 6 } 7 8 @Override 9 public boolean onOptionsItemSelected(MenuItem item) { 10 // Handle action bar item clicks here. The action bar will 11 // automatically handle clicks on the Home/Up button, so long 12 // as you specify a parent activity in AndroidManifest.xml. 13 int id = item.getItemId(); 14 15 //noinspection SimplifiableIfStatement 16 if (id == R.id.action_search) { 17 progressDialog=ProgressDialog.show(this,"","正在搜索音乐",true); 18 searchMusicFile(); 19 return true; 20 }else if(id==R.id.action_clear){ 21 list.clear(); 22 listAdapter.notifyDataSetChanged(); 23 return true; 24 }else if(id==R.id.action_exit){ 25 flag=false; 26 mediaPlayer.stop(); 27 mediaPlayer.release(); 28 this.finish(); 29 return true; 30 } 31 return super.onOptionsItemSelected(item); 32 }
二、搜索音乐文件——search的实现
先看一下相关的全局变量:
1 private ListView musicListView; 2 private SimpleAdapter listAdapter; 3 private List<HashMap<String,String>> list=new ArrayList<>();
为了播放音乐的便利,在播放器打开时,程序自动搜索音乐数据,将必要的信息保存在list中,并用ListView显示出来,以供用户进行选择。
而这个MusicPlayer用于播放手机外部存储设备(SD卡)的音乐,要搜索出SD卡中的全部音乐文件,主要有两种方法:1、直接遍历SD卡的File,判断文件名后缀,找到音乐文件。这种方法可以区别出一定格式的音乐文件,也可以找到对应的歌词文件,但是缺点是:遍历搜索,速度很慢。2、用Android提供的多媒体数据库MediaStore,直接用ContentResolver的query方法,就可以对MediaStore进行搜索啦,非常高效(果断选用这种方式~~),但是数据库里面没有歌词(泪目T_T~~~暂时放弃歌词播放的功能啦,以后要是想起来,再加上吧……)
1 private void searchMusicFile(){ 2 // 如果list不是空的,就先清空 3 if(!list.isEmpty()){ 4 list.clear(); 5 } 6 ContentResolver contentResolver=getContentResolver(); 7 //搜索SD卡里的music文件 8 Uri uri= MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 9 String[] projection={ 10 MediaStore.Audio.Media._ID, //根据_ID可以定位歌曲 11 MediaStore.Audio.Media.TITLE, //这个是歌曲名 12 MediaStore.Audio.Media.DISPLAY_NAME, //这个是文件名 13 MediaStore.Audio.Media.ARTIST, 14 MediaStore.Audio.Media.IS_MUSIC, 15 MediaStore.Audio.Media.DATA 16 }; 17 String where=MediaStore.Audio.Media.IS_MUSIC+">0"; 18 Cursor cursor=contentResolver.query(uri,projection,where,null, MediaStore.Audio.Media.DATA); 19 while (cursor.moveToNext()){ 20 //将歌曲的信息保存到list中 21 //其中,TITLE和ARTIST是用来显示到ListView中的 22 // _ID和DATA都可以用来播放音乐,其实保存任一个就可以 23 String songName=cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)); 24 String artistName=cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)); 25 String id=Integer.toString(cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media._ID))); 26 String data=Integer.toString(cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.DATA))); 27 HashMap<String,String> map=new HashMap<>(); 28 map.put("name",songName); 29 map.put("artist",artistName); 30 map.put("id",id); 31 map.put("data",data); 32 list.add(map); 33 } 34 cursor.close(); 35 //搜索完毕之后,发一个message给Handler,对ListView的显示内容进行更新 36 handler.sendEmptyMessage(SEARCH_MUSIC_SUCCESS); 37 }
搜索完了,要对ListView进行更新,这里的更新,在Handler中完成(也包括后面要讲到的播放时间的实时更新)。
这里用了两个常量来区别handler要处理的消息类别。
private static final int SEARCH_MUSIC_SUCCESS=0; private static final int CURR_TIME_VALUE=1;
取名无能,其实感觉统一一下Message的命名可能对于理解的帮助会更好(比如MSG_MUSIC_SERCH,MSG_TIME_MODIFY),下次改正~
1 private Handler handler=new Handler(){ 2 @Override 3 public void handleMessage(Message message){ 4 switch (message.what){ 5 //更新播放列表 6 case SEARCH_MUSIC_SUCCESS: 7 listAdapter=new SimpleAdapter(MainActivity.this,list,R.layout.musiclist, 8 new String[]{"name","artist"}, new int[]{R.id.songName,R.id.artistName}); 9 MainActivity.this.setListAdapter(listAdapter); 10 Toast.makeText(MainActivity.this,"找到"+list.size()+"份音频文件",Toast.LENGTH_LONG).show(); 11 progressDialog.dismiss(); 12 break; 13 //更新当前歌曲的播放时间 14 case CURR_TIME_VALUE: 15 currtimeView.setText(message.obj.toString()); 16 break; 17 default: 18 break; 19 } 20 } 21 };
仔细观察上面的handler以及搜索菜单中的动作,可以看到,在搜索音乐的过程中用到了一个进程对话框progressDialog,这是一个定义的全局变量,为了能随时启动和关闭。
private ProgressDialog progressDialog=null;
当搜索音乐的用时较长的时候,这个对话框就会显示一个一直在转的圆圈,并显示"正在搜索音乐"的字样,用来显示当前的进程。不过,不知道是由于我手机里面的音乐比较少(20多首),还是本身读取Android的MediaStore数据库的速度就很快,这个对话框存在的时间很短(几乎一闪而过,甚至闪都不闪)。虽然在这里,这个对话框实际意义并不大,还是把它的实现贴出来,备着以后用吧。
要显示这个对话框的时候,使用ProgressDialog的类方法show(),设置一些必要地参数,具体请参考Android的文档。
progressDialog=ProgressDialog.show(this,"","正在搜索音乐",true);
要关闭这个对话框的时候,使用它的dismiss()方法.
progressDialog.dismiss();
三、选择歌曲
好了,现在我们已经有了播放列表,那么下一个步骤自然是选择要播放的歌曲咯。
首先,是下面代码中涉及的几个全局变量。
private int currState=IDLE;//当前播放器的状态 private int currPosition;//list的当前选中项的索引值(第一项对应0) private String nameChecked;//当前选中的音乐名 private Uri uriChecked;//当前选中的音乐对应的Uri private AlwaysMarqueeTextView nameView;// 页面中用来显示当前选中音乐名的TextView
我们来看一下播放器的不同状态(currState可以取的几个值):
1 // 定义当前播放器的状态 2 private static final int IDLE=0; //空闲:没有播放音乐 3 private static final int PAUSE=1; //暂停:播放音乐时暂停 4 private static final int START=2; //正在播放音乐
选择歌曲,在IDLE状态下才有效。选中歌曲之后,要在具有跑马灯效果的TextView中显示歌名,并且更新播放总时长。
1 @Override 2 protected void onListItemClick(ListView l, View v, int position, long id) { 3 super.onListItemClick(l, v, position, id); 4 if(currState==IDLE) { 5 // 若在IDLE状态下,选中list中的item,则改变相应项目 6 HashMap<String, String> map = list.get(position); 7 nameChecked = map.get("name"); 8 Long idChecked = Long.parseLong(map.get("id")); 9 //uriChecked:选中的歌曲相对应的Uri 10 uriChecked = Uri.parse(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + "/" + idChecked); 11 nameView.setText(nameChecked); 12 currPosition = position; //这个是歌曲在列表中的位置,“上一曲”“下一曲”功能将会用到 13 } 14 }
四、播放
有关播放的全局变量:
1 private MediaPlayer mediaPlayer; 2 private TextView currtimeView; 3 private TextView totaltimeView; 4 private SeekBar seekBar; 5 private AlwaysMarqueeTextView nameView; 6 private ImageButton playBtn;
这里的播放,指的是音乐播放器的播放按钮,它要实现的功能有两个:1、IDLE状态下,按下即开始播放;2、播放时,按下,暂停;再按下,继续播放(这两个状态分别对应两种按钮图片)。
1 ExecutorService executorService= Executors.newSingleThreadExecutor(); 2 public void onPlayClick(View v){ 3 switch (currState){ 4 case IDLE: 5 start(); 6 currState=START; 7 break; 8 case PAUSE: 9 mediaPlayer.start(); 10 playBtn.setImageDrawable(getResources().getDrawable(R.drawable.player_pause)); 11 currState=START; 12 break; 13 case START: 14 mediaPlayer.pause(); 15 playBtn.setImageDrawable(getResources().getDrawable(R.drawable.player_play)); 16 currState=PAUSE; 17 break; 18 } 19 } 20 private void start(){ 21 if(uriChecked!=null){ 22 mediaPlayer.reset(); 23 try { 24 mediaPlayer.setDataSource(MainActivity.this,uriChecked); 25 mediaPlayer.prepare(); 26 mediaPlayer.start(); 27 initSeekBar(); 28 nameView.setText(nameChecked); 29 playBtn.setImageDrawable(getResources().getDrawable(R.drawable.player_pause)); 30 currState=START; 31 executorService.execute(new Runnable() { 32 @Override 33 public void run() { 34 flag=true; 35 while(flag){ 36 if(mediaPlayer.getCurrentPosition()<seekBar.getMax()){ 37 seekBar.setProgress(mediaPlayer.getCurrentPosition()); 38 Message msg=handler.obtainMessage(CURR_TIME_VALUE, 39 toTime(mediaPlayer.getCurrentPosition())); 40 handler.sendMessage(msg); 41 try { 42 Thread.sleep(500); 43 } catch (InterruptedException e) { 44 e.printStackTrace(); 45 } 46 }else { 47 flag=false; 48 } 49 } 50 } 51 }); 52 } catch (IOException e) { 53 e.printStackTrace(); 54 } 55 }else{ 56 Toast.makeText(this, "播放列表为空或尚未选中曲目", Toast.LENGTH_LONG).show(); 57 } 58 }
在播放时,播放进度体现在当前播放时长和进度条的变化上。因此,按下播放键时,我们要对进度条进行初始化。
1 private void initSeekBar(){ 2 int duration=mediaPlayer.getDuration(); 3 seekBar.setMax(duration); 4 seekBar.setProgress(0); 5 if(duration>0){ 6 totaltimeView.setText(toTime(duration)); 7 } 8 }
播放过程中,实时更新播放时间和进度条的工作则用一个ExecutorService来完成。
把时长(毫秒数)转化为时间格式(00:00)的方法:
1 private String toTime(int duration){ 2 Date date=new Date(); 3 SimpleDateFormat sdf=new SimpleDateFormat("mm:ss", Locale.getDefault()); 4 sdf.setTimeZone(TimeZone.getTimeZone("GMT+0")); 5 date.setTime(duration); 6 return sdf.format(date); 7 }
补充说明,这里还有一个附加功能的实现,就是在播放音乐的过程中,用手去滑动进度条,改变进度时,音乐播放的进度也随之跳到相应地进度(相信这个功能也是音乐播放器必备的功能啦)。
具体实现,就是在OnCreate()中,给SeekBar增加一个OnSeekBarChangeListener(),代码如下:
1 seekBar=(SeekBar)findViewById(R.id.seekBar); 2 seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 3 @Override 4 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 5 if(currState==START){ 6 if(fromUser){ //如果是人为改变进度,则改变相应地显示时长 7 currtimeView.setText(toTime(progress)); 8 } 9 } 10 } 11 12 @Override 13 public void onStartTrackingTouch(SeekBar seekBar) { 14 //开始拖动进度条,将音乐播放器停止 15 mediaPlayer.pause(); 16 } 17 18 @Override 19 public void onStopTrackingTouch(SeekBar seekBar) { 20 //结束拖动进度条,按照新的进度继续播放音乐 21 if(currState==START){ 22 mediaPlayer.seekTo(seekBar.getProgress()); 23 mediaPlayer.start(); 24 } 25 } 26 });
五、停止
1 private void stop() { 2 initState(); 3 mediaPlayer.stop(); 4 currState = IDLE; 5 }
停止功能很简单,注意在停止播放时,更新必要的信息(包括按钮、状态、进度条、时间等等),我就不赘述啦
在这里补充一下initState(),其实具体就是更新页面,使时间/进度条/按钮等等都恢复到歌曲尚未播放时的状态,具体代码如下:
1 private void initState(){ 2 nameView.setText(""); 3 currtimeView.setText("00:00"); 4 totaltimeView.setText("00:00"); 5 flag = false; 6 seekBar.setProgress(0); 7 playBtn.setImageDrawable(getResources().getDrawable(R.drawable.player_play)); 8 }
六、上一曲/下一曲
这两个功能恰好对立,实现起来原理都是一样的。这里我就只贴出上一曲的程序咯。
按下上一曲的按钮,将在该按钮的动作响应函数里面进行动作响应。
public void onPreviousClick(View v){ previous(); }
具体previous()做了什么呢,主要是根据音乐列表的当前选中的索引值,使列表滑动到前一个列表项(currPosition-1)并进行点击(这里的点击是用ListView的performItemClick()方法来实现的,没有用到人的手指哟),并且根据当前的播放状态作出相对应的音乐控制,代码如下:
1 private void previous(){ 2 if(musicListView.getCount()>0){ 3 if(currPosition>0){ 4 switch (currState){ 5 case IDLE: 6 musicListView.smoothScrollToPosition(currPosition - 1); 7 musicListView.performItemClick( 8 musicListView.getAdapter().getView(currPosition-1,null,null), 9 currPosition-1, 10 musicListView.getItemIdAtPosition(currPosition-1)); 11 break; 12 case START: 13 case PAUSE: 14 stop(); 15 musicListView.smoothScrollToPosition(currPosition - 1); 16 musicListView.performItemClick( 17 musicListView.getAdapter().getView(currPosition - 1, null, null), 18 currPosition - 1, 19 musicListView.getItemIdAtPosition(currPosition-1)); 20 break; 21 } 22 }else{ 23 switch (currState) { 24 case IDLE: 25 musicListView.smoothScrollToPosition(musicListView.getCount() - 1); 26 musicListView.performItemClick( 27 musicListView.getAdapter().getView(musicListView.getCount()-1, null, null), 28 musicListView.getCount()-1, 29 musicListView.getItemIdAtPosition(musicListView.getCount()-1)); 30 break; 31 case START: 32 case PAUSE: 33 stop(); 34 musicListView.smoothScrollToPosition(musicListView.getCount() - 1); 35 musicListView.performItemClick( 36 musicListView.getAdapter().getView(musicListView.getCount()-1, null, null), 37 musicListView.getCount()-1, 38 musicListView.getItemIdAtPosition(musicListView.getCount()-1)); 39 start(); 40 break; 41 } 42 } 43 } 44 }
比较难的地方,就是如何在按下上一曲(或下一曲)的时候,实现出ListView的点击效果。
1 //使选中的歌曲滑动到页面显示范围内 2 musicListView.smoothScrollToPosition(currPosition - 1); 3 //单击ListView中的Item 4 musicListView.performItemClick( musicListView.getAdapter().getView(currPosition-1,null,null),currPosition-1, 5 musicListView.getItemIdAtPosition(currPosition-1));
七、退出时,释放MediaPlayer
1 @Override 2 protected void onDestroy() { 3 if(mediaPlayer!=null){ 4 mediaPlayer.stop(); 5 mediaPlayer.release(); 6 } 7 super.onDestroy(); 8 }
八、用户权限
由于要播放SD卡中的音乐,我们还要在AndroidManifest.xml中添加读外部存储的权限。
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
好了,到现在,一个拥有基本功能的音乐播放器就完工啦。
(总算写完了~~~)
九、补充说明
由于前面是按照各个小功能的实现来讲的,比较碎,看上去很糊涂,也比较分化,当然,有点基础的朋友应该也能获取到自己想要的信息。
在这里我再补充,贴上程序的全局变量和OnCreate部分做的动作,前面看不懂的可以到这里找找。
1 private static final String TAG="yang"; 2 private static final int SEARCH_MUSIC_SUCCESS=0; 3 private ProgressDialog progressDialog=null; 4 private ListView musicListView; 5 private SimpleAdapter listAdapter; 6 private List<HashMap<String,String>> list=new ArrayList<>(); 7 8 private MediaPlayer mediaPlayer; 9 private TextView currtimeView; 10 private TextView totaltimeView; 11 private SeekBar seekBar; 12 private AlwaysMarqueeTextView nameView; 13 private ImageButton playBtn; 14 15 private String nameChecked; 16 private Uri uriChecked; 17 18 private int currPosition;//当前选中的list 19 20 // 定义当前播放器的状态 21 private static final int IDLE=0; //空闲:没有播放音乐 22 private static final int PAUSE=1; //暂停:播放音乐时暂停 23 private static final int START=2; //正在播放音乐 24 25 private static final int CURR_TIME_VALUE=1; 26 27 private int currState=IDLE;//当前播放器的状态 28 private boolean flag=false;//控制进度条的索引 29 30 31 ExecutorService executorService= Executors.newSingleThreadExecutor(); 32 33 @Override 34 protected void onCreate(Bundle savedInstanceState) { 35 super.onCreate(savedInstanceState); 36 setContentView(R.layout.activity_main); 37 38 musicListView=(ListView)findViewById(android.R.id.list); 39 currtimeView=(TextView)findViewById(R.id.currTime); 40 totaltimeView=(TextView)findViewById(R.id.totalTime); 41 seekBar=(SeekBar)findViewById(R.id.seekBar); 42 seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 43 @Override 44 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 45 if(currState==START){ 46 if(fromUser){ 47 currtimeView.setText(toTime(progress)); 48 } 49 } 50 } 51 52 @Override 53 public void onStartTrackingTouch(SeekBar seekBar) { 54 mediaPlayer.pause(); 55 } 56 57 @Override 58 public void onStopTrackingTouch(SeekBar seekBar) { 59 if(currState==START){ 60 mediaPlayer.seekTo(seekBar.getProgress()); 61 mediaPlayer.start(); 62 } 63 } 64 }); 65 66 nameView=(AlwaysMarqueeTextView)findViewById(R.id.nameDisplay); 67 playBtn=(ImageButton)findViewById(R.id.play); 68 69 mediaPlayer=new MediaPlayer(); 70 mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { 71 @Override 72 public void onCompletion(MediaPlayer mp) { 73 if (musicListView.getCount() > 0) { 74 next(); 75 } else { 76 Toast.makeText(MainActivity.this, "播放列表为空", Toast.LENGTH_LONG).show(); 77 } 78 } 79 }); 80 mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { 81 @Override 82 public boolean onError(MediaPlayer mp, int what, int extra) { 83 mediaPlayer.reset(); 84 return false; 85 } 86 }); 87 // 搜索MediaStore中的音频文件,填充文件列表 88 progressDialog=ProgressDialog.show(this,"","正在搜索音乐",true); 89 searchMusicFile(); 90 91 }
其实,onCreate主要是获取一些控件,然后就是给SeekBar和MusicPlayer添加必要的Listener。
前面没有提过的就是MusicPlayer的两个Listener,一个是OnCompletionListener,这个是监听音乐播放结束,我这里的实现也比较简单,当列表中的音乐超过1首,那就播放下一曲。另一个是OnErrorListener,这个是监听音乐播放出错,当出错的时候,我们就把MusicPlayer进行reset。关于MusicPlayer的使用,建议参考Android的文档。
Over!