Android组件化开发(五)--完整版音乐播放组件的封装

简介: 前面几篇系列文章我们讲解了`组件化开发`中几个常用功能组件的开发,包括:`网络请求组件`,`图片加载请求组件`,`应用保活组件`。今天我们来封装一个`音乐播放组件`。
🔥 Hi,我是小余。

本文已收录到 GitHub · Androider-Planet 中。这里有 Android 进阶成长知识体系,关注公众号 [小余的自习室] ,在成功的路上不迷路!

前言

前面几篇系列文章我们讲解了组件化开发中几个常用功能组件的开发,包括:网络请求组件图片加载请求组件应用保活组件。今天我们来封装一个音乐播放组件

组件化相关系列文章:

Android组件化开发(一)--Maven私服的搭建

Android组件化开发(二)--网络请求组件封装

Android组件化开发(三)--图片加载组件封装

Android组件化开发(四)--进程保活组件的封装

组件化开发.png

还记得开始写这个系列时候说过,我们这个组件化系列会以一个实战项目展开,那这个项目就是一个音乐播放器,暂且叫他xx云音乐

在写这篇文章之前,我在网上查找了一些类似文章,发现很多要么是直接贴代码,要么就是讲解了一些思路,并没有给出太多具体的代码,看了之后很容易就忘了。今天笔者使用架构思维和具体代码结合的方式带大家来看下笔者是如何去封装一个音乐播放组件的。

那我们就开始吧。

看过我之前文章都知道,笔者很注重需求的分析

一切代码的开发都原自需求,做好需求分析直观重要。

需求分析:

往大了讲:

  • 1.封装一个音乐播放组件:

往细了讲:

- 1.基础功能:播放,暂停,下一首,上一首
- 2.播放模式:单曲循环,顺序播放,随机播放
- 3.用户登录,收藏等
- 4.再扩展一点,开发一个类似朋友圈的功能。。。走远了哈

有了上面的需求分析,我们就有了一个切入点。

技术选型

要实现上面需求:

笔者可以想到的是使用一个 MediaPlayer来实现我们的需求

如果对MediaPlayer还不是很了解的,请转到官网查看:
https://developer.android.google.cn/guide/topics/media/mediaplayer?hl=zh_cn

引用官网的话:

MediaPlayer 类是媒体框架最重要的组成部分之一。此类的对象能够获取、解码以及播放音频和视频,而且只需极少量设置。它支持多种不同的媒体源,例如:

  • 本地资源
  • 内部 URI,例如您可能从内容解析器那获取的 URI
  • 外部网址(流式传输)

MediaPlayer最重要的是:

他内部维护了一套音乐播放状态机每个步骤都需要根据上一个状态来做改变,如果在某个状态执行了错误的步骤就会出异常甚至崩溃,你可能不想看到这种事件发生在你的app中吧。。
所以接下来你会在我的代码中看到很多关于状态的判断。

MediaPlayer播放状态切换图

音乐播放状态机.awebp

可以看到内部调用过程还是挺复杂的。好在这些都是MediaPlayer自己内部维护,大部分情况不需要人为去处理。

好了,有了前面的知识铺垫我们就正式开始封装啦

封装

这里先放一张笔者设计的一个UML类图:
音乐播放组件.png

  • CustomMediaPlayer:

这个类继承MediaPlayer,内部主要维护了MediaPlayer的各个阶段状态,MediaPlayer内部也维护了一个状态值,但是使用起来不够方便和优雅,还是自己掌控比较方便好用。

  • AudioPlayer

这个类主要作用是对外提供一些基础功能:如对传入的音乐进行同步或者异步加载,对音乐进行暂停,恢复等操作。内部维护了一个CustomMediaPlayer对象,所以操作都是使用这个CustomMediaPlayer来实现的.这个类还会对MediaPlayer的状态对外发布,让外部做出对应的处理。

  • AudioController

一个单例类,这个类很关键,是整个封装库的核心,做到承上启下的作用,
承上:管理音乐播放歌曲列表Queue和播放模式PlayModel
启下:内部维护一个AudioPlayer的对象,使用AudioPlayer提供的方法,做到对音乐列表的播放,暂停,恢复等操作也对外发布一些事件,如播放模式的改变等,需要外部UI做出一些反应。

  • MusicPlayerService

