我的最终效果图:
本项目效果图:
//音乐的名字(周杰伦 - 最长的电影.mp3),截取后缀获得.mp3的文件,subString("-")截取歌曲的名字
private String name;
//音乐文件的作者
private String artist;
//音乐文件的路径
private String url;
//音乐播放的时间
private int time;
实现步骤:
①编写静态方法getMusicData(),返回值就是查询到的音乐数据集合;
②在该方法中创建ContentRecolver实例,通过上下文的getContentResolver()方法;
③判断获取到的ContentResolver是否为空,如果不为空,调用contentResolver.query (Uri uri, String[] projection,String selection,String[] selectionArgs, String sortOrder)方法查询本地的音乐文件,返回一个Cursor对象。如果Cursor为空,说明没有数据,直接返回null;
④如果有数据,利用Cursor对象的moveToFirst()查询第一条数据,如果不为空,就循环调用moveToNext()方法查询下一条数据直到没有数据为止;
⑤在查询中,通过cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.××))获取到相应的数据,该数据就对应等于Music中的成员变量;’
⑥将查询到的数据赋值到Music对象,完成查询数据的存储,并把该Music对象添加到Music集合,这样就获取到了本地音乐数据集合。
对上面的补充说明(对ContentResolver和ContentProvider了解的可直接跳过)
ContentProvider在android中的作用是对外共享数据,也就是说你可以通过 ContentProvider把应用中的数据共享给其他应用访问,其他应用可以通过ContentProvider对你应用中的数据进行添删改查。
ContentResolver:提供访问ContentProvider的能力,可以使用ContentResolver读取和操作其他应用程序已经通过ContentProvider暴露出来的数据。
/**
* 从内部存储中读取下载好的后缀名为.mp3的音乐文件,返回值为Music集合
*/
public class MusicList {
public static ArrayList getMusicData(Context context){
ArrayList musicList = new ArrayList();
ContentResolver contentResolver = context.getContentResolver();
if(contentResolver != null){
Cursor cursor = contentResolver.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null, null,
MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
if(cursor == null){
return null;
}
if(cursor.moveToFirst()) {
do {
Music music = new Music();
String artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST));
String name = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME));
if ("".equals(artist)) {
String[] split = name.split("-");
artist = split[0];
}
int time = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));
String url = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA));
String isMp3 = name.substring(name.length() - 3, name.length());
if (isMp3.equals("mp3")) {
music.setArtist(artist);
music.setTime(time);
music.setUrl(url);
music.setName(name.substring(0,name.length()-4));
musicList.add(music);
}
} while (cursor.moveToNext());
}
}
return musicList;
}
}
实现步骤:
//自定义方法requestPermission()
private void requestPermission() {
//创建允许权限的集合
List permissionList = new ArrayList();
//如果该权限没同意,就加入到允许权限的集合
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.READ_EXTERNAL_STORAGE);
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
//如果允许权限不为空,说明有权限还没申请,就一起申请
if (!permissionList.isEmpty()) {
ActivityCompat.requestPermissions(this, permissionList.toArray(new String[permissionList.size()]), 1);
}
//权限申请完成之后,才去初始化视图
else {
//适配器的适配工作
initView();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0) {
for (int i = 0; i < grantResults.length; i++) {
int grantResult = grantResults[i];
if (grantResult == PackageManager.PERMISSION_DENIED) {
String s = permissions[i];
Toast.makeText(this, s + "权限被拒绝了", Toast.LENGTH_SHORT).show();
} else {
initView();
}
}
}
}
}
①编写主页的xml(activity_main.xml)
②在主页中获取到该控件,并给该控件设置适配器进行显示
public class MusicAdapter extends BaseAdapter {
private Context mContext;
private List mMusicList;
public MusicAdapter(Context context,List musicList){
mContext = context;
mMusicList = musicList;
}
@Override
public int getCount() {
return mMusicList.size();
}
@Override
public Object getItem(int position) {
return mMusicList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
Music music = mMusicList.get(position);
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(R.layout.music_item, parent,false);
viewHolder = new ViewHolder();
viewHolder.music_item_name = convertView.findViewById(R.id.music_item_name);
viewHolder.music_item_artist = convertView.findViewById(R.id.music_item_artist);
viewHolder.music_item_time = convertView.findViewById(R.id.music_item_time);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.music_item_name.setText(music.getName());
viewHolder.music_item_artist.setText(music.getArtist());
viewHolder.music_item_time.setText(formatTime(music.getTime()));
return convertView;
}
class ViewHolder {
TextView music_item_name;
TextView music_item_artist;
TextView music_item_time;
}
//将时间转换为××:××格式
private String formatTime(int time) {
int ms2s = (time / 1000);
int minute = ms2s / 60;
int second = ms2s % 60;
return String.format("%02d:%02d", minute, second);
}
}
public class MainActivity extends AppCompatActivity {
private ArrayList arrayList;
private MusicAdapter mMusicAdapter;
private ListView mListView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
requestPermission();
}
private void initView() {
arrayList = MusicList.getMusicData(this);
mListView = findViewById(R.id.listView_main);
mMusicAdapter = new MusicAdapter(this, arrayList);
mListView.setAdapter(mMusicAdapter);
//实现页面跳转,将当前点击的音乐条目传过去给音乐播放页面,这样它才知道播放哪条音乐
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
// Intent intent = new Intent(MainActivity.this,DetailActivity.class);
// Bundle bundle = new Bundle();
// bundle.putInt("position",position);
// intent.putExtras(bundle);
// startActivity(intent);
}
});
}
private void requestPermission() {
...
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
...
}
}
public class DetailActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_pre, btn_play, btn_next;
private TextView tv_cur_time, tv_total_time;
private ImageView btn_return;
private SeekBar seekBar;
private ArrayList mMusicList;
private int mPosition;
private MediaPlayer mMediaPlayer;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
initView();
initData();
}
private void initData() {
mMediaPlayer = new MediaPlayer();
//拿到播放音乐的位置
mPosition = getIntent().getIntExtra("position", -1);
//拿到音乐数据集合
mMusicList = MusicList.getMusicData(this);
}
private void initView() {
btn_pre = findViewById(R.id.btn_pre);
btn_play = findViewById(R.id.btn_play);
btn_next = findViewById(R.id.btn_next);
btn_return = findViewById(R.id.btn_return);
tv_cur_time = findViewById(R.id.tv_cur_time);
tv_total_time = findViewById(R.id.tv_total_time);
seekBar = findViewById(R.id.seekBar);
btn_pre.setOnClickListener(this);
btn_play.setOnClickListener(this);
btn_next.setOnClickListener(this);
btn_return.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_pre:
break;
case R.id.btn_play:
break;
case R.id.btn_next:
break;
case R.id.btn_return:
onBackPressed();
break;
}
}
}
public void playMusic(int position) {
try {
mMediaPlayer.reset();
mMediaPlayer.setDataSource(mMusicList.get(position).getUrl());
mMediaPlayer.prepare();
mMediaPlayer.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public void play() {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
btn_play.setText("播放");
} else {
mMediaPlayer.start();
btn_play.setText("暂停");
}
}
public void next(int offset) {
mPosition += offset;
mPosition = (mMusicList.size() + mPosition) % mMusicList.size();
playMusic(mPosition);
}
在按钮点击事件中调用,就可以播放,暂停,下一曲,上一曲。为了让界面一进入就能够播放音乐,可以在onCreate()方法中调用playMusic(mPosition),不过记得要在initData()之后,也就是获取到mPosition之后,mPosition就是在MainActivity中通过ListView的ItemClickListener将位置传过来。
别忘了DetailActivity要在AndroidManifest中注册!!
更新操作需要单独开一个线程执行,将工作线程需操作UI的消息传递到主线程,使得主线程可根据工作线程的需求更新UI。这是由于在Android开发中,为了UI操作是线程安全的,规定了只允许在主线程中更新UI。因此,在主线程中编写Handler。
实现步骤较简单,直接看代码。
消息应该在什么时候发送呢?
应该在播放音乐playMusic()中调用handler.sendEmptyMessage(0x01),也就是一播放音乐就开启调用。
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
if(msg.what == 0x01){
tv_cur_time.setText("00:00");
//这里的格式化时间在前面已有编写
tv_cur_time.setText(formatTime(mMediaPlayer.getCurrentPosition()));
tv_total_time.setText(formatTime(mMusicList.get(mPosition).getTime()));
seekBar.setProgress(mMediaPlayer.getCurrentPosition());
seekBar.setMax(mMusicList.get(mPosition).getTime());
handler.sendEmptyMessage(0x01);
}
}
};
需要用到seekbar的setOnSeekBarChangeListener()方法
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
isSeekBarChanging = true;
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
isSeekBarChanging = false;
mMediaPlayer.seekTo(seekBar.getProgress());
}
});
添加到handleMessage()方法中即可。
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
next(1);
Toast.makeText(DetailActivity.this, "自动为您切换下一首:"+mMusicList.get(mPosition).getName(), Toast.LENGTH_SHORT).show();
}
});
同样,将其添加到handleMessage()方法中。到此,播放功能已全部实现。
==不要着急,我们试着退出播放音乐的页面,再重新选择歌曲播放,会发现可以同时播放多条音乐,且他们之间互不影响,为什么呢?
这是因为我们的MediaPlayer每次使用完后都没有释放资源,每点击一个音乐条目进来就重新创建了一个MediaPlayer。那么如何做呢?
思路:根据两次传进来的position的异同,决定是否释放上次的MediaPlayer资源
创建一个静态的MediaPlayer,保存上次的MediaPlayer
创建一个静态的position,保存上次的position
每次播放音乐的时候,如果第一次传进来的position和第二次传进来的position不一样,说明不是同一个音乐文件,那么MediaPlayer就要进行一系列操作。首先创建一个新的MediaPlayer,把上一次的MediaPlayer资源释放掉,将本次的MediaPlayer赋值为上一次的MediaPlayer,将本次的position赋值为上次的position。
每次播放音乐的时候,如果第一次传进来的position和第二次传进来的position一样,说明是同一个音乐文件,那么MediaPlayer就不需要做任何操作,直接将上次的MediaPlayer赋值给本次的MediaPlayer,上次的position赋值给本次的position,这样就不用去创建新的MediaPlayer,直接拿到上次的position。
这里有一点需要注意,现在MediaPlayer的创建时放在了playMusic()方法中,因为每次播放音乐都要判断是否需要创建新的MediaPlayer,而且用handler处理message也加了个判断,具体更改见代码:
public class DetailActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_pre, btn_play, btn_next;
private TextView tv_cur_time, tv_total_time;
private ImageView btn_return;
private SeekBar seekBar;
private ArrayList mMusicList;
private int mPosition;
static int savePosition;
private MediaPlayer mMediaPlayer;
static MediaPlayer mPreMediaPlayer;
private boolean isSeekBarChanging;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 0x01 && mPosition == savePosition) {
tv_cur_time.setText("00:00");
tv_cur_time.setText(formatTime(mMediaPlayer.getCurrentPosition()));
tv_total_time.setText(formatTime(mMusicList.get(mPosition).getTime()));
seekBar.setProgress(mMediaPlayer.getCurrentPosition());
seekBar.setMax(mMusicList.get(mPosition).getTime());
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
isSeekBarChanging = true;
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
isSeekBarChanging = false;
mMediaPlayer.seekTo(seekBar.getProgress());
}
});
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
next(1);
Toast.makeText(DetailActivity.this, "自动为您切换下一首:" + mMusicList.get(mPosition).getName(), Toast.LENGTH_SHORT).show();
}
});
handler.sendEmptyMessage(0x01);
}
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
initView();
initData();
if (mPreMediaPlayer == null || mPosition != savePosition) {
playMusic(mPosition);
} else {
mMediaPlayer = mPreMediaPlayer;
mPosition = savePosition;
handler.sendEmptyMessage(0x01);
}
// playMusic(mPosition);
}
private void initData() {
//拿到播放音乐的位置
mPosition = getIntent().getIntExtra("position", -1);
//拿到音乐数据集合
mMusicList = MusicList.getMusicData(this);
}
private void initView() {
btn_pre = findViewById(R.id.btn_pre);
btn_play = findViewById(R.id.btn_play);
btn_next = findViewById(R.id.btn_next);
btn_return = findViewById(R.id.btn_return);
tv_cur_time = findViewById(R.id.tv_cur_time);
tv_total_time = findViewById(R.id.tv_total_time);
seekBar = findViewById(R.id.seekBar);
btn_pre.setOnClickListener(this);
btn_play.setOnClickListener(this);
btn_next.setOnClickListener(this);
btn_return.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_pre:
next(-1);
break;
case R.id.btn_play:
play();
break;
case R.id.btn_next:
next(1);
break;
case R.id.btn_return:
onBackPressed();
break;
}
}
public void playMusic(int position) {
mMediaPlayer = new MediaPlayer();
if (mPreMediaPlayer != null) {
mPreMediaPlayer.stop();
mPreMediaPlayer.release();
}
mPreMediaPlayer = mMediaPlayer;
savePosition = mPosition;
try {
mMediaPlayer.reset();
mMediaPlayer.setDataSource(mMusicList.get(position).getUrl());
mMediaPlayer.prepare();
mMediaPlayer.start();
} catch (IOException e) {
e.printStackTrace();
}
handler.sendEmptyMessage(0x01);
}
public void play() {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
btn_play.setText("播放");
} else {
mMediaPlayer.start();
btn_play.setText("暂停");
}
}
public void next(int offset) {
mPosition += offset;
mPosition = (mMusicList.size() + mPosition) % mMusicList.size();
playMusic(mPosition);
}
private String formatTime(int time) {
int ms2s = (time / 1000);
int minute = ms2s / 60;
int second = ms2s % 60;
return String.format("%02d:%02d", minute, second);
}
}
到此为止,前三个功能已经全部实现,下面开始实现 在Service中播放本地音乐。
大体思路:
①创建自定义Service类继承自Service,因为需要用到绑定服务,所以要自定义MusicBinder类继承自Binder,然后在onBind()方法中返回我们自定义的MusicBinder类的实例,该实例定义了Activity可以与Service交互的程序接口;
②在MusicBinder类中开始编写一系列操作音乐播放的代码,也就是把原来Activity中与MediaPlayer播放音乐有关的方法和与MediaPlayer操作有关的变量全部移植过来;
③在Activity中开启服务并绑定服务,这样就能访问到Service中程序接口定义的方法;
④如果Activity中需要用到MediaPlayer的方法接口中却没有,那么就在接口中添加自定义方法然后通过调用该接口即可。
⑤记得要在不使用Service的地方解绑该服务。
具体细节看更改后的代码:
public class MusicService extends Service {
private int mPosition;
//静态存储上次音乐条目的位置
static int savePosition;
private MediaPlayer mMediaPlayer;
//静态存储上次音乐条目的MediaPlayer
static MediaPlayer mPreMediaPlayer;
private ArrayList mMusicList;
//静态存储当前音乐是否在播放
static boolean isPlaying;
public MusicService() {
}
//onCreate()只被执行一次,因此用来做初始化
@Override
public void onCreate() {
super.onCreate();
mMusicList = MusicList.getMusicData(this);
}
class MusicBinder extends Binder {
//创建一个绑定服务时,必须提供一个客户端与Service交互的IBinder,有三种方法
//我使用的方法:返回当前Service实例,它具有一些客户端可以公开调用的公开方法
public MusicService getService() {
return MusicService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return new MusicBinder();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
mPosition = intent.getExtras().getInt("position", -1);
if (mPreMediaPlayer == null || mPosition != savePosition) {
playMusic(mPosition);
} else {
mMediaPlayer = mPreMediaPlayer;
mPosition = savePosition;
}
return super.onStartCommand(intent, flags, startId);
}
/**
* 播放音乐
*/
public void playMusic(int position) {
mMediaPlayer = new MediaPlayer();
if (mPreMediaPlayer != null) {
mPreMediaPlayer.stop();
mPreMediaPlayer.release();
}
mPreMediaPlayer = mMediaPlayer;
savePosition = position;
try {
mMediaPlayer.reset();
mMediaPlayer.setDataSource(mMusicList.get(position).getUrl());
mMediaPlayer.prepare();
mMediaPlayer.start();
} catch (IOException e) {
e.printStackTrace();
}
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mPosition += 1;
mPosition = (mMusicList.size() + mPosition) % mMusicList.size();
playMusic(mPosition);
Toast.makeText(getApplicationContext(), "自动为您切换下一首:" + mMusicList.get(mPosition).getName(), Toast.LENGTH_SHORT).show();
}
});
}
/**
* 按钮点击:播放音乐
*/
public void play() {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
isPlaying = false;
} else {
mMediaPlayer.start();
isPlaying = true;
}
}
/**
* 按钮点击:下一首
*/
public void next(int offset) {
mPosition += offset;
mPosition = (mMusicList.size() + mPosition) % mMusicList.size();
playMusic(mPosition);
}
/**
* 获取当前音乐的名字
*/
public String getName() {
return mMusicList.get(mPosition).getName();
}
/**
* 获取当前音乐的播放时间
*/
public int getTime() {
return mMusicList.get(mPosition).getTime();
}
/**
* 获取当前播放位置
*/
public int getCurrent() {
return mMediaPlayer.getCurrentPosition();
}
/**
* 设置音乐播放的进度
*/
public void seekTo(int progress) {
mMediaPlayer.seekTo(progress);
}
}
public class DetailActivity extends AppCompatActivity implements View.OnClickListener {
private Button btn_pre, btn_play, btn_next;
private TextView tv_cur_time, tv_total_time;
private SeekBar seekBar;
private ImageView btn_return;
//seekBar是否被拖动
private boolean isSeekBarChanging;
private MusicService musicService;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 0x01) {
tv_cur_time.setText("00:00");
tv_cur_time.setText(formatTime(musicService.getCurrent()));
tv_total_time.setText(formatTime(musicService.getTime()));
seekBar.setProgress(musicService.getCurrent());
seekBar.setMax(musicService.getTime());
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
isSeekBarChanging = true;
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
isSeekBarChanging = false;
musicService.seekTo(seekBar.getProgress());
}
});
handler.sendEmptyMessage(0x01);
}
}
};
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(conn);
handler.removeCallbacksAndMessages(null);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
initView();
Intent intent = new Intent(this, MusicService.class);
//获取MainActivity传过来的数据
Bundle bundle = getIntent().getExtras();
//再把这些包装好的数据重新装入Intent,发送给Service
intent.putExtras(bundle);
startService(intent);
bindService(intent, conn, BIND_AUTO_CREATE);
}
ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
musicService = ((MusicService.MusicBinder) service).getService();
handler.sendEmptyMessage(0x01);
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
/**
* 按钮点击事件
*/
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_pre:
musicService.next(-1);
break;
case R.id.btn_play:
musicService.play();
break;
case R.id.btn_next:
musicService.next(1);
break;
case R.id.btn_return:
onBackPressed();
break;
}
updateBtnPlayOrPause();
}
private void updateBtnPlayOrPause() {
if (MusicService.isPlaying) {
btn_play.setText("暂停");
} else {
btn_play.setText("播放");
}
}
private void initView() {
...
}
private String formatTime(int time) {
...
}
}
这里别忘了如果service不是通过Android Studio自动生成的,记得在AndroidManifest中声明。
实现步骤:
①创建播放/暂停、上一曲、下一曲的动态广播;
②在按钮点击事件中发送对应的广播;
③编写广播接收器,对广播进行过滤。拿到想要的广播后,根据广播的类型进行操作;
④在服务开启的时候就开启广播接收器。
在DetailActivity中创建动态广播
/**
* 按钮:播放音乐的广播
*/
public void playMusic(){
Intent intent = new Intent();
intent.setAction(MusicService.ACTION);
Bundle bundle = new Bundle();
bundle.putInt(MusicService.BTN_STATE,MusicService.PLAY_STATE);
intent.putExtras(bundle);
sendBroadcast(intent);
}
/**
* 按钮:下一首音乐的广播
*/
public void nextMusic(){
Intent intent = new Intent();
intent.setAction(MusicService.ACTION);
Bundle bundle = new Bundle();
bundle.putInt(MusicService.BTN_STATE,MusicService.NEXT_MUSIC_STATE);
intent.putExtras(bundle);
sendBroadcast(intent);
}
/**
* 按钮:上一首音乐的广播
*/
public void preMusic(){
Intent intent = new Intent();
intent.setAction(MusicService.ACTION);
Bundle bundle = new Bundle();
bundle.putInt(MusicService.BTN_STATE,MusicService.PRE_MUSIC_STATE);
intent.putExtras(bundle);
sendBroadcast(intent);
}
在MusicService中编写广播接收器
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(ACTION)) {
switch (intent.getIntExtra(BTN_STATE, -1)) {
case PLAY_STATE:
play();
break;
case PRE_MUSIC_STATE:
next(-1);
break;
case NEXT_MUSIC_STATE:
next(1);
break;
default:
Toast.makeText(context, "系统出错,请稍后重试!", Toast.LENGTH_SHORT).show();
break;
}
}
}
};
在MusicService刚刚启动的时候(onCreate())就注册了一个广播,这样他就能够接收其他页面点击了上一曲、下一曲、暂停/播放按钮发出的广播。因此,其他页面中点击上一曲、下一曲、暂停、播放按钮时都需要向MusicService发送广播通知,让它来更新音乐。
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION);
registerReceiver(receiver,intentFilter);
同时我们还要更改一下之前的代码,在按钮点击事件中,之前我们是通过服务的方法去响应事件,我们都将其改为用广播的方式。也就是把musicService.next(-1)改为nextMusic(),musicService.play()改为playMusic()、musicService.next(1)改为nextMusic()。
这里用到的常量我也贴出代码来:
//动态广播的Action
public static String ACTION = "action";
//按钮点击状态标识符
public static String BTN_STATE = "btn_state";
public static final int PLAY_STATE = 0, NEXT_MUSIC_STATE = 1, PRE_MUSIC_STATE = 2;
功能剖析:
首先需要先有Notification,所以在开启服务之后也就是onStartCommand()方法中,就先创建出Notification并进行一系列的初始化操作。
播放页面中按钮一点击就发送广播,广播接收器接收到广播,对音乐状态进行控制。并且播放页面中按钮的点击能够更改Notification中播放/暂停按钮显示的图片和当前播放的状态。
Notification中的按钮一点击就发送广播,广播接收器接收到广播,对音乐状态进行控制,并且Notification中按钮的点击能够更改播放页面中播放/暂停按钮显示的文字和当前播放的状态。
实现步骤:
①由于Notification布局本项目采用自定义,因此需要编写自定义的Notification布局RemoteViews;
②编写Notification初始化方法,这里需要在获取到position之后调用,因为RemoteViews需要根据position显示歌曲名字(这里我把Notification和Button的广播做成两个,方便大家逐块进行理解,所以代码比较累赘,可以考虑封装合并,读者有条件自行完成;)
③编写Notification的更新的方法并在每一次接收到按钮发出的广播的时候对Notification进行更新,这样按钮发出广播既实现了对音乐状态的控制,又实现了对Notification的更新;
④编写按钮的更新方法并在每一次接收到Notification发出的广播的时候对按钮进行更新,这样Notification发出的广播既实现了对音乐状态的控制,又实现了对按钮的更新。
public class DetailActivity extends AppCompatActivity implements View.OnClickListener {
private ImageView iv_pre, iv_next, iv_play, btn_return;
private TextView tv_cur_time, tv_total_time;
private SeekBar seekbar;
//seekBar是否被拖动
private boolean isSeekBarChanging;
private MusicService musicService;
//按钮点击状态标识符
public static String NOTIFICATION_ACTION = "notification_action";
public static String NOTIFICATION_BTN_STATE = "notification_btn_state";
public static final int NOTIFICATION_PLAY = 0, NOTIFICATION_NEXT_MUSIC = 1, NOTIFICATION_PRE_MUSIC = 2;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == 0x01) {
tv_cur_time.setText("00:00");
tv_cur_time.setText(formatTime(musicService.getCurrent()));
tv_total_time.setText(formatTime(musicService.getTime()));
seekbar.setMax(musicService.getTime());
seekbar.setProgress(musicService.getCurrent());
seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
isSeekBarChanging = true;
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
isSeekBarChanging = false;
musicService.seekTo(seekBar.getProgress());
}
});
handler.sendEmptyMessage(0x01);
}
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
initView();
initService();
initBroadcastReceiver();
}
private void initBroadcastReceiver() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(NOTIFICATION_ACTION);
registerReceiver(receiver,intentFilter);
}
private void initService() {
Intent intent = new Intent(this, MusicService.class);
//获取MainActivity传过来的数据
Bundle bundle = getIntent().getExtras();
//再把这些包装好的数据重新装入Intent,发送给Service
intent.putExtras(bundle);
startService(intent);
bindService(intent, conn, BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(conn);
handler.removeCallbacksAndMessages(null);
}
/**
* 广播接收器,接收来自Notification的广播
*/
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(NOTIFICATION_ACTION)) {
switch (intent.getIntExtra(NOTIFICATION_BTN_STATE, -1)) {
case NOTIFICATION_PRE_MUSIC:
musicService.next(-1);
break;
case NOTIFICATION_PLAY:
musicService.play();
break;
case NOTIFICATION_NEXT_MUSIC:
musicService.next(1);
break;
default:
Toast.makeText(context, "系统出错,请稍后重试!", Toast.LENGTH_SHORT).show();
break;
}
updateNotificationBtnPlayOrPause();
musicService.updateNotification();
}
}
};
private void updateNotificationBtnPlayOrPause() {
if (MusicService.isPlaying) {
iv_play.setImageResource(R.drawable.pause);
} else {
iv_play.setImageResource(R.drawable.play);
}
}
ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
musicService = ((MusicService.MusicBinder) service).getService();
handler.sendEmptyMessage(0x01);
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
/**
* 按钮点击事件
*/
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.iv_pre:
preMusic();
MusicService.isPlaying = false;
break;
case R.id.iv_play:
playMusic();
break;
case R.id.iv_next:
nextMusic();
MusicService.isPlaying = false;
break;
case R.id.btn_return:
onBackPressed();
break;
}
updateBtnPlayOrPause();
}
private void updateBtnPlayOrPause() {
if (MusicService.isPlaying) {
iv_play.setImageResource(R.drawable.play);
} else {
iv_play.setImageResource(R.drawable.pause);
}
}
/**
* 初始化视图
*/
private void initView() {
iv_pre = findViewById(R.id.iv_pre);
iv_play = findViewById(R.id.iv_play);
iv_next = findViewById(R.id.iv_next);
btn_return = findViewById(R.id.btn_return);
tv_cur_time = findViewById(R.id.tv_cur_time);
tv_total_time = findViewById(R.id.tv_total_time);
seekbar = findViewById(R.id.seekbar);
iv_pre.setOnClickListener(this);
iv_play.setOnClickListener(this);
iv_next.setOnClickListener(this);
btn_return.setOnClickListener(this);
}
/**
* 格式播放时间
*/
private String formatTime(int time) {
int miao = (time /= 1000);
int minute = miao / 60;
int second = miao % 60;
return String.format("%02d:%02d", minute, second);
}
/**
* 按钮:播放音乐的广播
*/
public void playMusic() {
Intent intent = new Intent();
intent.setAction(MusicService.ACTION);
Bundle bundle = new Bundle();
bundle.putInt(MusicService.BTN_STATE, MusicService.PLAY_STATE);
intent.putExtras(bundle);
sendBroadcast(intent);
}
/**
* 按钮:下一首音乐的广播
*/
public void nextMusic() {
Intent intent = new Intent();
intent.setAction(MusicService.ACTION);
Bundle bundle = new Bundle();
bundle.putInt(MusicService.BTN_STATE, MusicService.NEXT_MUSIC_STATE);
intent.putExtras(bundle);
sendBroadcast(intent);
}
/**
* 按钮:上一首音乐的广播
*/
public void preMusic() {
Intent intent = new Intent();
intent.setAction(MusicService.ACTION);
Bundle bundle = new Bundle();
bundle.putInt(MusicService.BTN_STATE, MusicService.PRE_MUSIC_STATE);
intent.putExtras(bundle);
sendBroadcast(intent);
}
}
public class MusicService extends Service {
private int mPosition;
//静态存储上次音乐条目的位置
static int savePosition;
private MediaPlayer mMediaPlayer;
//静态存储上次音乐条目的MediaPlayer
static MediaPlayer mPreMediaPlayer;
private ArrayList mMusicList;
static boolean isPlaying = true;
//动态广播的Action
public static String ACTION = "action";
//按钮点击状态标识符
public static String BTN_STATE = "btn_state";
public static final int PLAY_STATE = 0, NEXT_MUSIC_STATE = 1, PRE_MUSIC_STATE = 2;
private static final String CHANNEL_ID = "1";
private static final String CHANNEL_NAME = "MyChannel";
private static final int NOTIFICATION_ID = 2;
private RemoteViews remoteViews;
private Notification notification;
public MusicService() {
}
//onCreate()只被执行一次,因此用来做初始化
@Override
public void onCreate() {
super.onCreate();
mMusicList = MusicList.getMusicData(this);
//在MusicService刚刚启动的时候就注册了一个广播,为的是让它来接收到在其他页面点击了上一曲、下一曲、暂停/播放等按钮时,来做相应的处理
//因此,其他页面中点击上一曲、下一曲、暂停、播放按钮时都需要向MusicService发送广播通知,让它来更新音乐
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(ACTION);
registerReceiver(receiver, intentFilter);
}
class MusicBinder extends Binder {
public MusicService getService() {
return MusicService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return new MusicBinder();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
mPosition = intent.getExtras().getInt("position", -1);
initNotification();
if (mPreMediaPlayer == null || mPosition != savePosition) {
playMusic(mPosition);
} else {
mMediaPlayer = mPreMediaPlayer;
}
return super.onStartCommand(intent, flags, startId);
}
/**
* 初始化Notification(系统默认UI)
* 这里是谷歌官方使用步骤:
* A:要开始,您需要使用notificationCompat.builder对象设置通知的内容和通道。
* B: 在Android 8.0及更高版本上传递通知之前,必须通过将NotificationChannel实例传递给CreateNotificationChannel(),在系统中注册应用程序的通知通道。
* C: 每个通知都应该响应tap,通常是为了在应用程序中打开与通知对应的活动。为此,必须指定用PendingIntent对象定义的内容意图,并将其传递给setContentIntent()。
* D: 若要显示通知,请调用notificationManagerCompat.notify(),为通知传递唯一的ID以及notificationCompat.builder.build()的结果。
*/
private void initNotification() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);
}
Intent intent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
remoteViews = new RemoteViews(getPackageName(), R.layout.notification);
remoteViews.setTextViewText(R.id.tv_music_name, mMusicList.get(mPosition).getName());
remoteViews.setOnClickPendingIntent(R.id.iv_pre_music, getPendingIntent(this, DetailActivity.NOTIFICATION_PRE_MUSIC));
remoteViews.setOnClickPendingIntent(R.id.iv_play_pause, getPendingIntent(this, DetailActivity.NOTIFICATION_PLAY));
remoteViews.setOnClickPendingIntent(R.id.iv_next_music, getPendingIntent(this, DetailActivity.NOTIFICATION_NEXT_MUSIC));
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID);
builder.setSmallIcon(R.drawable.music)
.setContent(remoteViews)
.setOngoing(true)
.setContentIntent(pendingIntent);
notification = builder.build();
//两种方法都可以,这里使用兼容的NotificationManager
NotificationManagerCompat notificationManagerCompat = NotificationManagerCompat.from(this);
notificationManagerCompat.notify(NOTIFICATION_ID, notification);
// NotificationManager manager = (NotificationManager) getSystemService(Service.NOTIFICATION_SERVICE);
// manager.notify(NOTIFICATION_ID,builder.build());
}
private PendingIntent getPendingIntent(Context context, int state) {
Intent intent = new Intent();
intent.setAction(DetailActivity.NOTIFICATION_ACTION);
Bundle bundle = new Bundle();
bundle.putInt(DetailActivity.NOTIFICATION_BTN_STATE, state);
intent.putExtras(bundle);
PendingIntent pendingIntent = null;
switch (state) {
case DetailActivity.NOTIFICATION_PRE_MUSIC:
pendingIntent = PendingIntent.getBroadcast(context,1,intent,PendingIntent.FLAG_UPDATE_CURRENT);
break;
case DetailActivity.NOTIFICATION_PLAY:
pendingIntent = PendingIntent.getBroadcast(context,2,intent,PendingIntent.FLAG_UPDATE_CURRENT);
break;
case DetailActivity.NOTIFICATION_NEXT_MUSIC:
pendingIntent = PendingIntent.getBroadcast(context,3,intent,PendingIntent.FLAG_UPDATE_CURRENT);
break;
}
return pendingIntent;
}
/**
* 更新Notification
*/
public void updateNotification() {
remoteViews.setImageViewResource(R.id.iv_play_pause, isPlaying ? R.drawable.pause : R.drawable.play);
remoteViews.setTextViewText(R.id.tv_music_name, mMusicList.get(mPosition).getName());
NotificationManagerCompat manager = NotificationManagerCompat.from(this);
manager.notify(NOTIFICATION_ID, notification);
}
/**
* 广播接收器:接收playMusic()、nextMusic()、preMusic()的动态广播
*/
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(ACTION)) {
switch (intent.getIntExtra(BTN_STATE, -1)) {
case PLAY_STATE:
play();
break;
case PRE_MUSIC_STATE:
next(-1);
break;
case NEXT_MUSIC_STATE:
next(1);
break;
default:
Toast.makeText(context, "系统出错,请稍后重试!", Toast.LENGTH_SHORT).show();
break;
}
updateNotification();
}
}
};
/**
* 播放音乐
*/
public void playMusic(int position) {
mMediaPlayer = new MediaPlayer();
if (mPreMediaPlayer != null) {
mPreMediaPlayer.stop();
mPreMediaPlayer.release();
}
mPreMediaPlayer = mMediaPlayer;
savePosition = position;
try {
mMediaPlayer.reset();
mMediaPlayer.setDataSource(mMusicList.get(position).getUrl());
mMediaPlayer.prepare();
mMediaPlayer.start();
} catch (IOException e) {
e.printStackTrace();
}
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mPosition += 1;
mPosition = (mMusicList.size() + mPosition) % mMusicList.size();
playMusic(mPosition);
Toast.makeText(getApplicationContext(), "自动为您切换下一首:" + mMusicList.get(mPosition).getName(), Toast.LENGTH_SHORT).show();
updateNotification();
}
});
}
/**
* 按钮点击:播放音乐
*/
public void play() {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
isPlaying = false;
} else {
mMediaPlayer.start();
isPlaying = true;
}
}
/**
* 按钮点击:下一首
*/
public void next(int offset) {
mPosition += offset;
mPosition = (mMusicList.size() + mPosition) % mMusicList.size();
playMusic(mPosition);
isPlaying = true;
}
/**
* 设置音乐播放的进度
*/
public void seekTo(int progress) {
mMediaPlayer.seekTo(progress);
}
/**
* 获取当前音乐的名字
*/
public String getName() {
return mMusicList.get(mPosition).getName();
}
/**
* 获取当前音乐的播放时间
*/
public int getTime() {
return mMusicList.get(mPosition).getTime();
}
/**
* 获取当前播放位置
*/
public int getCurrent() {
return mMediaPlayer.getCurrentPosition();
}
}
这里有个巨坑,就是remoteViews.setOnClickPendingIntent的第二个参数传入的是PendingIntent,因为PendingIntent中需要传入一个Intent,我们就是通过这个Intent来发送按钮的广播。我尝试着用三个不同的Intnet,每一个PendingIntent包含一个Intent,这样三个PendingIntent就不一样了。原理确实是这样的,但是按照我们常规的,对PendingIntent的参数设置就是(this,0,intent,0),发现效果出不来。于是我百度了一些资料,基本都说是第四个参数的原因,于是我将第四个参数改为 PendingIntent.FLAG_UPDATE_CURRENT,发现还是不行。经过一系列的注释、Log,还是没发现问题。最后突然想到,PendingIendingInten的第二个参数我们还没用到,一直传入0是什么原因,于是我试着更改下三个PendingIntent用不同的reqestCode,结果就成了。第二个参数的翻译过来的意思就是请求码,请求码不一样系统才认为是不一样的PendingIntent。
有任何疑问欢迎留言提问。