上一次谈了音乐播放的实现,这次说下最复杂的进度条和歌词更新。由于需要在播放的Activity和播放的Service间进行交互,所以就涉及了Activity对Service的绑定以及绑定后数据的传输,这个需要对服务绑定熟悉才可以理解。原理不复杂,但是步骤稍微繁琐,代码贴起来可能会很混乱。
进度条和歌词放在一起说比较好,不然比较混乱。进度条的调整大家都懂的,就是进度条调到哪里歌曲的播放就跟到哪里,歌词也得跟到哪里。首先看下上一篇看过的开始按钮监听事件中服务的绑定代码:
//绑定播放服务
bindService(intent, conn,BIND_AUTO_CREATE);
//Runnable对象加入消息队列,在handler绑定的线程中执行,用于歌词更新
handler.post(updateTimeCallback);
start = System.currentTimeMillis();
bindService(intent, conn,BIND_AUTO_CREATE); 中的conn是绑定服务后的一个回调接口,我们看下代码:
/**
* 传入bindService()中的回调接口
*/
ServiceConnection conn = new ServiceConnection(){
@Override
public void onServiceConnected(ComponentName arg0, IBinder binder) {//绑定成功后调用该方法
PlayActivity.this.binder = (Binder)binder;
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
}
};
其实就是一个语句,作用就是将播放服务返回的IBinder对象引用赋给播放Activity,这样Activity就可以获取Service返回的数据了,我们这里是为了获取此时播放的进度数据。
handler.post(updateTimeCallback);是将一个Runnable对象加入队列中,这个Runnable对象updateTimeCallback
就是实现进度条和歌词更新的核心实现代码,不过看这个类之前,我们先看下进度条的监听事件:
/**
* 经过试验如果把进度条调动之前和调动之后合并为一种方式实现歌词的更新的话会奔溃,原因可能是不同线程间资源的同步问题,只好拆成
* 进度条调动之前(change == false)和调动之后(change == true)两部分在歌词更新的线程中执行
* @author yan
*
*/
class SeekBarListener implements OnSeekBarChangeListener{
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
@Override
public void onStartTrackingTouch(SeekBar arg0) {
Log.d("yinan", "start--------"+seekBar.getProgress());
}
@Override
public void onStopTrackingTouch(SeekBar arg0) {
if(stopMusic == false){
change = true;//进度条进度人为改变,将改变后的进度发送给播放服务
Intent intent = new Intent();
intent.setClass(PlayActivity.this, PlayService.class);
intent.putExtra("progress", seekBar.getProgress());
startService(intent);
}else{
seekBar.setProgress(0);
}
}
}
在进度条位置得到调整之后,仍然用startService(intent)方式,将封装进度条进度的Intent对象传给Service的onStartCommand方法处理。
现在来看看实现的核心代码Runnable对象updateTimeCallback的类:
/**
* 异步调整进度条位置与歌曲进度一致和显示歌词的类
* @author yinan
*
*/
class UpdateTimeCallback implements Runnable{
Queue times = null;
Queue messages = null;
ArrayList<Queue> queues = null;
public UpdateTimeCallback(ArrayList<Queue> queues){
times = queues.get(0);
messages = queues.get(1);
this.queues = queues;
}
/**
* run方法由于没有条用start所以并没有开辟子线程,所以在内部开启子线程
*/
@Override
public void run() {
total = PlayService.totalLength;
if(change == true){
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeString("change");
try {
binder.transact(0, data, reply, 0);
} catch (RemoteException e) {
e.printStackTrace();
}
//直到Service返回才执行这一句
float s = reply.readFloat();//s为歌曲播放的时间进度
float f = s*100;
seekBar.setProgress((int)f);//转化为进度条进度
//此时对应的播放时间点
final long t = (long) (s*total);
preparelrc(mp3Info.getLrcName());
times = queues.get(0);
messages = queues.get(1);
System.out.println("times"+times.size());
System.out.println("messages"+messages.size());
if(stopMusic == false){
new Thread(){
public void run(){
while(nextTime < t){ //从头遍历歌词,时间队列直到时间点大于当前时间
System.out.println("nextTime"+nextTime);///
lyric = message;
if((times.size()>0)||messages.size()>0){
nextTime = (Long)times.poll();
message = (String)messages.poll();
}else{
lyric = null;
stopMusic = true;
}//保存时间点刚大于当前时间的上一条歌词,即最接近当前的歌词
System.out.println("nextTime"+nextTime);
}
}
}.start();
System.out.println("lyric"+lyric);
if(lyric != null){
lrcText.setText(lyric);
}
}
}
if(!change){
nowTime = System.currentTimeMillis() - start;
seekBar.setProgress((int)(nowTime*100/total));
if(stopMusic == false){
new Thread(){
public void run(){
while(nextTime < nowTime){ //从头遍历歌词,时间队列直到时间点小于当前时间
System.out.println("nextTime"+nextTime);
lyric = message;//保存时间点刚大于当前时间的上一条歌词,即最接近当前的歌词
if((times.size()>0)||messages.size()>0){
nextTime = (Long)times.poll();
message = (String)messages.poll();
}else{
lyric = null;
stopMusic = true;
}
}
}
}.start();
if(lyric != null){
lrcText.setText(lyric);
}
}
}
if(stopMusic == true){
handler.removeCallbacks(updateTimeCallback);
}
handler.postDelayed( updateTimeCallback, 200);//每20毫秒执行一次线程
}
}
这段代码大致是当获取到服务返回的歌曲实时进度的数据时,将进度条的位置进行调整到相应为孩子的处理,然后将歌词更新到对应的部分。
由于进度条要实时跟进,所有
UpdateTimeCallback类的run方法是每隔200毫秒就执行一次,到Service中获取返回值。
这里是将处理的情况分为进度条被改变了和未被改变两种情况,用标志位change表示(一旦进度条被改变了就执行了change为true那一段代码)。开子线程是因为对歌词的处理耗时,不适合放在UI线程中。
具体的流程是:
首先,Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeString("change");
binder.transact(0, data, reply, 0);
binder就是之前和Service绑定的IBinder对象,binder.transact是将一个Parcel对象(传入"change"仅仅是一个标志)传给Service,然后Service返回歌曲进度,在float s = reply.readFloat()这句中接收,s是一个当前进度占总进度的百分比数,然后将进度条位置设置一下即可。
然后进行歌词更新的处理。首先是对歌词的准备处理工作,preparelrc(mp3Info.getLrcName()),将lrc格式的歌词中的时间点和对应的具体歌词拆分成两个队列。(具体代码请看源码)然后根据Service返回的歌曲播放进度,对两个队列进行遍历。当有时间点超过进度对应的时间点时,将该时间点对应的歌词取出,显示在Activity上。
前面总是说Service返回歌曲实时进度,我们来看看Service中对于数据返回的处理。
当进度条被调整时,会进入Service的onStartCommand方法,进入到以下的if语句中:
if(intent.getIntExtra("progress", 0) != 0){///
if(isPlay){
//进度条进度
int progress = intent.getIntExtra("progress", 0);
if(progress != 0)
//将音乐进度调整到对应位置
mediaPlayer.seekTo(progress*totalLength/100);
}
}
Service又是如何实时返回歌曲进度的呢?Service中有一个Binder类,是IBinder的子类:
/*
*执行 binder.transact()后执行
*/
class FirstBinder extends Binder{
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply,
int flags) throws RemoteException {
String str = data.readString();
int currentPosition = mediaPlayer.getCurrentPosition();
float s = (float)currentPosition/mediaPlayer.getDuration();
if(isPlay)
reply.writeFloat(s);
return super.onTransact(code, data, reply, flags);
}
是的,它就是之前Activity与Service绑定后获得的IBinder对象。
每当Activity的
binder.transact(0, data, reply, 0);被调用的时候,Service就会调用onTransact方法,将获取到的数据装入Parcel对象reply中,然后将整个IBinder对象返回Activity。
MP3播放器就这样讲完了,可能讲的很乱,希望对各位有帮助,源代码下载链接:http://download.csdn.net/detail/sinat_23092639/8933995