这是一个播放音乐用的Service,内部维护了一个AudioProvider.stubbinder通讯使用的类:对外提供通讯接口:如设置播放队列,暂停以及恢复等操作。这个挂接了一个播放器的Notification,可以让服务变为前台服务,进一步增加应用的保活能力。

  • NotificationHelper:

这个是一个Notification的辅助类,用于在通知栏显示我们的音乐播放事件。

  • GreenDaoHelper:

这个类用来处理对收藏的歌曲精选持久化

  • AudioBean:

这个类是贯彻整个播放体系的歌曲信息封装类

整个体系主要就是由这几个类撑起来的,每个类从下往上都有不同职责,相辅相成。

经过上面的分析,我们大概了解了本次封装的一个体系架构:

下面对具体代码来讲解:

笔者在代码中已经给出了很多非常详细的注解,方便大家查看:

  • CustomMediaPlayer.java
/**
 * 这个类主要是提供MediaPlayer的各种状态,内部回调
 */
public class CustomMediaPlayer extends MediaPlayer implements MediaPlayer.OnCompletionListener,
        MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener {
    //使用枚举类表示当前播放器的各种状态
    public enum Status{
        IDLE, INITIALIZED, PREPARED,STARTED, PAUSED, STOPPED, COMPLETED
    }

    //当前播放器状态
    private Status mStatus;

    public CustomMediaPlayer(){
        super();
        //默认状态 IDLE
        mStatus = Status.IDLE;
        //设置播放完毕后的回调
        setOnCompletionListener(this);
        //设置数据准备完毕,准备播放的回调
        setOnPreparedListener(this);
        //设置发生错误事件的回调
        setOnErrorListener(this);
    }

    //设置播放完毕后的回调
    @Override
    public void onCompletion(MediaPlayer mp) {
        mStatus = Status.COMPLETED;
        listener.omCompleted();
    }

    //播放器重置:状态设置为IDLE
    @Override
    public void reset() {
        super.reset();
        mStatus = Status.IDLE;
    }

    //设置数据元的回调:状态设置为:INITIALIZED
    @Override
    public void setDataSource(String path) throws IOException, IllegalArgumentException, IllegalStateException, SecurityException {
        super.setDataSource(path);
        mStatus = Status.INITIALIZED;
    }

    //数据准备完毕后,开始播放前的状态:PREPARED
    @Override
    public void onPrepared(MediaPlayer mp) {
        mStatus = Status.PREPARED;
        listener.onPrepared();
    }

    //发送错误事件的回调,返回true表示消费掉事件,如果为false则还会回调onComplete接口回调
    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        listener.onError();
        return true;
    }

    //开始播放状态
    @Override
    public void start() throws IllegalStateException {
        super.start();
        mStatus = Status.STARTED;
    }

    //停止播放状态
    @Override
    public void stop() throws IllegalStateException {
        super.stop();
        mStatus = Status.STOPPED;
    }

    //暂停状态
    @Override
    public void pause() throws IllegalStateException {
        super.pause();
        mStatus = Status.PAUSED;
    }

    public void setmStatus(Status mStatus) {
        this.mStatus = mStatus;
    }

    public Status getmStatus() {
        return mStatus;
    }

    private MediaPlayerListener listener;

    public void setListener(MediaPlayerListener listener) {
        this.listener = listener;
    }

    /**
     * 这个Listener用途AudioPlayer的事件回调
     */
    public interface MediaPlayerListener{
        void onPrepared();
        void omCompleted();
        void onError();
    }
}

看代码中,我们可以发现,这个类只是给MediaPlayer设置了一些供回调的接口,且维护了当前MediaPlayer的播放状态。

  • AudioPlayer.java
/**
 *播放器事件源
 */
public class AudioPlayer implements CustomMediaPlayer.MediaPlayerListener {
    private CustomMediaPlayer mMediaPlayer;
    private WifiManager.WifiLock mWifiLock;
    private static final String TAG = "AudioPlayer";
    private static final int TIME_MSG = 0x01;
    private static final int TIME_INVAL = 100;
    //是否是因为系统引起的焦点问题
    private boolean isPausedByFocusLossTransient;
    //音频焦点监听器
    private AudioFocusManager mAudioFocusManager;

