📱 Android | 简单播放器的实现

这次在 Android 上实现一个可以选歌以及后台播放的音乐播放器,主要涉及到的技术有 Android 中的 Service 以及RxJava

实现内容

实现一个简单的播放器,要求功能有:

  1. 播放、暂停、停止、退出功能,按停止键会重置封面转角,进度条和播放按钮;按退出键将停止播放并退出程序
  2. 后台播放功能,按手机的返回键和 home 键都不会停止播放,而是转入后台进行播放
  3. 进度条显示播放进度、拖动进度条改变进度功能
  4. 播放时图片旋转,显示当前播放时间功能,圆形图片的实现使用的是一个开源控件 CircleImageView

附加内容(加分项,加分项每项占 10 分)

1.选歌

用户可以点击选歌按钮自己选择歌曲进行播放,要求换歌后不仅能正常实现上述的全部功能,还要求选歌成功后不自动播放,重置播放按钮,重置进度条,重置歌曲封面转动角度,最重要的一点:需要解析 mp3 文件,并更新封面图片。

应用截图

Screenshot_20181124-225927_MusicPlayer应用主界面 Screenshot_20181124-225952_MusicPlayer开始播放
Screenshot_20181124-234722_MusicPlayer暂停播放 Screenshot_20181124-225927_MusicPlayer暂停播放
![Screenshot_20181125-004302_Sound picker](Android_Multimedia/Screenshot_20181125-004302_Sound picker.jpg) 选歌 Screenshot_20181124-230235_MusicPlayer选歌成功

界面

界面主要是采用约束布局,使用约束布局,很容易就可以设计出一个可以适应各种屏幕大小的界面。

选择音乐

和之前的选择图片差不多,不过这次的选择音乐需要文件读取权限。

使用ACTION_PICK的 Intent,并且设置好需要的内容类型,系统就会弹出指定类型媒体的选择器。

