Android音乐播放器开发小记——功能实现1

项目源码
https://github.com/dogmeng/littleyunmusic
功能实现:
第一部分 整体各部分之间的协作流程.
1.对音乐的操作控制放在service中处理
2.利用代理类PlayerProxy处理activity和service的交互
3.前台对后台的调用,利用在onServiceConnected中返回的AIDL内部类的实现类对象.后台对前台的调用,利用在onServiceConnected中对后台设置监听类对象,同样,此监听类也需要生成AIDL文件
下面看具体代码:
1.音乐service:具体参见源码中MusicService类和MusicControl类
为保证音乐能在程序退出至后台时,依然播放,这里使用的是跨进程的service,当然不跨进程也可以,具体看需求.在AndroidManifest.xml中配置

<service
            android:name=".service.MusicService"
            android:enabled="true"
            android:exported="true"
            android:process=":musicservice"
            >            
   </service>

在service中要做的工作有:初始化MediaPlayer,实现播放,暂停,上一首,下一首,设置播放模式,失去焦点暂停,重获焦点播放,与前台通知交互等功能.
常见的自动播放下一首,是在onComplete中play下一首即可.为了保证音乐无缝切换,此处使用了MediaPlayer的setNextMediaPlayer(MediaPlayer mp).不过这样会导致实现播放控制方面稍显复杂.这里我先声明三个MediaPlayer引用:
private MediaPlayer mPlayer,mNextPlayer1,mNextPlayer2;
在MusicControl的构造方法也就是MusicService的onCreate中初始化其中两个对象,mNextPlayer1 = new MediaPlayer();mNextPlayer2 = new MediaPlayer();
mPlayer指向的是当前正在使用的MediaPlayer, 默认mPlayer = mNextPlayer1;
在当前歌曲播放完时会回调onCompletion,在onCompletion 中改变mPlayer 的指向,并且setNextPlayer();这样就可以交替使用mNextPlayer1和mNextPlayer2,实现自动播放下一首了.

class CompletionListener implements OnCompletionListener{
MediaPlayer nextPlayer;
public CompletionListener(MediaPlayer next){
nextPlayer = next;
}
@Override
public void onCompletion(MediaPlayer arg0) {
// TODO Auto-generated method stub
if(arg0 == mPlayer && nextPlayer!=null){
//因为之前已经设置过setNextPlayer,所以会自动播放设置好的player,即此时正在播放的
//player为nextPlayer
mPlayer = nextPlayer;
//设置当前播放歌曲的位置 
mCurrentPosition = nextPosition; 
//加入播放历史,在自动循环列表模式下,避免自动获取下一首的歌曲为当前已播放的歌曲
mHistoryPositions.add(mCurrentPosition); 
if(mHistoryPositions.size()>mPlayList.size())
mHistoryPositions.remove(0);
mCurrentSong = mPlayList.get(mCurrentPosition);
mCurrentSongId = mCurrentSong.songId;
//回调activity的方法,改变页面信息 
if (mPlayer.isPlaying() && mUiListener != null) { 
try {
mUiListener.onMusicStart();
//更新前台通知信息
mainHandler.obtainMessage(MAINHANDLER_UPEATE, true).sendToTarget();
} catch (RemoteException e) {
} 
}
//设置下一首资源setNextPlayer();
}else{
mContext.cancelNotification();
mWakeLock.releaseWakeLock();
}
}
}

失去焦点和重获焦点的类:

