zoukankan      html  css  js  c++  java
  • Android 实现简单音乐播放器(二)

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

    关于菜单功能,直接上代码,很简单,就不做说明啦。重要的在后面。

     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

    二、搜索音乐文件——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     }
    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

    五、停止

    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     }
    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));

    七、退出时,释放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     }
    View Code

    其实,onCreate主要是获取一些控件,然后就是给SeekBar和MusicPlayer添加必要的Listener。

    前面没有提过的就是MusicPlayer的两个Listener,一个是OnCompletionListener,这个是监听音乐播放结束,我这里的实现也比较简单,当列表中的音乐超过1首,那就播放下一曲。另一个是OnErrorListener,这个是监听音乐播放出错,当出错的时候,我们就把MusicPlayer进行reset。关于MusicPlayer的使用,建议参考Android的文档。

    Over!

  • 相关阅读:
    02-自定义CALayer
    01-CALayer的基本操作
    抽屉效果
    手势识别
    事件响应
    寻找最合适的view
    hitTest方法与PointInside方法
    02-事件的产生与传递
    OC图标+启动图
    OC多线程操作
  • 原文地址:https://www.cnblogs.com/tt2015-sz/p/4505422.html
Copyright © 2011-2022 走看看