    public AudioPlayer() {
        isPausedByFocusLossTransient = false;
        mMediaPlayer = new CustomMediaPlayer();
        init();
    }

    /**
     * 初始化mMediaPlayer的事件,如:使用唤醒锁,设置音频格式,设置wifilock,设置mAudioFocusManager
     *
     */
    private void init() {
        //使用唤醒锁
        mMediaPlayer.setWakeMode(AudioHelper.getmContext(), PowerManager.PARTIAL_WAKE_LOCK);
        //设置音频格式
        mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
        //设置数据准备完毕回调
        mMediaPlayer.setListener(this);
        //设置wifilock,可以让在后台也可以运行wifi下载加载数据
        mWifiLock = ((WifiManager) AudioHelper.getmContext()
                .getApplicationContext()
                .getSystemService(Context.WIFI_SERVICE)).createWifiLock(WifiManager.WIFI_MODE_FULL, "AudioPlayer");
        //设置Audio管理回调器,在一些场景下会回调,如中途遇到打电话,其他应用占用音频端口
        mAudioFocusManager = new AudioFocusManager(AudioHelper.getmContext(),audioFocusListener);
    }

    //播放进度更新handler
    private Handler mHandler = new Handler(Looper.getMainLooper()) {
        @Override public void handleMessage(Message msg) {
            switch (msg.what) {
                case TIME_MSG:
                    //暂停也要更新进度,防止UI不同步,只不过进度一直一样
                    if (getStatus() == CustomMediaPlayer.Status.STARTED
                            || getStatus() == CustomMediaPlayer.Status.PAUSED) {
                        //UI类型处理事件
                        EventBus.getDefault()
                                .post(new AudioProgressEvent(getStatus(), getCurrentPosition(), getDuration()));
                        sendEmptyMessageDelayed(TIME_MSG, TIME_INVAL);
                    }
                    break;
            }
        }
    };

    AudioFocusManager.AudioFocusListener audioFocusListener = new AudioFocusManager.AudioFocusListener() {
        // 重新获得焦点
        @Override
        public void audioFocusGrant() {
            setVolumn(1.0f,1.0f);
            if(isPausedByFocusLossTransient){
                resume();
            }
            isPausedByFocusLossTransient = false;
        }

        // 永久丢失焦点,如被其他播放器抢占
        @Override
        public void audioFocusLoss() {
            if(mMediaPlayer != null) {
                pause();
            }
            isPausedByFocusLossTransient = true;
        }
        // 短暂丢失焦点,如来电
        @Override
        public void audioFocusLossTransient() {
            if (mMediaPlayer != null) pause();
            isPausedByFocusLossTransient = true;
        }
        // 瞬间丢失焦点,如通知
        @Override
        public void audioFocusLossDuck() {
            //瞬间失去焦点,
            setVolumn(0.5f, 0.5f);
        }
    };

    /**
     * 开始启动播放音乐,由异步onPrepared回调调用
     */
    private void start(){
        mMediaPlayer.setmStatus(CustomMediaPlayer.Status.PREPARED);
        mMediaPlayer.start();
        // 启用wifi锁
        mWifiLock.acquire();
        //更新进度
        mHandler.sendEmptyMessage(TIME_MSG);
        //发送start事件,UI类型处理事件
        EventBus.getDefault().post(new AudioStartEvent());
    }

