上一次谈了音乐播放的实现,这次说下最复杂的进度条和歌词更新。因为须要在播放的Activity和播放的Service间进行交互,所以就涉及了Activity对Service的绑定以及绑定后数据的传输,这个须要对服务绑定熟悉才干够理解。原理不复杂。可是步骤略微繁琐,代码贴起来可能会非常混乱。
进度条和歌词放在一起说比較好,不然比較混乱。进度条的调整大家都懂的,就是进度条调到哪里歌曲的播放就跟到哪里,歌词也得跟到哪里。首先看下上一篇看过的開始button监听事件中服务的绑定代码:
//绑定播放服务
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