【Android】简易的本地音乐播放器

前言

Github: SimpleMediaPlayerDemo 下载
主要实现的功能:
1、桌面小工具Widget,可实现切换歌曲、暂停/开始播放、显示歌名、歌手名
2、切换歌曲时具有系统栏提示,快速导航返回播放界面
3、搜索SD卡中的音乐文件,加入ListView中
4、滑动控制条(SeekBar) 拖动可控制音乐播放进度
5、正在播放歌曲可以在ListView中标记出来
6、播放界面播放/暂停、切歌功能
大致思路:
对于音乐数据的存储:

	我们可以将SD卡中的音乐文件通过Cursor,将音乐标题、歌手名、播放时长、存储地址、音乐专辑图信息拿出来,
写入到Android的提供SQLite数据库中,我们在需要需要使用到上述音乐文件信息的地方,直接拿着音乐的位置信息
到数据库中查找对应的音乐文件即可;

对于数据库的建立:

		我们需要一个建立一个数据库的对象和一个对这个数据库对象操作的方法类,这样我们对数据库的增删查就可以
	实现多个Activity共用一个数据库对象。我们的数据库里面需要存储SD卡中搜索到的音乐文件具体信息,这些具
	体的音乐信息又需要我们提供一个具体的JavaBean类。这样我们每次查到的音乐数据,都可以返回一个包含所有我们
	需要的音乐信息的JavaBean。
	
关于服务:

		对于音乐播放器,我们首先需要一个音乐播放服务,它可以接受来自我们列表界面、播放界面和桌面小程序Widget的
	播放动作,音乐播放服务可以将这些动作,转换成一个具体的位置信息,然后带着位置信息去数据库中查照该歌曲的
	存储地址,完成播放;
	
		对于系统消息提示,我们也需要一个消息提示类,Android给我们提供了一个NotificationManager可以便于我们调
	用系统的消息提示服务,我们需要将当前正在播放的音乐文件信息发送给Notification,再通过Notification-
	Manager发布出去就OK了!

关于数据库部分

首先我们需要确立,我们的数据库里面存什么:
音乐地址 ----------------------------- 用于提供给系统Mediaplayer一个播放文件
音乐名称 ----------------------------- 音乐列表、桌面Widget、播放界面、系统提示…用于显示
歌手 ------------------------------------ 同上
音乐的图片地址---------------------- 播放界面用于显示
持续时间 ------------------------------- 我们的滑动条需要显示时间、调节进度
一个自增的主键 ----------------------- 用于确定我们的音乐次序

根据上面的需求,我们可以自己写一个JavaBean即一个音乐文件信息类:

//音乐文件的包含内容
public MusicItem(String title, String artist, String path, long duration, String imagePath) {

        this.artist = artist;
        this.duration = duration;
        this.path = path;
        this.title = title;
        this.imagePath = imagePath;
    }
//依据音乐文件地址,获得音乐的专辑图片
 public Bitmap loadPicture(String path){

        MediaMetadataRetriever mediaMetadataRetriever=new MediaMetadataRetriever();
        mediaMetadataRetriever.setDataSource(path);
        byte[] picture = mediaMetadataRetriever.getEmbeddedPicture();
        Bitmap bitmap= BitmapFactory.decodeByteArray(picture,0,picture.length);
        return bitmap;
    }

数据库的建立没什么好说的,列名和我们的JavaBean对应即可:

 db.execSQL("create table my_music (" +
                "id integer  primary key autoincrement," +
                "title text," +
                "artist text, " +
                "path text, " +
                "duration long, " +
                "imagePath text)");
    }

有了数据库对象,我们接下来需要对这个对象进行增删该查的操作,建立一个数据库操作类,添加数据库的增删查三个方法:

//增
    public void add(MusicItem music){

        db = helper.getWritableDatabase();
        db.execSQL("insert into my_music (title, artist, path, duration, imagePath) values (?, ?, ?, ?, ?)",
                new Object[]{music.getTitle(), music.getArtist(), music.getPath(), music.getDuration(), music.getImagePath()});
    }


//删
public void delete(){

        db = helper.getWritableDatabase();
        db.delete("my_music", null, null);
        db.execSQL("update sqlite_sequence set seq=0 where name='my_music'" );
    }