    /**
     * 对外提供的加载音频的方法
     */
    public void load(AudioBean audioBean) {
        try {
            mMediaPlayer.reset();
            mMediaPlayer.setDataSource(audioBean.mUrl);
            mMediaPlayer.prepareAsync();
            //发送加载音频事件,UI类型处理事件
            EventBus.getDefault().post(new AudioLoadEvent(audioBean));
        } catch (IOException e) {
            e.printStackTrace();
            EventBus.getDefault().post(new AudioErrorEvent());
        }
    }
    /**
     * 对外提供的播放方法
     */
    public void resume() {
        if(getStatus() == CustomMediaPlayer.Status.PAUSED){
            start();
        }
    }
    /**
     * 对外暴露pause方法
     */
    public void pause() {
        if (getStatus() == CustomMediaPlayer.Status.STARTED) {
            mMediaPlayer.pause();
            //关注wifi锁
            if(mWifiLock.isHeld()){
                mWifiLock.release();
            }
            // 取消音频焦点
            if (mAudioFocusManager != null) {
                mAudioFocusManager.abandonAudioFocus();
            }
            //发送暂停事件,UI类型事件
            EventBus.getDefault().post(new AudioPauseEvent());
        }
    }
    /**
     * 销毁唯一mediaplayer实例,只有在退出app时使用
     */
    public void release() {
        if(mMediaPlayer == null){
            return;
        }
        mMediaPlayer.release();
        mMediaPlayer = null;
        // 关闭wifi锁
        if (mWifiLock.isHeld()) {
            mWifiLock.release();
        }
        mWifiLock = null;
        // 取消音频焦点
        if (mAudioFocusManager != null) {
            mAudioFocusManager.abandonAudioFocus();
        }
        mAudioFocusManager = null;
        //发送销毁播放器事件,清除通知等
        EventBus.getDefault().post(new AudioReleaseEvent());
    }

    /**获取当前状态
     * @return
     */
    public CustomMediaPlayer.Status getStatus(){
        return mMediaPlayer.getmStatus();
    }

    /**
     * 数据元准备完毕的回调,可以直接启动start开始播放
     */
    @Override
    public void onPrepared() {
        start();
    }

    /**
     * 播放完成事件
     */
    @Override
    public void omCompleted() {
        //发送播放完成事件,逻辑类型事件
        EventBus.getDefault().post(new AudioCompleteEvent());
    }

    @Override
    public void onError() {
        //发送当次播放实败事件,逻辑类型事件
        EventBus.getDefault().post(new AudioErrorEvent());
    }

    /**
     * 获取当前音乐总时长,更新进度用
     */
    public int getDuration() {
        if (getStatus() == CustomMediaPlayer.Status.STARTED
                || getStatus() == CustomMediaPlayer.Status.PAUSED) {
            return mMediaPlayer.getDuration();
        }
        return 0;
    }

    /**呼气当前的播放进度
     * @return
     */
    public int getCurrentPosition() {
        if (getStatus() == CustomMediaPlayer.Status.STARTED
                || getStatus() == CustomMediaPlayer.Status.PAUSED) {
            return mMediaPlayer.getCurrentPosition();
        }
        return 0;
    }

    /**设置音量,
     * @param left 左声道 0-1.0f
     * @param right 右声道 0-1.0f
     */
    public void setVolumn(float left, float right) {
        if (mMediaPlayer != null) mMediaPlayer.setVolume(left, right);
    }
}

这个类内部做的事情就比较多了:

  • 1.监听MediaPlayer当前状态
    如果是prepare状态:则启动内部的start方法,这个方法我将其设置为了private,说明是一个内部方法。
    监听当前播放位置,并回调给业务层。
  • 2.对外提供:load,pause,resume,getCurrentPosition获取当前播放位置等接口。

这些方法会对我们当前播放器状态做判断:
如调用pause,一定要当前状态是start状态
调用resume,一定要当前状态是pause等。
这些状态都是MediaPlayer统一管理的,不能乱设置。

  • 3.使用EventBus对外发送一些内部状态事件:

包括:AudioLoadEvent AudioErrorEvent AudioPauseEvent AudioCompleted等事件
外部可以在需要的地方注册这些Event

  • 4.监听播放器焦点事件:如有电话来,通知或者其他获取焦点的事件,
    这些事件如何处理?
    1.重新获得焦点:则判断是不是系统行为的焦点事件:如果是则调用resume恢复播放
    2.永久失去焦点,如被其他播放器抢占了:则调用暂停pause事件
    3.短暂失去焦点:如有电话来了:也是暂停
    4.瞬间失去焦点:如通知:则将音量设置小一点即可

