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

相关文章
|
14天前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
40 19
|
15天前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
41 14
|
14天前
|
开发框架 Android开发 iOS开发
安卓与iOS开发中的跨平台策略:一次编码,多平台部署
在移动应用开发的广阔天地中,安卓和iOS两大阵营各占一方。随着技术的发展,跨平台开发框架应运而生,它们承诺着“一次编码,到处运行”的便捷。本文将深入探讨跨平台开发的现状、挑战以及未来趋势,同时通过代码示例揭示跨平台工具的实际运用。
|
15天前
|
搜索推荐 前端开发 测试技术
打造个性化安卓应用:从设计到开发的全面指南
在这个数字时代,拥有一个定制的移动应用不仅是一种趋势,更是个人或企业品牌的重要延伸。本文将引导你通过一系列简单易懂的步骤,从构思你的应用理念开始,直至实现一个功能齐全的安卓应用。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你提供必要的工具和知识,帮助你将创意转化为现实。
|
Android开发 数据格式 XML
|
27天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
1月前
|
缓存 前端开发 Android开发
安卓开发中的自定义视图:从零到英雄
【10月更文挑战第42天】 在安卓的世界里,自定义视图是一块画布,让开发者能够绘制出独一无二的界面体验。本文将带你走进自定义视图的大门,通过深入浅出的方式,让你从零基础到能够独立设计并实现复杂的自定义组件。我们将探索自定义视图的核心概念、实现步骤,以及如何优化你的视图以提高性能和兼容性。准备好了吗?让我们开始这段创造性的旅程吧!
26 1
|
1月前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
27天前
|
IDE Java 开发工具
移动应用与系统:探索Android开发之旅
在这篇文章中,我们将深入探讨Android开发的各个方面,从基础知识到高级技术。我们将通过代码示例和案例分析,帮助读者更好地理解和掌握Android开发。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧。让我们一起开启Android开发的旅程吧!
|
18天前
|
Java Linux 数据库
探索安卓开发:打造你的第一款应用
在数字时代的浪潮中,每个人都有机会成为创意的实现者。本文将带你走进安卓开发的奇妙世界,通过浅显易懂的语言和实际代码示例,引导你从零开始构建自己的第一款安卓应用。无论你是编程新手还是希望拓展技术的开发者,这篇文章都将为你打开一扇门,让你的创意和技术一起飞扬。