SimplifyReader源码学习:(一)音乐播放功能总结


转载请注明出处:http://blog.csdn.net/qq_32199531/article/details/51438767 谢谢!

写在前面:
SimplifyReader是我第一个用心研究源码的app,在这里首先感谢开源代码的分享者。 这也是我的第一篇博客,希望可以记录下来学习中的经验和总结。也欢迎大家指正错误,共同进步。
最后,分享给大家最近读到的两句话:
我已亭亭,无忧亦无惧。
趁秋雨还未滴落,趁风霜还未曾侵蚀。


一、 概述

基本功能:开始播放、暂停播放、重新播放、播放下一首、播放前一首、跳转进度播放、停止播放。
附加功能:页面信息更新、进度条显示、相关动画(needle和disc动画)。

MVP框架模式在SimplifyReader中的应用:
SimplifyReader采用MVP架构(不了解的可以参考:http://blog.csdn.net/feiduclear_up/article/details/46374653)。

View:
MusicsFragment(实现了MusicsView接口)负责页面显示、响应用户操作功能,一般调用Presenter的方法,具体处理逻辑在Presenter中实现。

Presenter:
MusicsPresenterImpl实现了MusicsPresenter、BaseMultiLoadedListener接口。它是View和Model的桥梁,进行一些逻辑处理。

Model:
Model主要提供业务数据。MusicsInteractorImpl的getMusicListData可以获取音乐播放功能所需要的音乐信息,并通过BaseMultiLoadedListener接口回调数据给Presenter(MusicsPresenterImpl)。

二、 MediaPlayer介绍

音乐播放功能涉及的一个很重要的类就是MediaPlayer,其生命周期如下图所示。当一个MediaPlayer对象被刚刚用new操作符创建或是调用了reset()方法后,它就处于Idle状态。当调用了release()方法后,它就处于End状态。这两种状态之间是MediaPlayer对象的生命周期。在SimplifyReader的MusicPlayer中使用了这个类。

MediaPlayer的生命周期可参考http://blog.csdn.net/ddna/article/details/5178864,我这里只简单介绍下在MusicPlayer中是如何使用的。
MusicPlayer实现了MediaPlayer的四个listener接口,覆写了相关回调方法:
1)onCompletion():音乐播放完成时回调,根据播放模式(单曲循环、顺序播放、…)进行相应处理。
2)onPrepared():进入prepared状态时的回调方法。一般进行start()操作、广播当前播放音乐的信息和播放进度等。
3)onBufferingUpdate():网络流状态改变时回调方法。这里一般广播缓冲进度。View中接收广播,实时更新音乐播放缓冲进度。(这里源码中有错误,可改进。)
4)onError():播放错误时的回调方法。

MediaPlayer生命周期

三、 动画相关

SimplifyReader音乐播放界面仿网易云音乐的播放界面,包括两个动画效果,唱针动画和唱片动画。其主要方法如下:

//开启Needle(唱针)动画
private void startNeedleAnimator() {
    if (isPlaying) {
        mNeedleAnimator = ObjectAnimator.ofFloat(mNeedle, "rotation", 0, NEEDLE_ROTATE_CIRCLE);//-30  表示逆时针旋转30度
    } else {
        mNeedleAnimator = ObjectAnimator.ofFloat(mNeedle, "rotation", NEEDLE_ROTATE_CIRCLE, 0);//从-30度的位置旋转到0度的位置
    }
    mNeedleAnimator.setDuration(NEEDLE_ANIMATOR_TIME);//350ms
    mNeedleAnimator.setInterpolator(new DecelerateInterpolator());//设置旋转速率为减速模式
    if (mNeedleAnimator.isRunning() || mNeedleAnimator.isStarted()) {//取消之前正在或已经start而将要animate的animator
        mNeedleAnimator.cancel();
    }
    mNeedleAnimator.start();
}

//开启中间圆盘(唱片)转动
private void startDiscAnimator(float animatedValue) {
    mDiscLayoutAnimator = ObjectAnimator.ofFloat(mDiscLayout, "rotation", animatedValue, 360 + animatedValue);//顺时针旋转一周
    //对动画的过程进行监听
    mDiscLayoutAnimator.addUpdateListener(new AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator arg0) {
            mDiscLayoutAnimatorValue = (Float) arg0.getAnimatedValue();//获得圆盘实时的旋转角度
        }
    });
    mDiscLayoutAnimator.setDuration(DISC_ANIMATOR_TIME);
    mDiscLayoutAnimator.setRepeatCount(DISC_ANIMATOR_REPEAT_COUNT); //这里设置为-1   表示不停止
    //设置旋转速率  这里为匀速效果
    mDiscLayoutAnimator.setInterpolator(new LinearInterpolator());

    if (mDiscLayoutAnimator.isRunning() || mDiscLayoutAnimator.isStarted()) {
        mDiscLayoutAnimator.cancel();
    }
    mDiscLayoutAnimator.start();
}

