Android 實現簡單音樂播放器(二)

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>
View Code

關於菜單功能,直接上代碼,很簡單,就不作說明啦。重要的在後面。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     }
View Code

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     }
View Code

補充說明,這裏還有一個附加功能的實現,就是在播放音樂的過程當中,用手去滑動進度條,改變進度時,音樂播放的進度也隨之跳到相應地進度(相信這個功能也是音樂播放器必備的功能啦)。

具體實現,就是在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         });
View Code

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     }
View Code

比較難的地方,就是如何在按下上一曲(或下一曲)的時候,實現出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     }
View Code

其實,onCreate主要是獲取一些控件,而後就是給SeekBar和MusicPlayer添加必要的Listener。

前面沒有提過的就是MusicPlayer的兩個Listener,一個是OnCompletionListener,這個是監聽音樂播放結束,我這裏的實現也比較簡單,當列表中的音樂超過1首,那就播放下一曲。另外一個是OnErrorListener,這個是監聽音樂播放出錯,當出錯的時候,咱們就把MusicPlayer進行reset。關於MusicPlayer的使用,建議參考Android的文檔。

Over!

相關文章
相關標籤/搜索