//查
public MusicItem find(int id){

        db = helper.getWritableDatabase();
        Cursor cursor = db.rawQuery("select * from my_music where id = ?", new String[]{
                String.valueOf(id)
        });
        if (cursor.moveToNext()){
            return new MusicItem(cursor.getString(cursor.getColumnIndex("title")),
                    cursor.getString(cursor.getColumnIndex("artist")),
                    cursor.getString(cursor.getColumnIndex("path")),
                    cursor.getLong(cursor.getColumnIndex("duration")),
                    cursor.getString(cursor.getColumnIndex("imagePath")));
        }
        return null;
    }


//获得数据库最后一条记录的自增键
public int getCount(){

        db = helper.getWritableDatabase();
        Cursor cursor = db.rawQuery("select count(id) from my_music", null);
        if (cursor.moveToNext()){
            return cursor.getInt(0);
        }
        return 0;
    }

音乐文件的信息获取可以通过Android提供的cursor.getColumnIndex(MediaStore.Audio.Media.×××××)进行获取
具体操作代码如下:

    //先确定搜索对象是外部存储卡的音乐文件
    Cursor cursor = context.getContentResolver()
            .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                    null,
                    null,
                    null,
                    MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
		//对搜索到的音乐文件信息分类
        int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
        int artistIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
        int pathIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);
        int durationIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION);
        int imagePathIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID);

        while (cursor.moveToNext()) {

            String Path = cursor.getString(pathIndex);
            String Title = cursor.getString(titleIndex);
            String Artist = cursor.getString(artistIndex);
            long Duration = cursor.getLong(durationIndex);
            String imagePath = cursor.getString(imagePathIndex);
            MusicItem song = new MusicItem(Title, Artist, Path, Duration, imagePath);

			//存入数据库
            musicDAO.add(song);
        }
        cursor.close();

音乐列表

这里我的音乐列表使用的是ListView适配自定义BaseAdapter,追求极简也可以使用Android自带的SimpleAdapter, 如果想要美观且高性能,我推荐尝试一下RecyclerView。

这里我选用的是ListView,那么老规矩,先写适配器:

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder;

        if (convertView == null) {
        
            viewHolder = new ViewHolder();
            convertView = mInflater.inflate(R.layout.music_adapter, null);
            viewHolder.title = convertView.findViewById(R.id.Title);
            viewHolder.singer = convertView.findViewById(R.id.Singer);
            convertView.setTag(viewHolder);

        } else {

            viewHolder = (ViewHolder) convertView.getTag();
        }

        viewHolder.title.setText(musicItem.get(position).getTitle());
        viewHolder.singer.setText(musicItem.get(position).getArtist());

        return convertView;
    }

这里就只贴 getView () 部分了,其他的写法很简单。接下来我们应用适配器:

		MusicAdapter adapter;
		ListView listView;
		List musicList;
		... ...
		adapter = new MusicAdapter(this, musicList);
        listView = findViewById(R.id.list_view);
        listView.setAdapter(adapter);

到这里,我们的基础搭建的就差不多了,接下来是整个程序的功能实现部分,先来理一下逻辑:
我们点击音乐列表 -------> 获得position ---------> 传给音乐播放服务(开始播放)+ 传给播放界面(显示时长、显示专辑图片、显示歌名/歌手名)+ 传给桌面Widget (显示歌名/歌手名)+ 传给系统消息提示服务(提示"×××歌曲开始播放啦!")
因为我们的自增主键ID = position + 1 ,所以各个界面在拿到这个position之后,都会带着这个position 到数据库中找对应的歌曲信息:

MusicDAO musicDAO = new MusicDAO(this);
musicDAO.find(position).get××××()

这里我们可以使用全局广播,发送 position 来通知各个Activity更新自己的工作对象,所以我们要在列表界面开启各自的服务

	 //广播发送给服务
     Intent intent1 = new Intent(Utils.POSITION);
     intent1.putExtra("position", position + 1);

   	 //广播发送给note服务
     Intent intent3 = new Intent("data_for_note");
     intent3.putExtra("position", position + 1);

发送全局广播,通知各个Activity:

	//广播发送给插件
    Intent intent2 = new Intent(Utils.POSITION_FOR_WIDGET);
    intent2.putExtra("position", position + 1);

    //广播发送回ListView
    Intent intent4 = new Intent("position_for_list");
    intent4.putExtra("position", position + 1);
	            
	//通过跳转发送给播放界面
    Intent intent = new Intent(context, PlayActivity.class);
    intent.putExtra("position", position + 1);