class AudioFocusManager implements AudioManager.OnAudioFocusChangeListener { 
private boolean isPausedByFocusLossTransient = false; 
private int mVolumeWhenFocusLossTransientCanDuck; 
private WeakReferenceplayerControl; 
public AudioFocusManager(MusicControl control) {
 playerControl = new WeakReference(control); 
} 
@Override 
public void onAudioFocusChange(int focusChange) { 
int volume; 
switch (focusChange) { 
// 暫時丢失焦点,如来电,應暫停播放,但不清除 
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: 
if(playerControl.get().isPlaying()){
isPausedByFocusLossTransient = true; 
}
playerControl.get().pause(); 
break; 
// 重新获得焦点
case AudioManager.AUDIOFOCUS_GAIN: 
if(isPausedByFocusLossTransient){
 isPausedByFocusLossTransient = false;
 playerControl.get().continuePlay(mPlayer); 
} 
volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); 
if (mVolumeWhenFocusLossTransientCanDuck > 0 && volume == mVolumeWhenFocusLossTransientCanDuck / 2) { 
// 恢复音量
 mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeWhenFocusLossTransientCanDuck, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); 
} 
mVolumeWhenFocusLossTransientCanDuck = 0;
break; 
// 永久丢失焦点,如被其他播放器抢占,清理資源 
case AudioManager.AUDIOFOCUS_LOSS: 
playerControl.get().stop();
 isPausedByFocusLossTransient = true;
 break; 
// 瞬间丢失焦点,如通知,但是允許持續播放音樂(以很小的聲音)
 case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: 
// 音量减小为一半 
volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); 
if (playerControl.get().isPlaying() && volume > 0) {
mVolumeWhenFocusLossTransientCanDuck = volume;
mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mVolumeWhenFocusLossTransientCanDuck / 2, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); 
}
break; 
}
}
}

当点击播放音乐时,开启前台通知:

public void updateNotification(SingleSongBean music,boolean isPlaying){
if(isPlaying){
if(notifyMode == NOTIFICATION_STOP){
//如果没有开启过,就先开启前台服务;
startForeground(id, creatNotification(music,isPlaying));
notifyMode = NOTIFICATION_START;
}else{
//如果已经开启前台服务,就更新通知内容
notificationManager.notify(id, creatNotification(music,true));
}
}else{
if(notifyMode == NOTIFICATION_STOP){
}else{
//暂停前台服务 
stopForeground(false); 
notificationManager.notify(id, creatNotification(music,false));
}
} 
}
public Notification creatNotification(SingleSongBean music,boolean isPlaying){
RemoteViews remoteViews = new RemoteViews(this.getPackageName(), R.layout.notification);
RemoteViews remoteViewsSmall = new RemoteViews(this.getPackageName(), R.layout.notification_small); 
String title = music.songName; 
String detail = music.albumName+"_"+music.artistName; 
remoteViews.setImageViewResource(R.id.notification_image,R.drawable.album); 
remoteViews.setTextViewText(R.id.music_title, title); 
remoteViews.setTextViewText(R.id.music_detail, detail); 
remoteViewsSmall.setImageViewResource(R.id.notification_image,R.drawable.album);
remoteViewsSmall.setTextViewText(R.id.music_title, title);
remoteViewsSmall.setTextViewText(R.id.music_detail, detail);
 
Intent playIntent = new Intent(NOTIFICATION_PLAY_PAUSE); 
PendingIntent playPendingIntent = PendingIntent.getBroadcast(this, 0, playIntent, PendingIntent.FLAG_UPDATE_CURRENT); 
remoteViews.setImageViewResource(R.id.play, isPlaying? R.drawable.pause : R.drawable.play); 
remoteViews.setOnClickPendingIntent(R.id.play, playPendingIntent);
remoteViewsSmall.setImageViewResource(R.id.play, isPlaying? R.drawable.pause : R.drawable.play);
remoteViewsSmall.setOnClickPendingIntent(R.id.play, playPendingIntent);

Intent preIntent = new Intent(NOTIFICATION_PRE);
PendingIntent nextPendingIntent = PendingIntent.getBroadcast(this, 0, preIntent, PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.play_prev, nextPendingIntent);
remoteViewsSmall.setOnClickPendingIntent(R.id.play_prev, nextPendingIntent);
Intent nextIntent = new Intent(NOTIFICATION_NEX); 
PendingIntent nextPIntent = PendingIntent.getBroadcast(this, 0, nextIntent, 0);
remoteViews.setOnClickPendingIntent(R.id.play_next, nextPIntent);
remoteViewsSmall.setOnClickPendingIntent(R.id.play_next, nextPIntent);

Intent cancelIntent = new Intent(NOTIFICATION_CANCEL);
PendingIntent cancelPIntent = PendingIntent.getBroadcast(this, 0, cancelIntent, 0);
remoteViews.setOnClickPendingIntent(R.id.notification_close, cancelPIntent);
remoteViewsSmall.setOnClickPendingIntent(R.id.notification_close, cancelPIntent);

Intent intent = new Intent(this, PlayActivity.class); 
intent.setAction(Intent.ACTION_VIEW); 
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 
if(mNotification == null){ 
NotificationCompat.Builder builder = new NotificationCompat.Builder(this.getApplicationContext()) .setContentIntent(pendingIntent) .setSmallIcon(R.drawable.album) .setWhen(System.currentTimeMillis()); 
mNotification = builder.build();
//notification的bigContentView在通知出现在状态栏列表第一行,或者用户向下拉时
//contentView 非第一行非下拉时的显示状态 
mNotification.bigContentView = remoteViews; 
mNotification.contentView = remoteViewsSmall; 
}else { 
mNotification.bigContentView = remoteViews; 
mNotification.contentView = remoteViewsSmall;
 } 
notificationTarget = new NotificationTarget(this, remoteViews, R.id.notification_image, mNotification, id); 
notificationTargetSmall = new NotificationTarget(this, remoteViewsSmall, R.id.notification_image, mNotification, id); 
if(music.islocal == 0){
Glide.with(getApplicationContext()).load(Uri.parse(music.album_art)).asBitmap().placeholder(R.drawable.album).into(notificationTarget);
Glide.with(getApplicationContext()).load(Uri.parse(music.album_art)).asBitmap().placeholder(R.drawable.album).into(notificationTargetSmall); 
}else{
Glide.with(getApplicationContext()).load(music.album_art).asBitmap().placeholder(R.drawable.album).into(notificationTarget);
Glide.with(getApplicationContext()).load(music.album_art).asBitmap().placeholder(R.drawable.album).into(notificationTargetSmall);
 } 
return mNotification;
}

