在Android 實現簡單音樂播放器(一)中,我介紹了MusicPlayer的頁面設計。html
如今,我簡單總結一些功能實現過程當中的要點和有趣的細節,結合MainActivity.java代碼進行說明(寫出來可能有點碎……一貫不太會總結^·^)。java
1、功能菜單android
在MusicPlayer中,我添加了三個菜單:數據庫
search(搜索手機中的音樂文件,更新播放列表)、app
clear(清除播放列表……這個功能是最初加進去的,後來改進以後,已經沒什麼實際意義)、ide
exit(退出)。函數
menu_main.xmlpost
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>
關於菜單功能,直接上代碼,很簡單,就不作說明啦。重要的在後面。this
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 }
2、搜索音樂文件——search的實現spa
先看一下相關的全局變量:
1 private ListView musicListView; 2 private SimpleAdapter listAdapter; 3 private List<HashMap<String,String>> list=new ArrayList<>();
爲了播放音樂的便利,在播放器打開時,程序自動搜索音樂數據,將必要的信息保存在list中,並用ListView顯示出來,以供用戶進行選擇。
而這個MusicPlayer用於播放手機外部存儲設備(SD卡)的音樂,要搜索出SD卡中的所有音樂文件,主要有兩種方法:一、直接遍歷SD卡的File,判斷文件名後綴,找到音樂文件。這種方法能夠區別出必定格式的音樂文件,也能夠找到對應的歌詞文件,可是缺點是:遍歷搜索,速度很慢。二、用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();
3、選擇歌曲
好了,如今咱們已經有了播放列表,那麼下一個步驟天然是選擇要播放的歌曲咯。
首先,是下面代碼中涉及的幾個全局變量。
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 }
4、播放
有關播放的全局變量:
1 private MediaPlayer mediaPlayer; 2 private TextView currtimeView; 3 private TextView totaltimeView; 4 private SeekBar seekBar; 5 private AlwaysMarqueeTextView nameView; 6 private ImageButton playBtn;
這裏的播放,指的是音樂播放器的播放按鈕,它要實現的功能有兩個:一、IDLE狀態下,按下即開始播放;二、播放時,按下,暫停;再按下,繼續播放(這兩個狀態分別對應兩種按鈕圖片)。
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 });
5、中止
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 }
6、上一曲/下一曲
這兩個功能剛好對立,實現起來原理都是同樣的。這裏我就只貼出上一曲的程序咯。
按下上一曲的按鈕,將在該按鈕的動做響應函數裏面進行動做響應。
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));
7、退出時,釋放MediaPlayer
1 @Override 2 protected void onDestroy() { 3 if(mediaPlayer!=null){ 4 mediaPlayer.stop(); 5 mediaPlayer.release(); 6 } 7 super.onDestroy(); 8 }
8、用戶權限
因爲要播放SD卡中的音樂,咱們還要在AndroidManifest.xml中添加讀外部存儲的權限。
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
好了,到如今,一個擁有基本功能的音樂播放器就完工啦。
(總算寫完了~~~)
9、補充說明
因爲前面是按照各個小功能的實現來說的,比較碎,看上去很糊塗,也比較分化,固然,有點基礎的朋友應該也能獲取到本身想要的信息。
在這裏我再補充,貼上程序的全局變量和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!