音乐播放界面

首先是我们接受到上一个列表界面传过来的position,更新UI:

	imageView.setImageBitmap(musicDAO.find(position).loadPicture(musicDAO.find(position).getPath()));
    totalTime.setText(simpleDateFormat.format(musicDAO.find(position).getDuration()));
    tv_Title.setText(musicDAO.find(position).getTitle());
    tv_Singer.setText(musicDAO.find(position).getArtist());
    seekBar.setMax((int) musicDAO.find(position).getDuration());
关于进度条

当前播放进度我们可以从service中的 mediaPlayer.getCurrentPosition() 方法中获得, 我们只需要通过绑定服务后返回的 binder对象,来调用服务中的getCurrentPosition()方法获取当前播放进度:

//获取返回的binder对象
 @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        binder = (MusicService.Binder) service;
    }
//更新UI
Handler handler = new Handler() {
         @Override
         public void handleMessage(Message msg) {
             super.handleMessage(msg);
             if (msg.what == 1) {
                 if (binder != null) {
                     seekBar.setProgress(binder.getProgress());
                     nowTime.setText(simpleDateFormat.format(binder.getProgress()));
                 }
             }
             Message message = Message.obtain();
             message.what = 1;
             handler.sendMessageDelayed(message, 500);
         }
     };

关于我们拖动进度条,调整音乐播放进度,则是通过onProgressChanged()方法,给Service发送一个progress的广播:

@Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        nowTime.setText(simpleDateFormat.format(progress));
        if (fromUser){
            Intent intent = new Intent(Utils.SEEKBAR);
            intent.putExtra("progress", progress);
            sendBroadcast(intent);
        }
    }
更新UI

接下来就很简单了,点击切换歌曲,就是给MusicService发送一个动作广播:

case R.id.Next_Button:
                intent.putExtra("action", Utils.NEXT_ACTION);
                start_or_stop_view.setBackgroundResource(R.mipmap.ic_pause);
                status = 1;
                position ++;
                sendBroadcast(intent);
                break;

            case R.id.pre_Button:
                intent.putExtra("action", Utils.PRE_ACTION);
                start_or_stop_view.setBackgroundResource(R.mipmap.ic_pause);
                status = 1;
                position --;
                sendBroadcast(intent);
                break;

            case R.id.play_or_stop:
                intent.putExtra("action", Utils.START_OR_STOP);
                if (status == 0){
                    start_or_stop_view.setBackgroundResource(R.mipmap.ic_pause);
                    status = 1;
                } else if (status == 1){
                    start_or_stop_view.setBackgroundResource(R.mipmap.ic_play);
                    status = 0;
                }
                sendBroadcast(intent);
                break;

            default:
                break;
        }
    }

更新前一首、后一首的UI,这里需要注意下,判断是否是数据库的第一首或者最后一首,代码主要放在SetView() 方法中:
Intent intent = new Intent(Utils.POSITION_FOR_WIDGET);

    if (position == 0) {

        imageView.setAlpha(0.5f);
    	imageView.setImageBitmap(musicDAO.find(musicDAO.getCount()).loadPicture(musicDAO.find(musicDAO.getCount()).getPath()));
        totalTime.setText(simpleDateFormat.format(musicDAO.find(musicDAO.getCount()).getDuration()));
        tv_Title.setText(musicDAO.find(musicDAO.getCount()).getTitle());
        tv_Singer.setText(musicDAO.find(musicDAO.getCount()).getArtist());
        seekBar.setMax((int) musicDAO.find(musicDAO.getCount()).getDuration());
        position = musicDAO.getCount();
        
    } else if (position > musicDAO.getCount()) {

        imageView.setAlpha(0.7f);
        imageView.setImageBitmap(musicDAO.find(1).loadPicture(musicDAO.find(1).getPath()));
        totalTime.setText(simpleDateFormat.format(musicDAO.find(1).getDuration()));
        tv_Title.setText(musicDAO.find(1).getTitle());
        tv_Singer.setText(musicDAO.find(1).getArtist());
        seekBar.setMax((int) musicDAO.find(1).getDuration());
        position = 1;
        
    } else {
    
        imageView.setAlpha(0.5f);
        imageView.setImageBitmap(musicDAO.find(position).loadPicture(musicDAO.find(position).getPath()));
        totalTime.setText(simpleDateFormat.format(musicDAO.find(position).getDuration()));
        tv_Title.setText(musicDAO.find(position).getTitle());
        tv_Singer.setText(musicDAO.find(position).getArtist());
        seekBar.setMax((int) musicDAO.find(position).getDuration());
    }
}