播放器焦点监听使用的是:AudioManager.OnAudioFocusChangeListener

  • 5.使用唤醒锁Wifi_lock:

唤醒锁可以让设备在息屏状态也能继续执行播放事件
Wifi_lock可以让设备在后台运行下载数据,
记得添加对应的权限:

<uses-permission android:name="android.permission.WAKE_LOCK"/>
可以看到我们的AudioPlayer内部执行逻辑还是比较多的,先慢慢消化下吧。
  • AudioController.java
/**
 * 控制播放逻辑类,注意添加一些控制方法时,要考虑是否需要增加Event,来更新UI
 */
public class AudioController {

    /**
     * 播放方式
     */
    public enum PlayMode {
        /**
         * 列表循环
         */
        LOOP,
        /**
         * 随机
         */
        RANDOM,
        /**
         * 单曲循环
         */
        REPEAT
    }
    private AudioPlayer mAudioPlayer;
    //播放队列,不能为空,不设置主动抛错
    private ArrayList<AudioBean> mQueue = new ArrayList<>();
    private int mQueueIndex = 0;
    private PlayMode mPlayMode = PlayMode.LOOP;

    private AudioController() {
        EventBus.getDefault().register(this);
        mAudioPlayer = new AudioPlayer();
    }

    /**设置歌曲列表
     * @param listBean
     */
    public void setmQueue(List<AudioBean> listBean) {
        setmQueue(listBean,0);
    }
    public void setmQueue(List<AudioBean> listBean,int index){
        if(mQueue == null) mQueue = new ArrayList<>();
        mQueue.addAll(listBean);
        mQueueIndex = index;
    }

    public ArrayList<AudioBean> getmQueue() {
        return mQueue;
    }

    public PlayMode getmPlayMode() {
        return mPlayMode;
    }

    public void setmPlayMode(PlayMode mPlayMode) {
        this.mPlayMode = mPlayMode;
        EventBus.getDefault().post(new AudioPlayModeEvent(mPlayMode));
    }



    /**
     * 队列头添加播放哥曲
     */
    public void addAudio(AudioBean bean) {
        this.addAudio(bean,0);
    }
    public void addAudio(AudioBean bean,int index){
        if (mQueue == null){
            mQueue = new ArrayList<>();
        }
        int queryId = queryBean(bean);
        if(queryId<=-1){
            //没添加过此id的歌曲,添加且直播番放
            addCustomBean(bean,index);
            setPlayIndex(index);
        }else{
            AudioBean currentBean = getNowPlaying();
            if(!currentBean.id.equals(queryId)){
                setPlayIndex(queryId);
            }
        }
    }

    /**
     * 对外提供获取当前播放时间
     */
    public int getNowPlayTime() {
        return mAudioPlayer.getCurrentPosition();
    }

    /**
     * 对外提供获取总播放时间
     */
    public int getTotalPlayTime() {
        return mAudioPlayer.getCurrentPosition();
    }

    /**
     * 对外提供的获取当前歌曲信息
     */
    public AudioBean getNowPlaying() {
        return getPlaying(mQueueIndex);
    }

    /**获取下一首AudioBean
     * @return
     */
    private AudioBean getNextPlaying() {
        switch (mPlayMode) {
            case LOOP:
                mQueueIndex = (mQueueIndex + 1) % mQueue.size();
                return getPlaying(mQueueIndex);
            case RANDOM:
                mQueueIndex = new Random().nextInt(mQueue.size()) % mQueue.size();
                return getPlaying(mQueueIndex);
            case REPEAT:
                return getPlaying(mQueueIndex);
        }
        return null;
    }

    /**获取前一个AudioBean
     * @return
     */
    private AudioBean getPreviousPlaying() {
        switch (mPlayMode) {
            case LOOP:
                mQueueIndex = (mQueueIndex + mQueue.size() - 1) % mQueue.size();
                return getPlaying(mQueueIndex);
            case RANDOM:
                mQueueIndex = new Random().nextInt(mQueue.size()) % mQueue.size();
                return getPlaying(mQueueIndex);
            case REPEAT:
                return getPlaying(mQueueIndex);
        }
        return null;
    }

