转载请注明出处: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():播放错误时的回调方法。
三、 动画相关
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)关于初始进入音乐界面问题:
源码中,初始进入音乐界面即开始播放,个人认为是不符合用户体验的。改进的方法是,设置初始进入的标志位,初始进入音乐界面时由用户点击播放按钮才能播放音乐。(当然,这里初始进入更新页面显示信息也是必须的,为了解决这个问题,我是让初始进入的时候暂停播放,由用户控制来重新播放)。