音乐播放服务 (Music Service)

和上面一样,我们先来梳理一下逻辑:
【Android】简易的本地音乐播放器_第1张图片
既然要接收消息,那么先来个BroadcastReceiver:
注册监听器:

		IntentFilter filter = new IntentFilter();
        filter.addAction(Utils.POSITION);
        filter.addAction(Utils.CONTROL_ACTION);
        filter.addAction("data_from_widget");
        filter.addAction("pop_action");
        filter.addAction(Utils.SEEKBAR);
        registerReceiver(myReceiver, filter);

内部逻辑的实现:

public class MyReceiver extends BroadcastReceiver {

        @RequiresApi(api = Build.VERSION_CODES.KITKAT)
        @Override
        public void onReceive(Context context, Intent intent) {
            nowPlay = intent.getIntExtra("position", 0);
            String pop = intent.getStringExtra("pop_action");
            action = intent.getStringExtra("action");
            int progress = intent.getIntExtra("progress", -1);
           if (nowPlay !=  0 ){
               position = nowPlay;
               prepareMusic(position);
           } else if (pop != null){
               if (mediaPlayer.isPlaying()){
                   mediaPlayer.pause();
               } else {
                   mediaPlayer.start();
               }
           }else if (action != null){
               switch (Objects.requireNonNull(intent.getAction())){
                   case Utils.CONTROL_ACTION:
                       if (action.equals(Utils.NEXT_ACTION)){
                           next();
                       } else if (action.equals(Utils.PRE_ACTION)){
                           pre();
                       } else if (action.equals(Utils.START_OR_STOP)){
                           if (mediaPlayer.isPlaying()){
                               mediaPlayer.pause();
                           } else {
                               mediaPlayer.start();
                           }
                       }
                       break;

                   default:
                       break;
               }
           } else if (progress != -1){
               mediaPlayer.seekTo(progress);
           }
        }
    }

播放音乐的具体实现代码:
private void prepareMusic(final int position) {

    try {
        mediaPlayer.reset();
        mediaPlayer.setDataSource(musicDAO.find(position).getPath());
        mediaPlayer.prepare();
        mediaPlayer.start();
        mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer arg0) {
                Intent intent = new Intent(Utils.FINISH);
                intent.putExtra("finish", Utils.FINISH);
                sendBroadcast(intent);
                next();//如果当前歌曲播放完毕,自动播放下一首.
            }
        });
    } catch (IOException e) {
        e.printStackTrace();
    }
}

前一首:

private void pre(){
        if (position == 1){
            prepareMusic(musicDAO.getCount());
            position = musicDAO.getCount();
        } else {
            prepareMusic(--position);
        }
    }

下一首:

private void next(){
        if (position == musicDAO.getCount()){
            prepareMusic(1);

            position = 1;
        } else {
            prepareMusic(++ position);

        }
    }

系统消息提示服务(Notification Service)

这个逻辑就很简单了,接受来自服务、Listview的广播,设置Title:

BroadcastReceiver receiver = new BroadcastReceiver() {
        @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
        @Override
        public void onReceive(Context context, Intent intent) {
            if (musicDAO == null){
                musicDAO = new MusicDAO(context);
            }
            position = intent.getIntExtra("position", -1);
            if (position == 0){
                Log.e("推送", "无推送");
            } else {

                Intent intent1 = new Intent(getApplicationContext(), PlayActivity.class);
                intent1.putExtra("position", position);
                messageNotification = new Notification.Builder(context)
                        .setContentTitle("New mail from MediaPlayer")
                        .setContentText(musicDAO.find(position).getTitle() + "开始播放啦")
                        .setSmallIcon(R.mipmap.music)
                        .setAutoCancel(true)
                        .setDefaults(Notification.DEFAULT_SOUND)
                        .setContentIntent(PendingIntent.getActivity(context,0, intent1, FLAG_UPDATE_CURRENT))
                        .build();

                // 通知栏消息
                int messageNotificationID = 1;
                messageNotificationManager.notify(messageNotificationID, messageNotification);
            }
        }
    };

