音乐播放器第二版
简介
音乐播放器基于第一版所做的改进为增加了后台服务,也就是退出app后音乐在后台播放。
整体构思:因为要实现在后台播放,所以播放功能整体放入服务中。点击事件依然在主活动中实现,这样做的原因有两个:1、点击事件是发送在主界面中的活动,与服务无关。2、点击事件之后会发送UI更新,服务不能进行UI操作。
实现步骤:
- 新建MusicList类作为数据源(即把第一版中的加载数据源方法提取为一个类,目的是服务和活动都能获取此数据源,不需要靠intent进行传递。)
- 新建服务,把有关播放的代码放入服务中,新增播放逻辑(因为实现了后台播放,如果不新增播放逻辑的话,当你点击另一首歌曲时,原有歌曲不会停止,会继续播放,这也是初步写完第二版之后所遇到的bug)
- 在主活动中和服务进行通信,由点击事件对服务进行指挥。
新建MusicList类作为数据源
public class MusicList {
static LocalMusicBean musicBean;
@SuppressLint("NotifyDataSetChanged")
public static List<LocalMusicBean> getMusic(Context context){
List<LocalMusicBean> mData = new ArrayList<>();
//获取ContentResolver对象
String[] selectionArgs = new String[]{context.getString(R.string.music)};
String selection = MediaStore.Audio.Media.DATA + context.getString(R.string.like);
// 媒体库查询语句
@SuppressLint("Recycle") Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, selection, selectionArgs, MediaStore.Audio.AudioColumns.IS_MUSIC);
//遍历Cursor
int id = 0; //歌曲排序的序列号
while (cursor.moveToNext()) {
String song = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
String singer = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
String album = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM));
long duration = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION));
@SuppressLint("SimpleDateFormat") SimpleDateFormat dateFormat = new SimpleDateFormat(context.getString(R.string.timeformat));
String time = dateFormat.format(new Date(duration));
//将一行当中的数据封装到对象当中
if (!time.equals(context.getString(R.string.timeiszeero))) {
id++;
String sid = String.valueOf(id);
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));
musicBean = new LocalMusicBean(sid, song, singer, album, time, path);
mData.add(musicBean);
}
}
return mData;
}
}
注意点:
- 与第一版的加载数据源方法整体代码一致,但是这里需要注意的是:适配器的更新不放在此类中,适配器的更新是发送在主活动中事项,MusicList类只作为数据源类,即搜索本地歌曲并且添加到集合中。
新建服务
public class MusicService extends Service {
//记录当前正在播放的音乐的位置
private int currentPosition = -1;
//记录上次播放音乐的位置
private int lastPosition;
//记录当前播放的音乐播放到哪了
private int currentPositionInMusic = 0;
//音乐实例
private LocalMusicBean musicBean;
//数据源
private List<LocalMusicBean> mData;
//当前播放的音乐
MediaPlayer currentMediaPlayer;
//上次播放的音乐
MediaPlayer lastMediaPlayer;
Bundle bundle;
public MusicService() {
}
@Override
public void onCreate() {
super.onCreate();
mData = MusicList.getMusic(this);
}
class MusicBinder extends Binder{
public MusicService getService(){
return MusicService.this;
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//获取点击的音乐实例
bundle = intent.getExtras();
currentPosition = bundle.getInt(getString(R.string.key));
musicBean = mData.get(currentPosition);
if (lastPosition != currentPosition){
playMusicInMusicBean(musicBean);
lastPosition = currentPosition;
}
else
{
currentMediaPlayer = lastMediaPlayer;
currentPosition = lastPosition;
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public IBinder onBind(Intent intent) {
return new MusicBinder();
}
//根据歌曲来进行播放
public void playMusicInMusicBean(LocalMusicBean musicBean) {
//如果当前有音乐播放,停止播放
if (lastMediaPlayer != null){
lastMediaPlayer.stop();
lastMediaPlayer.release();
}
currentMediaPlayer = new MediaPlayer();
lastMediaPlayer = currentMediaPlayer;
try {
currentPositionInMusic = 0;
currentMediaPlayer.setDataSource(musicBean.getPath());
playMusic();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
//播放音乐
void playMusic(){
if (currentPositionInMusic == 0){
//从头开始播放
if (currentMediaPlayer != null && !currentMediaPlayer.isPlaying()){
try {
currentMediaPlayer.prepare();
} catch (IOException e) {
e.printStackTrace();
}
currentMediaPlayer.start();
}
}else{
//从暂停处开始播放
currentMediaPlayer.seekTo(currentPositionInMusic);
currentMediaPlayer.start();
}
}
//暂停播放音乐
void pauseMusic() {
if (currentMediaPlayer != null && currentMediaPlayer.isPlaying()){
currentPositionInMusic = currentMediaPlayer.getCurrentPosition();
currentMediaPlayer.pause();
}
}
//上一首或者下一首
void lastOrNext(int position){
currentPosition = position;
currentPositionInMusic = 0;
musicBean = mData.get(currentPosition);
playMusicInMusicBean(musicBean);
}
}
新增逻辑:
创建一个记录上次播放的音乐的位置的变量,创建一个上次播放对象。
在播放前判断本次要播放的音乐和正在播放的音乐是否为同一个,不是就播放要播放的音乐,是不进行操作。
播放时要判断当前是否有音乐在播放,有则需停止,这里需要注意的是此时还在播放的音乐术语lastMediaPlayer对象播放的,所以停止操作要针对lastMediaPlayer对象。同时更新lastMediaPlayer对象。
上一首下一首的代码合并,由活动给出位置信息,是加还是减,然后服务更新位置信息并进行播放,代码变得简洁且重复性代码减少。
在主活动中和服务进行通信
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_test);
initView();
mData = MusicList.getMusic(this);
//设置适配器
musicAdapter = new LocalMusicAdapter(this, mData);
//设置适配器
musicRv.setAdapter(musicAdapter);
//设置布局管理器
LinearLayoutManager layoutManager = new LinearLayoutManager(this, RecyclerView.VERTICAL, false);
musicRv.setLayoutManager(layoutManager);
//加载本地数据源
musicAdapter.notifyDataSetChanged();
//设置每一项的点击事件
setEventListener();
}
private final ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
musicService = ((MusicService.MusicBinder) service).getService();
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
//设置recyclerView的item点击事件
private void setEventListener() {
musicAdapter.setOnItemClickListener((view, position) -> {
currentPosition = position;
Intent intent = new Intent(MainActivity.this, MusicService.class);
Bundle bundle = new Bundle();
bundle.putInt(getString(R.string.key),currentPosition);
intent.putExtras(bundle);
startService(intent);
bindService(intent, conn, BIND_AUTO_CREATE);
initCurrentView();
playIv.setImageResource(R.mipmap.icon_pause);
});
}
private void initCurrentView(){
musicBean = mData.get(currentPosition);
songTv.setText(musicBean.getSong());
singerTv.setText(musicBean.getSinger());
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(conn);
}
代码逻辑:
数据源由MusicList类提供。这里需要注意的是,活动需要传入上下文给MusicList类,因为该类需要搜索手机本地的所有歌曲。
增加一个ServiceConnection内部类,在此类中获取Service的实例。
在recyclerView的item的点击事件中对服务进行绑定和启动,原因:服务是用来实现音乐后台播放功能的,只有当recyclerView的item被点击后,播放音乐的功能才正式启动,此时启动服务是最好的。
在启动服务之前需要向intent传入位置信息,Service中取出。这里通过bundle放入,Service中也通过bundle取出。试验过putIntEctrat,失败,debug发现无论点击何处,传入和取出的位置都为-1,失败后进行学习和试验发现bundle可以实现目的。
服务的解绑在onDestroy中进行。
public void onClick(View view) {
switch (view.getId()){
case R.id.iv_current_last:
if (currentPosition == -1){
//没有选中要播放的音乐
Toast.makeText(this,"请选择要播放的音乐",Toast.LENGTH_LONG).show();
return;
}
if (currentPosition == 0){
Toast.makeText(this,"已经是第一首了 ",Toast.LENGTH_LONG).show();
}
else {
currentPosition--;
musicService.lastOrNext(currentPosition);
}
break;
case R.id.iv_current_play:
if (currentPosition == -1){
//没有选中要播放的音乐
Toast.makeText(this,"请选择要播放的音乐",Toast.LENGTH_LONG).show();
return;
}
if (musicService.currentMediaPlayer.isPlaying()){
//处于播放状态,需要暂停音乐
musicService.pauseMusic();
playIv.setImageResource(R.mipmap.icon_play);
}else {
musicService.playMusic();
playIv.setImageResource(R.mipmap.icon_pause);
}
break;
case R.id.iv_current_next:
if (currentPosition == -1){
Toast.makeText(this,"请选择要播放的音乐 ",Toast.LENGTH_LONG).show();
return;
}else if (currentPosition == mData.size()-1){
Toast.makeText(this,"已经是最后一首了",Toast.LENGTH_LONG).show();
}else {
currentPosition++;
musicService.lastOrNext(currentPosition);
}
break;
default:
break;
}
}
按钮的点击事件的响应由Service中的方法完成。但是UI更新在主活动中进行。
音乐播放器第一版文章链接:https://blog.csdn.net/qq_45779316/article/details/119802909?spm=1001.2014.3001.5501
完整代码:https://gitee.com/luozhaosong/new-music-app