美文网首页
Android音乐播放器开发小记——功能实现1

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

作者: 气球鼓鼓 | 来源:发表于2017-11-15 10:56 被阅读0次

    项目源码
    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);
    }
    }
    

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

    相关文章

      网友评论

          本文标题:Android音乐播放器开发小记——功能实现1

          本文链接:https://www.haomeiwen.com/subject/zjdcvxtx.html