点击系统提示,回到播放界面:

        Intent messageIntent = new Intent(getApplicationContext(), PlayActivity.class);
        messagePendingIntent = PendingIntent.getActivity(this, 0, messageIntent, PendingIntent.FLAG_CANCEL_CURRENT);

整体优化

数据库部分

我们可以将写入的耗时操作放在异步任务中执行,给用户一个启动提示,减轻ListView的onCreate负担,同时也加快App的启动速度:

我们将写入数据库的方法封装起来:

 public List getBaseData() {

        musicList = new ArrayList<>();
        musicDAO = new MusicDAO(context);

        //先确定搜索对象是外部存储卡的音乐文件
        Cursor cursor = context.getContentResolver()
                .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                        null,
                        null,
                        null,
                        MediaStore.Audio.Media.DEFAULT_SORT_ORDER);


        assert cursor != null;

        //对搜索到的音乐文件信息分类
        int titleIndex = cursor.getColumnIndex(MediaStore.Audio.Media.TITLE);
        int artistIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST);
        int pathIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DATA);
        int durationIndex = cursor.getColumnIndex(MediaStore.Audio.Media.DURATION);
        int imagePathIndex = cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID);

        while (cursor.moveToNext()) {

            String Path = cursor.getString(pathIndex);
            String Title = cursor.getString(titleIndex);
            String Artist = cursor.getString(artistIndex);
            long Duration = cursor.getLong(durationIndex);
            String imagePath = cursor.getString(imagePathIndex);
            MusicItem song = new MusicItem(Title, Artist, Path, Duration, imagePath);

            musicDAO.add(song);
            musicList.add(song);

        }
        cursor.close();
        return musicList;
    }

使用异步任务去完成它:

 @Override
    protected Void doInBackground(Void...voids) {

        musicList = getBaseData();
        return null;
    }

在完成它之前,给用户展示一个请等待的提示进度条:

		progressDialog = new ProgressDialog(context);
        progressDialog.show();
数据库数据清理部分

在使用App写入数据前,我们可以将数据库信息清空一遍,避险重复打开,重复添加数据:
在写入数据之前,添加一句:


        musicDAO.delete();
        musicList = new ArrayList<>();
        musicDAO = new MusicDAO(context);
        ... .... .... ...

数据库数据实时更新

每次列表界面从后台启动,我们可以刷新一遍列表,防止歌曲文件的丢失或者加入引发异常:

刷新列表的方法:

 public void changeList(){
        musicDAO.delete();
        musicList = myAsyncTask.getBaseData();
        adapter = new MusicAdapter(this, musicList);
        listView.setAdapter(adapter);

        if (nowposition >= 0 && nowposition <= musicList.size() + 1){
            adapter.updateRed(nowposition - 1, listView);
        }
    }

Activity从后台重获焦点的生命周期在onRestart()中,所以我们将上述方法写在onRestart()中:

   @Override
    protected void onRestart() {
        super.onRestart();
        changeList();
    }
音乐列表标记

我们在正在播放的音乐Tittle TextView中加入"正在播放…"字样,通过ViewHolder获取TextView,调用我们的更新TextView方法,刷新一遍列表:

public void updateRed(int index, ListView listview){

        int visibleFirstPosition = listview.getFirstVisiblePosition();
        int visibleLastPosition = listview.getLastVisiblePosition();
        if (index >= visibleFirstPosition && index <= visibleLastPosition){

            View view = listview.getChildAt(index - visibleFirstPosition);
            ViewHolder holder = (ViewHolder) view.getTag();
            String str = holder.title.getText().toString();
            holder.title.setText("正在播放..." + str);
            musicItem.get(index).setTitle("正在播放..." + str);

        } else {

            String str = musicItem.get(index).getTitle();
            musicItem.get(index).setTitle("正在播放..." + str);
        }
    }

这其中要注意,getChildAt()只可以获取视图可见部分的position,需要通过计算得到具体的位置信息

Github:SimpleMediaPlayerDemo 下载

你可能感兴趣的:(Android,学习)