最近公司有个项目需要做一个音频的播放列表,类似应用市场的下载列表,每一个item都是一个音乐播放器能单独播放,单独的进度。拿到需求的时候脑子回忆当前有使用的音乐播放app,里面的实现也没有这么的场景里过了一遍怎么实现。脑海里下意识的就想到2种实现方案。
1:每个item都是一个独立的音乐播放控件,自定义一个view。 view里面包含了一个mediaplay,和播放,暂停,进度条。每个item都由对应的mediaplay独立控制,实现时就可以不关心对应item的数据实时刷新是否正确问题。但是疑问点是:android里可以同时存在多个mediaplay么?如果可以,每个item都持有一个mediaplay,当recyclerview列表上下滚动时,由于控件的复用时,当第一个正在播放的item还在刷新渲染UI,移动翻滚出显示区域后,最新出现的item调用onBindViewHolder时,重新对item初始时设置的初始数据,又会有什么影响?会不会影响到该item所持有的mediaplay。比如当前mediaplay线程是正在进行的,虽然onBindViewHolder调用了设置了值,但是没有触发到重新启动mediaplay播放,而mediaplay所在的item又是复用的View内所持有的mediaplay,而它正在播放,所以它会刷新渲染所持有它的View的UI。那这时候新出现的item未播放,但是它的ui却是在进行运动的,那就出问题了。 又或者onBindViewHolder 重新调用服用时,由于复用的item重新设值,当前正在播放的音乐停止的这样场景。
2:全局只有一个mediaplay,而列表里只是有UI,当所有的按钮操作只是向mediaplay发送 操作事件,而全局mediaplay 接收到对应的事件后,对事件做相应的响应,并且实时的监听mediaplay的状态。不断的向列表adapter发送事件重新绘制对应的item的ui重绘请求,这样比较容易保证数据的统一。而且通过观察市面上的音乐播放主流app,估计他们也是使用这样实现。所有我也使用这样的方案来实现。
音乐播放器需要使用的到mediaplay,很久以前写过一个简易的播放器。不过都过了好久忘记了。所有需要重新回炉下就BD下相关的资料。去github上搜索了些开源的demo,直接根据星星数来排列筛选。排名第一的就是这个Exoplayer。好奇去了解了下。EXOPlayer是Google官方开源的一种播放器官方介绍 ,能够支持DASH, SmoothStreaming 和 HLS,不能支持Adobe的rtsp、rtmp。功能还停强大,视频,音频都可以。我这里需要处理的是列表的音频播放。具体的单个简单播放,导入包之类我就不提了,我这里给个连接大家自己去了解下:https://www.jianshu.com/p/cb1e79267319。
添加依赖
先在应用app模块的build.gradle文件中添加ExoPlayer库的依赖:
音频创建与播放
```
controlDispatcher =new com.google.android.exoplayer2.DefaultControlDispatcher();
//获取player的一个实例,大多数情况可以直接使用 DefaultTrackSelector
// DefaultTrackSelector 该类可以对当前播放的音视频进行操作,比如设置音轨,设置约束曲目选择,禁用渲染器
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context,new DefaultTrackSelector());
defaultDataSourceFactory =new DefaultDataSourceFactory(context,"audio/mpeg");
//创建一个媒体连接源
ConcatenatingMediaSource concatenatingMediaSource =new ConcatenatingMediaSource();
concatenatingMediaSource.addMediaSources(sourceList);
simpleExoPlayer.setPlayWhenReady(false);
final ExtractorMediaSource mediaSource =new ExtractorMediaSource.Factory(defaultDataSourceFactory)
//创建一个播放数据源
.createMediaSource(Uri.parse("http://5.595818.com/2015/ring/000/140/6731c71dfb5c4c09a80901b65528168b.mp3"));
simpleExoPlayer.prepare(mediaSource);
```
改造开始
在熟悉了简单的音频播放后。我们需要对demo进行改造,AudioControl是核心控制类,并且一个Activity只有一个AudioControl对象,保证Activity开启关闭时对其进行初始化和资源的释放将SimpleExoPlayer对象放入AudioControl内,首先构造函数进行初始化,主要是生成Player实例。
Player对象实例
```
private void init() {
if(handler==null){
handler=new Handler();
}
formatBuilder =new StringBuilder();
formatter =new Formatter(formatBuilder, Locale.getDefault());
adGroupTimesMs =new long[0];
playedAdGroups =new boolean[0];
extraAdGroupTimesMs =new long[0];
extraPlayedAdGroups =new boolean[0];
period =new Timeline.Period();
window =new Timeline.Window();
controlDispatcher =new com.google.android.exoplayer2.DefaultControlDispatcher();
//获取player的一个实例,大多数情况可以直接使用 DefaultTrackSelector
// DefaultTrackSelector 该类可以对当前播放的音视频进行操作,比如设置音轨,设置约束曲目选择,禁用渲染器
simpleExoPlayer = ExoPlayerFactory.newSimpleInstance(context,new DefaultTrackSelector());
defaultDataSourceFactory =new DefaultDataSourceFactory(context,"audio/mpeg");
//创建一个媒体连接源
ConcatenatingMediaSource concatenatingMediaSource =new ConcatenatingMediaSource();
concatenatingMediaSource.addMediaSources(sourceList);
simpleExoPlayer.setPlayWhenReady(false);
initListener();
}
```
Player.EventListener监听
增加Player对象的事件监听,主要是要对Player启动监听,当Player启动后,Handler开启定时轮询线程loadStatusRunable,该线程主要是为了当每次轮询时,获得当前时刻的player的数据,比如:当前播放时间量,总时长,当前已经缓冲数据时间量,播放状态,需要注意的是,获得的时间单位均是毫秒数。 loadStatusRunable代码如下:
```
Runnable loadStatusRunable=new Runnable() {
@Override
public void run() {
long durationUs =0;
int adGroupCount =0;
long currentWindowTimeBarOffsetMs =0;
Timeline currentTimeline =simpleExoPlayer.getCurrentTimeline();
if (!currentTimeline.isEmpty()) {
int currentWindowIndex =simpleExoPlayer.getCurrentWindowIndex();
int firstWindowIndex = currentWindowIndex;
int lastWindowIndex = currentWindowIndex;
for (int i = firstWindowIndex; i <= lastWindowIndex; i++) {
if (i == currentWindowIndex) {
currentWindowTimeBarOffsetMs = C.usToMs(durationUs);
}
currentTimeline.getWindow(i,window);
if (window.durationUs == C.TIME_UNSET) {
// /**/ Assertions.checkState(!multiWindowTimeBar);
break;
}
for (int j =window.firstPeriodIndex; j <=window.lastPeriodIndex; j++) {
currentTimeline.getPeriod(j,period);
int periodAdGroupCount =period.getAdGroupCount();
for (int adGroupIndex =0; adGroupIndex < periodAdGroupCount; adGroupIndex++) {
long adGroupTimeInPeriodUs =period.getAdGroupTimeUs(adGroupIndex);
if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) {
if (period.durationUs == C.TIME_UNSET) {
// Don't show ad markers for postrolls in periods with unknown duration.
continue;
}
adGroupTimeInPeriodUs =period.durationUs;
}
long adGroupTimeInWindowUs = adGroupTimeInPeriodUs +period.getPositionInWindowUs();
if (adGroupTimeInWindowUs >=0 && adGroupTimeInWindowUs <=window.durationUs) {
if (adGroupCount ==adGroupTimesMs.length) {
int newLength =adGroupTimesMs.length ==0 ?1 :adGroupTimesMs.length *2;
adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength);
playedAdGroups = Arrays.copyOf(playedAdGroups, newLength);
}
adGroupTimesMs[adGroupCount] = C.usToMs(durationUs + adGroupTimeInWindowUs);
playedAdGroups[adGroupCount] =period.hasPlayedAdGroup(adGroupIndex);
adGroupCount++;
}
}
}
durationUs +=window.durationUs;
}
}
durationUs = C.usToMs(window.durationUs);
long curtime = currentWindowTimeBarOffsetMs +simpleExoPlayer.getContentPosition();
long bufferedPosition = currentWindowTimeBarOffsetMs +simpleExoPlayer.getContentBufferedPosition();
if(listener!=null){
listener.setCurTimeString(position,""+Util.getStringForTime(formatBuilder,formatter, curtime));
listener.setDurationTimeString(position,""+Util.getStringForTime(formatBuilder,formatter, durationUs));
listener.setBufferedPositionTime(position,bufferedPosition);
listener.setCurPositionTime(position,curtime);
listener.setDurationTime(position,durationUs);
}
handler.removeCallbacks(loadStatusRunable);
int playbackState=simpleExoPlayer==null?Player.STATE_IDLE:simpleExoPlayer.getPlaybackState();
//播放器未开始播放后者播放器播放结束
if(playbackState!=Player.STATE_IDLE&&playbackState!=Player.STATE_ENDED){
long delayMs=0;
//当正在播放状态时
if(simpleExoPlayer.getPlayWhenReady()&&playbackState==Player.STATE_READY){
float playBackSpeed =simpleExoPlayer.getPlaybackParameters().speed;
if(playBackSpeed<=0.1f){
delayMs=1000;
}else if(playBackSpeed<=5f){
//中间更新周期时间
long mediaTimeUpdatePeriodMs=1000 / Math.max(1, Math.round(1 / playBackSpeed));
//当前进度时间与中间更新周期之间的多出的不足一个中间更新周期时长的时间
long surplusTimeMs = curtime % mediaTimeUpdatePeriodMs;
//播放延迟时间
long mediaTimeDelayMs=mediaTimeUpdatePeriodMs-surplusTimeMs;
if(mediaTimeDelayMs<(mediaTimeUpdatePeriodMs/5)){
mediaTimeDelayMs+=mediaTimeUpdatePeriodMs;
}
delayMs=playBackSpeed==1?mediaTimeDelayMs:(long) (mediaTimeDelayMs/playBackSpeed);
}else {
delayMs=200;
}
}else {
//当暂停状态时
delayMs=1000;
}
handler.postDelayed(this,delayMs);
}else {
if(listener!=null){
//播放完结
listener.isPlay(position,false);
}
}
}
};
```
增加AutioControlListener供外部监听AutioControl的使用状态
在设计AutioControl支在播放音频时,外部需要知道内部的运行情况用以实时的刷新对应的UI显示。这时候就需要增加一个监听器AutioControlListener 用以提供外部对AutioControl的监听。只需要在定时轮询的loadStatusRunable线程中获得到对应的数据后调用即可
···
public interface AutioControlListener{
//当前播放时长
void setCurPositionTime(int position,long curPositionTime);
//总时长
void setDurationTime(int position,long durationTime);
//当前缓冲时长
void setBufferedPositionTime(int position,long bufferedPosition);
//当前播放时长的字符串显示
void setCurTimeString(int position,String curTimeString);
//是否正在播放
void isPlay(int position,boolean isPlay);
//当前音频的总时长字符串显示
void setDurationTimeString(int position,String durationTimeString);
}
···
以上AudioControl的实现就已经基本完成大部分的工作了,剩下的就是在列表内如何处理这些监听的回调处理UI了,由于过往开发中RecyclerView使用到了 BaseRecyclerViewAdapterHelper框架,所以这次为了简化Adapter也是用到了这个
dependencies{
implementation'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.31'
}
创建MediaEntity实体
创建MyBaseAdapter
为什么要从外部注入AutioControl,因为实例AutioControl需要一个上下文对象,而在MyBaseAdapter由于BaseQuickAdapter框架的原因,虽然有上下文对象,但是在最初的构造方法里,上下文是不存在还没有生成或者注入的,为NULL.具体的源代码没有去深入了解。不做深究。所以在Activity里实例AutioControl对象,再将AutioControl对象在MyBaseAdapter实例时当做参数注入进去。
TimeBar的使用
谷歌在Exoplayer内置一个进度条控件。可根据自定义属性来修改样式,比如进度条颜色,大小,当前进度颜色,剩余进度颜色,当前刻度圆大小颜色。或可制定自定义icon图片,缓冲进度颜色等。基本能满足当前所需要的所有的进度显示样式。TimeBar的设置刻度单位也是毫秒数,与Player相配套一起使用的
UI刷新
准备好上面的所有时候基本算是已经完成。剩下的就是在Adapter内实现AutioControlListener的抽象方法。在Player实例成功并且启动后启动无限定时轮询。在时间间隔内不断的获得当前Player的播放状态和播放进度。回调通知Adapter。adapter根据回调的方法参数做相应的Item UI刷新。
最主要的是数据UI的初始化方法。当我们点击一首歌的时候对应下标的UI在刷新显示。这时候我们点击别的歌的时候 原先播放的UI应该恢复到初始播放状态。新歌的条目的UI应该开始跑动
这个方法只在点击歌曲条目的播放按钮时候调用,上面的playIndex 是当前正在播放的音乐的下标,clickindex是点击新歌曲播放按钮时候新歌曲的音乐条目下标。需要注意的是,这个地方才用了播放和暂时是同一个位置大小相同,互斥显示的2个按钮。这样保证一个按钮做一件事。而且播放的音乐和暂停的音乐下标,回调函数都已经告诉我们了。省得只是一个按钮显示播放与暂停增加一个boolean变量做判断,在列表内的状态的保存。
每次点击播放时候都是先初始化UI,再启动播放,往autioContrl内传入歌曲URL地址。启动时候,在AudioControl内保存当前播放的歌曲的下标。
最后别忘记了在Activity关闭时候release吧AudioControl内的PlayerRelease了
以上就是实现音频列表UI刷新的整个实现流畅。集体代码已经上传到Github上。有需要的朋友可以去下载集体了解下。完
地址:https://github.com/dingdingww/AudioList