    /**获取当前播放的AudioBean
     * @param index
     * @return
     */
    private AudioBean getPlaying(int index) {
        if (mQueue != null && !mQueue.isEmpty() && index >= 0 && index < mQueue.size()) {
            return mQueue.get(index);
        } else {
            throw new AudioQueueEmptyException("当前播放队列为空,请先设置播放队列.");
        }
    }

    /**设置当前播放列表
     * @param index
     */
    public void setPlayIndex(int index) {
        if (mQueue == null) {
            throw new AudioQueueEmptyException("当前播放队列为空,请先设置播放队列.");
        }
        mQueueIndex  = index;
        play();
    }

    /**
     * 加载当前index歌曲
     */
    public void play() {
        AudioBean bean = getPlaying(mQueueIndex);
        load(bean);
    }

    /**
     * 加载next index歌曲
     */
    public void next() {
        AudioBean bean = getNextPlaying();
        load(bean);
    }

    /**
     * 加载previous index歌曲
     */
    public void previous() {
        AudioBean bean = getPreviousPlaying();
        load(bean);
    }
    private void load(AudioBean bean) {
        mAudioPlayer.load(bean);
    }

    /**
     * 添加/移除到收藏
     */
    public void changeFavourite() {
        if (null != GreenDaoHelper.selectFavourite(getNowPlaying())) {
            //已收藏,移除
            GreenDaoHelper.removeFavourite(getNowPlaying());
            EventBus.getDefault().post(new AudioFavouriteEvent(false));
        } else {
            //未收藏,添加收藏
            GreenDaoHelper.addFavourite(getNowPlaying());
            EventBus.getDefault().post(new AudioFavouriteEvent(true));
        }
    }
    private void addCustomBean(AudioBean bean, int index) {
        if (mQueue == null) {
            throw new AudioQueueEmptyException("当前播放队列为空,请先设置播放队列.");
        }
        mQueue.add(index,bean);
    }

    /**查找bean在队列中的位置
     *  -1:表示每找到,找到返回索引位置
     * @return
     */
    private int queryBean(AudioBean bean) {
        if(mQueue==null){
            throw new AudioQueueEmptyException("当前队列为空");
        }
        return mQueue.indexOf(bean);
    }

    public void resume() {
        mAudioPlayer.resume();
    }

    public void pause() {
        mAudioPlayer.pause();
    }
    /**
     * 播放/暂停切换
     */
    public void playOrPause() {
        if (isStartState()) {
            pause();
        } else if (isPauseState()) {
            resume();
        }
    }
    public void release() {
        mAudioPlayer.release();
        EventBus.getDefault().unregister(this);
    }
    /*
     * 获取播放器当前状态
     */
    private CustomMediaPlayer.Status getStatus() {
        return mAudioPlayer.getStatus();
    }
    /**
     * 对外提供是否播放中状态
     */
    public boolean isStartState() {
        return CustomMediaPlayer.Status.STARTED == getStatus();
    }

    /**
     * 对外提提供是否暂停状态
     */
    public boolean isPauseState() {
        return CustomMediaPlayer.Status.PAUSED == getStatus();
    }

    //插放完毕事件处理
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onAudioCompleteEvent(
            AudioCompleteEvent event) {
        next();
    }

    //播放出错事件处理
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onAudioErrorEvent(AudioErrorEvent event) {
        next();
    }


    public static AudioController getInstance() {
        return AudioController.SingletonHolder.instance;
    }
    private static class SingletonHolder {
        private static AudioController instance = new AudioController();
    }
}

这个类是整个体系的核心类:起承上启下作用

1.内部维护了一个List的歌曲列表,一个当前索引index以及当前播放模式:
播放模式:
loop:顺序播放
random:随机播放
repeat:单曲循环

2.提供了很多外部方法:
play,pause,resume,next,prev等这些方法实际是将数据元传递给AudioPlayer执行,AudioPlayer传递给MediaPlayer执行。

3.接收MediaPlayer对外输出的事件,并执行对应的方法。对外输出一些Event:如播放模式的改变等

这个方法是核心类,但是好像做的事情并没有AudioPlayer多呢。。

  • MusicPlayerService.java