//中间圆盘(唱针)返回到最初的位置
private void reverseDiscAnimator() {
    mDiscLayoutAnimator = ObjectAnimator.ofFloat(mDiscLayout, "rotation", mDiscLayoutAnimatorValue, 360);//顺时针回到原点
    mDiscLayoutAnimator.addUpdateListener(new AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator arg0) {
            mDiscLayoutAnimatorValue = (Float) arg0.getAnimatedValue();
        }
    });
    mDiscLayoutAnimator.setDuration(DISC_REVERSE_ANIMATOR_TIME);
    mDiscLayoutAnimator.setInterpolator(new AccelerateInterpolator());//加速模式
    if (mDiscLayoutAnimator.isRunning() || mDiscLayoutAnimator.isStarted()) {
        mDiscLayoutAnimator.cancel();
    }
    mDiscLayoutAnimator.start();
}

四、 MusicPlayService

MusicPlayService主要有两个功能。
其一,监听手机电话状态,并通过EventBus通知UI线程。通过PhoneCallReceiver监听手机外拨电话,PhoneStateChangedListener监听来电状态、摘机状态和空闲状态。
其二,响应音乐播放指令。通过PlayBroadCastReceiver接收View中广播的音乐播放指令,并响应相关指令。而具体指令的实现则由MusicPlayer完成。

五、 图片加载和高斯模糊处理

SimplifyReader音乐播放功能中Disc(圆盘部分)背景图片的加载用到了ImageLoader的displayImage方法。这里不详细说明。
音乐播放界面背景图片是对Disc背景图片进行高斯模糊处理得到的。关键代码如下:

Bitmap bitmap = ImageBlurManager.doBlurJniArray(loadedImage, BLUR_RADIUS, false);
mBackgroundImage.setImageBitmap(bitmap);

而这里的doBlurJniArray对应的代码如下:

public static Bitmap doBlurJniArray(Bitmap sentBitmap, int radius, boolean canReuseInBitmap) {
        Bitmap bitmap;
        if (canReuseInBitmap) {
            bitmap = sentBitmap;
        } else {
            //此处返回一个与原图同像素的图,且像素可修改
            bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); 
        }
        if (radius < 1) {
            return (null);
        }
        int w = bitmap.getWidth();//返回位图的宽度。
        int h = bitmap.getHeight();//返回位图的高度值。
        int[] pix = new int[w * h];
        bitmap.getPixels(pix, 0, w, 0, 0, w, h);
        // Jni array calculate   此处通过jni调用C实现模糊效果  radius为模糊系数
        //所谓"模糊",可以理解成每一个像素都取周边像素的平均值。 所以radius越大  模糊效果越好
        ImageBlur.blurIntArray(pix, w, h, radius);
        bitmap.setPixels(pix, 0, w, 0, 0, w, h);
        return (bitmap);
    }

六、 MusicsFragment主要方法说明:

1)onFirstUserVisible():
在onActivityCreated中调用,fragment第一次可见时加载。这里主要是显示缓冲进度条(注:此进度条在加载完成进入prepared状态后经发送歌曲信息的广播调用refreshPageInfo而隐藏)和加载音乐列表。

2)onActivityCreated():
这里主要注册广播接收器(音乐信息广播接收、播放进度广播接收、缓冲进度广播接收),开启MusicPlayService服务。

3)onDetach():
这里对所有的广播接收器取消注册(动态注册的广播接收器必须取消注册),并调用onStopPlay()停止播放音乐。

4)initViewsAndEvents():
在onViewCreated()中调用,进行背景图片初始化和控件Listener绑定等操作。

5)onEventComming():
接收来自MusicPlayService的EventBus通知,并根据手机电话状态来开始或停止播放。

6)refreshMusicsList():
此方法在MusicsPresenter.loadListData()加载成功的回调方法中调用(EVENT_REFRESH_DATA参数)。当首次进入音乐播放界面时加载成功会调用该方法。开始播放音乐。

7)addMoreMusicsList():
此方法在MusicsPresenter.loadListData()加载成功的回调方法中调用(EVENT_LOAD_MORE_DATA参数)。一般当点击PlayNext按钮并加载成功的时候调用。

8)rePlayMusic():暂停后重新播放。

9)startPlayMusic():开始播放。

10)pausePlayMusic():暂停播放。

11)stopPlayMusic():停止播放,一般在onDetach方法中调用。

12)playNextMusic():播放下一首。

13)playPrevMusic():播放前一首。

14)seekToPosition():跳转至指定位置播放。