1
2
3
4
5
6
7
8
9
public void onClickSelect(View v) {
if (ContextCompat.checkSelfPermission(activity,Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { //表示未授权时
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
return;
}
Intent intent = new Intent(Intent.ACTION_PICK, null);
intent.setData(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
activity.startActivityForResult(intent, PICK_AUDIO);
}

选择完之后,我们得到的是一个内容访问器的 URI

1
2
3
4
5
6
7
8
void onAudioResult(Intent data) {
if (data != null) {
Uri uri = data.getData();
initView();
iService.openMusicUri(uri);
updateInfo(iService.getMusicInfo());
}
}

然后就需要处理这个 URI,读取出相关信息(文件路径、标题、歌手)等等

1
2
3
4
5
6
7
8
9
10
11
12
13
public MusicInfo openMusicUri(Uri uri) {
Cursor musicCursor = MusicService.this.getContentResolver().query(uri, null, null, null, null);
Objects.requireNonNull(musicCursor).moveToFirst();
MusicService.this.musicInfo = new MusicInfo();
try {
musicInfo.path = musicCursor.getString(musicCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));
musicInfo.title = musicCursor.getString(musicCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
musicInfo.artist = musicCursor.getString(musicCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
String albumId = musicCursor.getString(musicCursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID));
...
musicCursor.close();
return musicInfo;
}

这里是通过ContentResolver使用 URI 读取,还读取出他的专辑 ID,有了这个专辑 ID,就可以再次使用ContentResolver来查询专辑图片的路径,URI 为MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,然后加上 ID 的过滤条件,就可以得到专辑图片路径。

1
2
3
4
5
6
7
8
9
10
11
Cursor albumCursor = getContentResolver().query(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI,
new String[] {MediaStore.Audio.Albums._ID, MediaStore.Audio.Albums.ALBUM_ART},
MediaStore.Audio.Albums._ID+ "=?",
new String[] {String.valueOf(albumId)},
null);
if (Objects.requireNonNull(albumCursor).moveToFirst()) {
String path = albumCursor.getString(albumCursor.getColumnIndex(MediaStore.Audio.Albums.ALBUM_ART));
if (path != null) {
musicInfo.image = BitmapFactory.decodeFile(path);
}
}

获取完数据之后,就可以通过路径把数据设置到MediaPlayer里面

1
2
3
4
5
6
7
try {
mediaPlayer.reset();
mediaPlayer.setDataSource(musicInfo.path);
mediaPlayer.prepare();
} catch (IOException e) {
e.printStackTrace();
}

Service

既然需要后台播放,自然就需要用到 Service

首先写一个MusicService,使用IBinder在服务与Activity之间的通讯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MusicService extends Service{
MediaPlayer mediaPlayer;
MusicInfo musicInfo;

@Nullable
@Override
public IBinder onBind(Intent intent) {
return new MusicBinder();
}
public class MusicBinder extends Binder implements IService {
public MediaPlayer getMediaPlayer() {return mediaPlayer;}
public MusicInfo getMusicInfo() {return musicInfo; }
}
}

然后在ActivityOnCreate的时候,使用ServiceConnectionService连接起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 开启服务
Intent intent = new Intent(activity, MusicService.class);
activity.startService(intent);
musicSC = new MusicServiceConnection();
activity.bindService(intent, musicSC, Context.BIND_AUTO_CREATE);


private class MusicServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
iService = service;
}

@Override
public void onServiceDisconnected(ComponentName name) {

}
}

这样,我们就得到了一个IBinder对象,通过transact方法可以调用服务中的各种事件

首先定义好各种事件的参数

1
2
3
4
5
6
7
8
9
10
final static int SET_MUSIC_PATH = 1;
final static int PLAY_MUSIC = 2;
final static int STOP_MUSIC = 3;
final static int PAUSE_MUSIC = 4;
final static int GET_CURRENT_POS = 5;
final static int GET_TOTAL_DUR = 6;
final static int SET_SEEK_TO = 7;
final static int SET_MUSIC_URL = 8;
final static int GET_MUSIC_URL = 9;
final static int IS_PLAYING = 10;

然后再BinderonTransact函数里面响应各种事件的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class MusicBinder extends Binder {

@Override
protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
switch (code) {
case SET_MUSIC_PATH:
try {
mediaPlayer.reset();
mediaPlayer.setDataSource(data.readString());
mediaPlayer.prepareAsync();
} catch (IOException e) {
e.printStackTrace();
}
break;
case PLAY_MUSIC:
mediaPlayer.start();
break;
case STOP_MUSIC:
mediaPlayer.stop();
break;
case PAUSE_MUSIC:
mediaPlayer.pause();
break;
case GET_CURRENT_POS:
if (reply != null) reply.writeInt(mediaPlayer.getCurrentPosition());
break;
case GET_TOTAL_DUR:
if (reply != null) reply.writeInt(mediaPlayer.getDuration());
break;
case SET_SEEK_TO:
mediaPlayer.seekTo(data.readInt());
break;
case SET_MUSIC_URL:
musicUri = data.readString();
break;
case GET_MUSIC_URL:
if (reply != null) reply.writeString(musicUri);
break;
case IS_PLAYING:
if (reply != null) reply.writeInt(mediaPlayer.isPlaying() ? 1 : 0);
break;
}
return super.onTransact(code, data, reply, flags);
}
}

然后调用IBindertransact方法就可以控制服务,data传输参数、reply获取结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void setMusicUri(String uri) {
Parcel data = Parcel.obtain();
data.writeString(uri);
transact(MusicService.SET_MUSIC_URL, data);
}

private Parcel transact(int control, Parcel data) {
Parcel reply = Parcel.obtain();
try {
iBinder.transact(control, data, reply, 0);
} catch (RemoteException e) {
e.printStackTrace();
}finally {
data.recycle();
}
return reply;
}

然后把这些方法封装在MusicServiceConnection里面,在Activity里面只需要调用

1
MusicSC.setMusicUri(uri);

就可以设置音乐的 URI

Handler

对于进度条的更新,我们需要新开一个进程进行处理,这里就使用到了Handler

首先定义一个Runnable,循环调用自身,使其每过一定时间执行一次。

1
2
3
4
5
6
7
runnable = new Runnable() {
@Override
public void run() {
// Do something.
handler.postDelayed(this, 500);
}
};

然后使用Handler来管理和调用它

1
2
handler.removeCallbacks(runnable); 	// 停止执行
handler.post(runnable); // 开始执行

图片旋转

这个可以使用ObjectAnimator实现

首先定义一个旋转动画,并且绑定到指定的控件上

1
2
3
4
5
animation = ObjectAnimator.ofFloat(binding.musicImage, "rotation", 0f, 360f);
animation.setDuration(10000);
animation.setInterpolator(new LinearInterpolator());
animation.setRepeatCount(ObjectAnimator.INFINITE);
animation.setRepeatMode(ObjectAnimator.RESTART);

然后就可以随心所欲地控制它了鸭

1
2
3
4
animation.start(); 	// 开始动画
animation.end(); // 结束动画
animation.pause(); // 暂停动画
animation.resume(); // 恢复动画

RXJava

这个项目需要使用 RxJava,因此在更新进度条的时候就使用了 RxJava

首先新建一个Observable对象,在subscrible里面循环获取歌曲播放进度交给观察者的onNext处理,如果播放已经结束,就调用onComplete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//RxJava
observable = Observable.create(emitter -> {
Thread.sleep(500);
MediaPlayer mediaPlayer = iService.getMediaPlayer();
while (mediaPlayer.isPlaying()) {
if (!touching) {
emitter.onNext(mediaPlayer.getCurrentPosition());
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
emitter.onComplete();
});

然后建立一个DisposableObserver,观察歌曲播放进去并更新 UI,然后使用subscriblemainThread里面订阅可观察者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 更新进度
if (disposableObserver != null && !disposableObserver.isDisposed()) return;
disposableObserver = new DisposableObserver<Integer>() {
@Override
public void onNext(Integer integer) {
updateTime(integer);
}

@Override
public void onError(Throwable e) {

}

@Override
public void onComplete() {
if (disposableObserver != null && !disposableObserver.isDisposed()) {
disposableObserver.dispose();
disposableObserver = null;
}
}
};
observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(disposableObserver);

退出的时候需要主动解除订阅状态

1
disposableObserver.dispose();

遇到的困难以及解决思路

这次实验第一次使用到Service,虽然一开始不知道怎么使用,但是通过一番查阅资料后还是大概知道了服务的使用方法。

一开始遇到的困难主要是播放器无法播放所选择的音乐,MediaPlayer一直都报错说无法解析文件。

解决方法:最后发现原来是setDataSource之前忘记了MediaPlayer.reset(),它还保留着之前的数据,导致新的数据无法加载。

后来也遇到了一个问题,加载资源的时候一直报错MediaPlayer Error(-38, 0),通过查阅资料发现这个错误是因为播放器还没有加载完毕资源之前对资源进行了操作,导致播放器崩溃。

解决方法:我使用了onPrepare的回调来做加载之后的操作,就没有问题了。

总结

这次实验第一次使用到了 Service,Service 作为 Android 应用的四大部件之一,其重要性是不言而喻的。有了 Service,我们就可以使应用在后台处理一些工作,比如接受来自服务器的信息(如 QQ,微信),或者是播放音乐。但是,如果不正确地使用 Service,就会成为流氓软件,不仅占用计算资源而且还耗电。因此对于 Service 的使用必须要遵循 Android 开发的规范。

土豪通道
0%