public class MusicPlayerService extends Service{
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new AudioProvider.Stub() {

            @Override
            public void setQueue(List<AudioBean> audioList) throws RemoteException {
                AudioController.getInstance().setmQueue(audioList);
                //初始化前台Notification
                NotificationHelper.getInstance().init(new NotificationHelper.NotificationHelperListener() {
                    @Override
                    public void onNotificationInit() {
                        //让服务处于前台,提高优先级,增加保活能力
                        startForeground(NotificationHelper.NOTIFICATION_ID,NotificationHelper.getInstance().getNotification());
                    }
                });
            }

            @Override
            public void addAudio(AudioBean bean) throws RemoteException {
                AudioController.getInstance().addAudio(bean);
            }

            @Override
            public void play() throws RemoteException {
                AudioController.getInstance().play();

            }

            @Override
            public void playNext() throws RemoteException {
                AudioController.getInstance().next();
            }

            @Override
            public void playPre() throws RemoteException {
                AudioController.getInstance().previous();
            }

            @Override
            public void pause() throws RemoteException {
                AudioController.getInstance().pause();
            }

            @Override
            public void resume() throws RemoteException {
                AudioController.getInstance().resume();
            }

            @Override
            public void release() throws RemoteException {
                AudioController.getInstance().release();
            }
        };
    }

    @Override
    public void onCreate() {
        EventBus.getDefault().register(this);
        registerBroadcastReceiver();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        EventBus.getDefault().unregister(this);
        AudioController.getInstance().release();
        unRegisterBroadcastReceiver();
    }

    private void registerBroadcastReceiver() {
        if (mReceiver == null) {
            mReceiver = new NotificationReceiver();
            IntentFilter filter = new IntentFilter();
            filter.addAction(NotificationReceiver.ACTION_STATUS_BAR);
            registerReceiver(mReceiver, filter);
        }
    }

    private NotificationReceiver mReceiver;
    private void unRegisterBroadcastReceiver() {
        if (mReceiver != null) {
            unregisterReceiver(mReceiver);
        }
    }
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onAudioLoadEvent(AudioLoadEvent event) {
        //更新notifacation为load状态
        NotificationHelper.getInstance().showLoadStatus(event.mAudioBean);
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onAudioPauseEvent(AudioPauseEvent event) {
        //更新notifacation为暂停状态
        NotificationHelper.getInstance().showPauseStatus();
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onAudioStartEvent(AudioStartEvent event) {
        //更新notifacation为播放状态
        NotificationHelper.getInstance().showPlayStatus();
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onAudioFavouriteEvent(AudioFavouriteEvent event) {
        //更新notifacation收藏状态
        NotificationHelper.getInstance().changeFavouriteStatus(event.isFavourite);
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onAudioReleaseEvent(AudioReleaseEvent event) {
        //移除notifacation
    }

    /**
     * 接收Notification发送的广播
     */
    public static class NotificationReceiver extends BroadcastReceiver {
        public static final String ACTION_STATUS_BAR =
                AudioHelper.getmContext().getPackageName() + ".NOTIFICATION_ACTIONS";
        public static final String EXTRA = "extra";
        public static final String EXTRA_PLAY = "play_pause";
        public static final String EXTRA_NEXT = "play_next";
        public static final String EXTRA_PRE = "play_previous";
        public static final String EXTRA_FAV = "play_favourite";

        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent == null || TextUtils.isEmpty(intent.getAction())) {
                return;
            }
            String extra = intent.getStringExtra(EXTRA);
            switch (extra) {
                case EXTRA_PLAY:
                    //处理播放暂停事件,可以封到AudioController中
                    AudioController.getInstance().playOrPause();
                    break;
                case EXTRA_PRE:
                    AudioController.getInstance().previous(); //不管当前状态,直接播放
                    break;
                case EXTRA_NEXT:
                    AudioController.getInstance().next();
                    break;
                case EXTRA_FAV:
                    AudioController.getInstance().changeFavourite();
                    break;
            }
        }
    }
}

我们看到这个类:

1.内部实现了一个AudioProvider.Stub类,这个类实现了很多外部调用常用方法;这么做的设计原则是为了把我们封装的代码方便第三方应用调用,就不必重复开发了。

2.注册了一个NotificationReceiver的广播类,这个类是用于我们的音乐服务和通知栏通讯。

3.注册了一些EventBus事件,这些事件可以用来监听当前MediaPlayer的状态,并对通知栏中的UI进行修改。

还有其他几个类我这边就不继续讲了:

总结下我们这个版本的事件流程图

事件流程图.png

我这边顺便封装了几个自定义View和一个播放器通知栏界面,效果如下:

  • 一个MusicBottomDialog的组件
/**
 * 底部弹出歌曲列表:
 * 1.功能:
 *  循环模式:
 *  收藏功能
 *  删除列表功能
 *  歌曲列表
    左边是一个圆形的可旋转专辑图片,使用的是之前封装的图片加载库加载的
 */
public class MusicBottomDialog extends BottomSheetDialog {

image.png

  • 这是通知栏的音乐播放组件:

image.png

  • 最后这儿是音乐播放单曲界面,可以左右滑动听其他歌曲

device-2022-07-22-003546.png

得益于前面我们封装的音乐播放组件,我们可以很快的开发出一个音乐播放界面,可以把重心放在界面的设计和渲染上面。

完整代码可以查看github上的Demo:
https://github.com/ByteYuhb/anna_music_app

总结

我们今天要封装的音乐播放组件在这个项目中的比重很大,算是该系列中的核心部分。希望大家可以从中获取到自己想要的东西。

到今天我们组件化实战项目已经开发了差不多一半了,后面还会对分享组件以及视频播放组件进行封装,以及对base库的封装,arout库的使用,让组件之间进行解耦
持续输出中。。

组件化开发.png

相关文章
|
2天前
|
编解码 数据库 Android开发
安卓应用开发:打造高效用户界面的五大技巧
【5月更文挑战第18天】在竞争激烈的应用市场中,一个流畅且直观的用户界面(UI)对于安卓应用的成功至关重要。本文将探讨五种提升安卓应用用户界面性能的技巧,包括合理布局设计、优化资源使用、利用硬件加速、内存管理以及响应式编程。通过这些方法,开发者可以创建出既美观又高效的应用体验,从而吸引和保留用户。
|
3天前
|
存储 设计模式 监控
88 PM撸代码之【Android四大基本组件】
88 PM撸代码之【Android四大基本组件】
9 0
|
3天前
|
达摩院 安全 Java
80 PM撸代码之Android【武侠讲封装、继承、多态】
80 PM撸代码之Android【武侠讲封装、继承、多态】
8 0
|
4天前
|
JSON Android开发 数据格式
Android框架-Google官方Gson解析,android开发实验报告总结
Android框架-Google官方Gson解析,android开发实验报告总结
|
4天前
|
前端开发 Android开发
Android架构组件JetPack之DataBinding玩转MVVM开发实战(四)
Android架构组件JetPack之DataBinding玩转MVVM开发实战(四)
Android架构组件JetPack之DataBinding玩转MVVM开发实战(四)
|
4天前
|
安全 Linux Android开发
Android最强保活黑科技的最强技术实现,2024年最新阿里资深Android开发带你搞懂Framework
Android最强保活黑科技的最强技术实现,2024年最新阿里资深Android开发带你搞懂Framework
Android最强保活黑科技的最强技术实现,2024年最新阿里资深Android开发带你搞懂Framework
|
4天前
|
算法 前端开发 Android开发
Android文字基线Baseline算法的使用讲解,Android开发面试题
Android文字基线Baseline算法的使用讲解,Android开发面试题
Android文字基线Baseline算法的使用讲解,Android开发面试题
|
Android开发 数据格式 XML
|
4天前
|
设计模式 算法 前端开发
Android面经分享,失业两个月,五一节前拿到Offer,设计思想与代码质量优化+程序性能优化+开发效率优化
Android面经分享,失业两个月,五一节前拿到Offer,设计思想与代码质量优化+程序性能优化+开发效率优化
|
4天前
|
XML Android开发 数据格式
ConstraintLayout 2,Android高级开发面试
ConstraintLayout 2,Android高级开发面试