前台服务通过发送广播,来调用service方法Service中的其他方法,可参看源码在activity中利用PlayerProxy类对service进行操作,主要是实现一层封装,利用单例模式,最终调用的还是service中的方法.开启和绑定service

public void startAndBindPlayService(Context context, ServiceConnection connection){
intent = new Intent(context, MusicService.class); 
if(mControl == null){ context.startService(intent); } 
this.context = context; 
context.bindService(intent, connection, Service.BIND_AUTO_CREATE); }

在ServiceConnection中获取后台的AIDL的Stub对象,进而可以调用service中的方法(这些方法运行在后台的binder线程池中);同时,把前台的AIDL的Stub对象,传入后台中去,当后台音乐状态改变时,即调用Stub对象相对应的方法,这些方法运行在前台binder线程池中,所以,此处进行了线程切换,使其在主线程刷新界面.也可以使用广播来处理,广播本来也是跨进程的一种方式.当后台发送广播时,系统会在已注册的广播中寻找匹配的广播接收器来响应.

public class MusicServiceConnection implements ServiceConnection{
private MusicControlInterface control;
private UIChangedListener uiListener;
private Handler mHandler = new Handler(Looper.getMainLooper());
private BaseActivity activity;
public MusicServiceConnection(final UIChangedListenerImpl listener,BaseActivity activity){
this.activity = activity;
uiListener = new UIChangedListener.Stub() {
@Override
public void onMusicStart() throws RemoteException {
// TODO Auto-generated method stub
mHandler.post(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
listener.onMusicStart();
}
});
}
@Override
public void onMusicPause() throws RemoteException {
// TODO Auto-generated method stub
mHandler.post(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
listener.onMusicPause();
}
});
}
@Override
public void onBufferingUpdate(int percent) throws RemoteException {
// TODO Auto-generated method stub
}
@Override
public void onDeleteAll() throws RemoteException {
// TODO Auto-generated method stub
mHandler.post(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
listener.onDeleteAll();
}
});
}
};
}
@Override
public void onServiceConnected(ComponentName arg0, IBinder arg1) {
// TODO Auto-generated method stub
control =
MusicControlInterface.Stub.asInterface(arg1);PlayerProxy.getIntance().setService(control);
try {
List song = PlayerProxy.getIntance().getPlayList();
if(song!=null&&song.size()>0){  
activity.showQuickControl(true);
activity.isNow = false;
}else{
activity.showQuickControl(false);
activity.isNow = true;
}
} catch (RemoteException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
control.setUiListener(uiListener);
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
// TODO Auto-generated method stub
PlayerProxy.getIntance().startAndBindPlayService(activity,this);
}
}

这样前台和后台就可以互相传递数据,进行交互

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容