15)refreshPageInfo():
在prepared状态,接收到音乐信息的广播后调用。用于页面信息更新,包括歌曲名、歌手名、背景图片和Disc背景图片的更新显示。

16)refreshPlayProgress():
当接收到播放进度的广播时调用,实时更新播放进度。

17)refreshPlaySecondProgress():
当接收到缓冲进度的广播时调用,更新歌曲缓冲进度。

18)calss PlayBundleBroadCast
播放的音乐信息的广播接收器,在这里调用refreshPageInfo()更新页面信息。一般在onPrepared()中发送广播。

19)class PlayPositionBroadCast
播放进度的广播接收器,在这里调用refreshPlayProgress()更新播放进度。一般在onPrepared()中发送广播(这里的发送广播在Thread进行,每隔1s发送一次)。

20)PlaySecondProgressBroadCast
缓冲进度的广播接收器,在这里调用refreshPlaySecondProgress()更新缓冲进度。一般在onBufferingUpdate中发送广播(这里源码的方法待改进,具体见个人改进建议)。

七、 主要操作调用顺序:

1)初始进入音乐播放界面:
说明:我这里对源码进行了一些修改,增加或删减了一些方法,大家只关注生命周期就好。
initViewsAndEvents() → onFirstUserVisible() → loadListData() → onActivityCreated() →
refreshMusicsList() → startPlayMusic() → pause() → onPrepared() → sendPlayBundle() → sendPlayCurPosition() → refreshPageInfo()
这里的pause() 是因为首次进入页面,须由用户控制是否播放。但并不影响页面更新。
这里写图片描述

2)点击播放:
onClick: ctrBtn clicked → rePlayMusic() → 开启动画
这里写图片描述

3)点击暂停:
onClick: ctrBtn clicked → pausePlayMusic() → 关闭动画
这里写图片描述

4)再次播放:
onClick: ctrBtn clicked → rePlayMusic() → 开启动画
这里写图片描述

5)点击PlayNext:
click next button → addMoreMusicsList()→ refreshMusicsList → playNext() →prepareMusic() → onPrepared() → refreshPageInfo() → 开启动画
这里写图片描述

6)点击PlayPrev:
click prev button → playPrevMusic()→ 停止动画→ playPrev() →prepareMusic() → onPrepared() → refreshPageInfo() → 开启动画
这里写图片描述

7)点击back键
onDetach() → stopPlayMusic()
这里写图片描述

八、 个人改进建议:

1)关于音乐缓冲进度显示:
源码中在onBufferingUpdate()回调方法中当缓冲进度有更新时发送广播,在MusicsFragment中接收广播,并进行进度更新。然而我发现app中并没有显示缓冲进度条。仔细查看发现,onBufferingUpdate()回调方法中的进度指的是占歌曲全部时间的百分比(是0-100之间的数值),而进度更新PlayerSeekBar.setSecondaryProgress(progress)中的progress指的是实际的totalTime(以ms为单位,所以是一个很大的值)。因而会发生那样的错误。以下为改进方法:

public void onBufferingUpdate(MediaPlayer mp, int percent) {
    if(percent<100){
        isbufferFinished = false;
        Intent intent = new Intent();
        intent.setAction(Constants.ACTION_MUSIC_SECOND_PROGRESS_BROADCAST);
        intent.putExtra(Constants.KEY_MUSIC_SECOND_PROGRESS, percent);
        context.sendBroadcast(intent);
   }else {//percent=100时只广播一次
        if (!isbufferFinished){
            isbufferFinished = true;
            Intent intent = new Intent();
            intent.setAction(Constants.ACTION_MUSIC_SECOND_PROGRESS_BROADCAST);
            intent.putExtra(Constants.KEY_MUSIC_SECOND_PROGRESS, percent);
            context.sendBroadcast(intent);
        }
}

public void refreshPlaySecondProgress(int progress) {
    //mTotalDuration为总时间
    mPlayerSeekBar.setSecondaryProgress(progress*mTotalDuration/100);
    }

2)关于跳转进度播放问题:
源码中虽然实现了seekTo()相关的函数,但跳转功能并未实现。究其原因发现源码并没有为seekBar绑定Listener,因而无法响应触摸跳转。改进如下:

mPlayerSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
    }
    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
    }
    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
        mMusicsPresenter.seekTo(seekBar.getProgress()*100/mTotalDuration);
    }
});

3)关于初始进入音乐界面问题:
源码中,初始进入音乐界面即开始播放,个人认为是不符合用户体验的。改进的方法是,设置初始进入的标志位,初始进入音乐界面时由用户点击播放按钮才能播放音乐。(当然,这里初始进入更新页面显示信息也是必须的,为了解决这个问题,我是让初始进入的时候暂停播放,由用户控制来重新播放)。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值