本章小节主要讲述音频播放器的页面相关内容。由于代码属于公司资产个人无权外发,而且代码牵扯的范围广泛也难以全部贴上来,所以只讲细节实现和小段代码。
背景:播放内容为专辑,一个专辑内有多首歌曲,功能有基础的歌曲列表、播放/暂停、上/下一首、收藏、设置播放倍速、单曲/循环/随机播放、进度条滑动 等等,我以自己的网易云为例功能相当
1、首先得有网,网络权限、网络监听不能少
scss
<uses-permission android:name="android.permission.INTERNET" />
IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(networkReceiver, filter, Context.RECEIVER_EXPORTED);
}else {
registerReceiver(networkReceiver, filter);
}
boolean isNetWork = false;
private BroadcastReceiver networkReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction())) {
isNetWork = JxwApplication.isNetwork();
if(isNetWork){ //有网
if (netWorkDialog != null && netWorkDialog.isShowing()) {
netWorkDialog.dismiss();//隐藏无网弹窗
}
}else {//无网
if(netWorkDialog!=null){
netWorkDialog.show(); //显示无网弹窗
}
animatorSet.pause(); //暂停封面旋转
}
}
}
};
2、封面旋转的动画
scss
AnimatorSet animatorSet;
private String book_cover; //专辑封面
//==========在onCreate 中===================
RequestOptions options = new RequestOptions();//图片圆角
Glide.with(ReadTextActivity.this).load(book_cover)
.placeholder(R.mipmap.img_med_player_placeholder) // 占位图资源ID
.error(R.mipmap.img_med_player_placeholder) // 错误占位图资源ID
.apply(options).into(mBinding.ivCover);
animatorSet = new AnimatorSet();
ObjectAnimator rotateAnimator = ObjectAnimator.ofFloat(mBinding.ivCover, "rotation", 0f, 360f);
rotateAnimator.setInterpolator(new LinearInterpolator());
rotateAnimator.setDuration(10000);
rotateAnimator.setRepeatCount(Animation.INFINITE);
rotateAnimator.setRepeatMode(ValueAnimator.RESTART);
animatorSet.playTogether(rotateAnimator);
animatorSet.start();
animatorSet.pause();
//====================播放====================
animatorSet.resume();
//====================暂停====================
animatorSet.pause();
//==========在onDestroy 中===================
if (animatorSet != null) {
animatorSet.cancel();
animatorSet = null;
}
3、播放中的变量有 专辑ID、专辑名称、是否收藏、专辑封面、循环类型、倍速、歌曲名称,其中倍速和循环类型设置完后需要用SP存储起来,在页面初始化中取用
arduino
private String book_cover; //专辑封面
private String book_name;
private int bookId;
private boolean isCollect = false; //是否收藏
private String unit_name; //正在播放的歌曲名称
public static float speed = 1f; //预选 播放速度
public static int loop_type = 0; // 0顺序播放 1单篇循环 2随机播放
public static boolean isplay = true; //播放状态
private String jsonList; //播放的歌曲列表数据
private int pre_gb_time = 0; //定时播放的时间 0/15/30/45分钟
4、播放消息的发送和监听,我这里使用的是EventBus,我初次写播放器时由于没有经验,播放服务于UI的交互用的监听,代码设计有不少缺陷容易造成内存泄漏以及回调地狱不便维护,采用EventBus通信是迭代版本后比较高效靠谱稳定的方案。 服务端的消息订阅和发生我放到下一个章节讲解
typescript
//引用
implementation 'org.greenrobot:eventbus:3.2.0'
//=============在onCreate() 中注册===============
EventBus.getDefault().register(this);
//=============在onDestroy()中注销===============
// 针对性移除所有粘性事件类型(与订阅方法对应)
EventBus.getDefault().removeStickyEvent(PlayTimeShowMsg.class);
EventBus.getDefault().removeStickyEvent(PoiToUIMsg.class);
EventBus.getDefault().removeStickyEvent(PlayUnitPoiMsg.class);
EventBus.getDefault().removeStickyEvent(PlayShowPoiMsg.class);
EventBus.getDefault().removeStickyEvent(PlayStateMsg.class);
EventBus.getDefault().removeStickyEvent(RequestMsg.class);
EventBus.getDefault().removeStickyEvent(PlayInfoMsg.class);
// 最后注销订阅
EventBus.getDefault().unregister(this);
//===========发送播放消息============
//封装成工具类 可从任何地方调用 比如通知栏或控制栏的自定义UI中实现控制播放
public class PlayControlMsg {
public static final String YYTS_ACTION_PAUSE = "YYTS_ACTION_PAUSE";//暂停
public static final String YYTS_ACTION_PLAY = "YYTS_ACTION_PLAY";//播放
public static final String YYTS_PLAY_NEXT = "YYTS_PLAY_NEXT";//播放下一个
public static final String YYTS_PLAY_PREVIOUS = "YYTS_PLAY_PREVIOUS";//播放上一个
public String msg;
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
public class PlayControlTools {
public static void setACTION_PAUSE(){//暂停
PlayControlMsg msg = new PlayControlMsg();
msg.setMsg(PlayControlMsg.YYTS_ACTION_PAUSE);
EventBus.getDefault().post(msg);
}
public static void setACTION_PLAY(){//播放
PlayControlMsg msg = new PlayControlMsg();
msg.setMsg(PlayControlMsg.YYTS_ACTION_PLAY);
EventBus.getDefault().post(msg);
}
public static void setPLAY_PREVIOUS(){ //上一个
PlayControlMsg msg = new PlayControlMsg();
msg.setMsg(PlayControlMsg.YYTS_PLAY_PREVIOUS);
EventBus.getDefault().post(msg);
}
public static void setPLAY_NEXT(){ //下一个
PlayControlMsg msg = new PlayControlMsg();
msg.setMsg(PlayControlMsg.YYTS_PLAY_NEXT);
EventBus.getDefault().post(msg);
}
}
//UI页滑动进度的 监听和消息发送
mBinding.seekbar1.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) {
// Log.e("TAG","进度条,当前值:"+seekBar.getProgress());
// mBinding.ivReadBf.setImageResource(R.mipmap.icon_pause);
PoiToPlayMsg.sendMsg(seekBar.getProgress()); //发送指针
}
});
//=============接收播放消息================
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receiveTime(PlayTimeShowMsg msg) { //接收时间显示 定时播放的倒计时
mBinding.tvPlayTime.setText(ConstantFd.FormatMiss(msg.getTime()));
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receiveProcess(PoiToUIMsg msg) { //接收进度
// Log.i("TAG", "process: "+msg.getPoi());
// Log.i("TAG", "getMax: "+msg.getMax());
mBinding.seekbar1.setMax((int)msg.getMax());
mBinding.seekbar1.setProgress((int)msg.getPoi());
if(msg.getPoi()<=msg.getMax()){
//将int类型 进度转为String的 为分:秒 显示
mBinding.tvNum1.setText(ConstantFd.FormatMiss(msg.getPoi()));
}else {
mBinding.tvNum1.setText(ConstantFd.FormatMiss(msg.getMax()));
}
mBinding.tvNum2.setText(ConstantFd.FormatMiss(msg.getMax()));
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receivePlayPoi(PlayUnitPoiMsg msg) { //接收单元指针
Log.i("TAG", "单元指针poi: "+msg.getPoi());
mUnitAdapter.mPosSomeTime = msg.getPoi(); //适配器播放指针更新
getCollectState(); //获取收藏状态
updatePlayUI(); //刷新列表ui
playOrPauseUI(); //播放ui和动画更新
mBinding.recyclerView.smoothScrollToPosition(msg.getPoi()); //列表滚动到指定位置
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receivePlayPoi(PlayShowPoiMsg msg) { //接收单元指针
Log.i("TAG", "单元指针poi: "+msg.getPoi());
mUnitAdapter.mPosSomeTime = msg.getPoi();//适配器播放指针更新
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receivePlayState(PlayStateMsg msg) { //接收播放状态
playOrPauseUI(); //播放状态ui更新
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receiveEventBus(RequestMsg msg) { //数据请求中
if(!isFinishing()){
mBinding.loading.setVisibility(View.VISIBLE);
}
}
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void receiveEventBus(PlayInfoMsg msg) {
mBinding.loading.setVisibility(View.GONE);
if(ConstantFd.unit_List.size() > 0 && mUnitAdapter!=null){
mUnitAdapter.updateData(ConstantFd.unit_List);//适配器数据更新
updatePlayUI(); //更新播放器ui 指针显示等
}
}
//================消息封装类===================
/**
* 歌曲播放进度
*/
public class PoiToPlayMsg { //UI->服务
long poi; //当前指针
public long getPoi() {
return poi;
}
public void setPoi(long poi) {
this.poi = poi;
}
public static void sendMsg(long poi){
PoiToPlayMsg msg = new PoiToPlayMsg();
msg.setPoi(poi);
EventBus.getDefault().post(msg); //发送指针
}
}
/**
* 歌曲播放进度
*/
public class PoiToUIMsg { //服务->UI
long poi; //当前指针
long max; //最大指针
public long getPoi() {
return poi;
}
public void setPoi(long poi) {
this.poi = poi;
}
public long getMax() {
return max;
}
public void setMax(long max) {
this.max = max;
}
public static void sendMsg(long poi, long max){
PoiToUIMsg msg = new PoiToUIMsg();
msg.setPoi(poi);
msg.setMax(max);
EventBus.getDefault().post(msg); //发送指针
}
}
//单个歌曲在列表中的指针和封面信息
public class PlayUnitPoiMsg {
int poi; //播放单元指针
String cover; //专辑封面
public int getPoi() {
return poi;
}
public void setPoi(int poi) {
this.poi = poi;
}
public String getCover() {
return cover;
}
public void setCover(String cover) {
this.cover = cover;
}
public static void sendPoi(int poi, String cover){
PlayUnitPoiMsg msg = new PlayUnitPoiMsg();
msg.setPoi(poi);
msg.setCover(cover);
EventBus.getDefault().post(msg); //发送指针
}
}
/**
* 播放时间设置
*/
public class PlayTimeSetMsg { //服务端接收,ui设置的整时间 0/10/30/45分钟的定时
long time;
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public static void sendMsg(long time){
PlayTimeSetMsg msg = new PlayTimeSetMsg();
msg.setTime(time);
EventBus.getDefault().post(msg); //发送状态
}
}
/**
* 播放时间设置
*/
public class PlayTimeShowMsg {//UI端接收的 倒计时剩余时间,每秒更新
long time;
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public static void sendMsg(long time){
PlayTimeShowMsg msg = new PlayTimeShowMsg();
msg.setTime(time);
EventBus.getDefault().post(msg); //发送状态
}
}
/**
* 播放状态 UI->播放服务 用于控制播放状态
*/
public class PlayStateMsg {
boolean isPlay;
public boolean isPlay() {
return isPlay;
}
public void setPlay(boolean play) {
isPlay = play;
}
public static void sendState(boolean isPlay){
PlayStateMsg msg = new PlayStateMsg();
msg.setPlay(isPlay);
EventBus.getDefault().post(msg); //发送状态
}
}
public class PlayShowPoiMsg { //服务发给ui 更新播放指针(歌曲)
int poi; //播放单元指针
public int getPoi() {
return poi;
}
public void setPoi(int poi) {
this.poi = poi;
}
public static void sendPoi(int poi){
PlayShowPoiMsg msg = new PlayShowPoiMsg();
msg.setPoi(poi);
EventBus.getDefault().post(msg); //发送指针
}
}
/**
* 播放设置倍速 ui发给服务端
*/
public class PlaySetMsg {
boolean isChange;
public boolean isChange() {
return isChange;
}
public void setChange(boolean change) {
isChange = change;
}
public static void sendMsg(boolean change){
PlaySetMsg msg = new PlaySetMsg();
msg.setChange(change);
EventBus.getDefault().post(msg); //发送状态
}
}
/**
* 播放数据改变 服务端发给ui 通知其刷新播放数据、进度条 在播放新的一首时触发
*/
public class PlayInfoMsg {
boolean isChange;
public boolean isChange() {
return isChange;
}
public void setChange(boolean change) {
isChange = change;
}
public static void sendInfo(boolean isChange){
PlayInfoMsg msg = new PlayInfoMsg();
msg.setChange(isChange);
EventBus.getDefault().post(msg); //发送通知
}
}
5、进度条int 转时分秒
ini
// 格式化 录音时长为 时:分:秒
public static String FormatMiss(long miss) {
String mm,ss;
if(((miss / 1000) % 3600) / 60 < 10){
mm = "0"+((miss / 1000) % 3600) / 60;
}else {
mm = ""+((miss / 1000) % 3600) / 60;
}
if((miss / 1000) % 60 < 10){
ss = "0"+(miss / 1000) % 60;
}else {
ss = ""+(miss / 1000) % 60;
}
return mm + ":" + ss;
}
6、列表适配器
ini
/**
* 列表 适配器
*/
public class PlayUnitAdapter extends RecyclerView.Adapter<PlayUnitAdapter.Holder> {
public List<ReadDataBean> mDatas = new ArrayList();
public Drawable[] mFrames; //列表上的播放动画
private int isFrom = 0; //来源 0 专辑列表 1磨耳朵首页 2 AI问答
public void setIsFrom(int from){
this.isFrom = from;
}
public void updateData(List<ReadDataBean> data) {
mDatas.clear();
mDatas.addAll(data);
notifyDataSetChanged();
}
public int mPosSomeTime = 0;//临时选中 正在播放的单元指针 这个变量极为重要
private Context mContext;
private View.OnClickListener mListener;
public PlayUnitAdapter(Context context, View.OnClickListener listener) {
this.mContext = context;
this.mListener = listener;
mFrames = new Drawable[]{
context.getResources().getDrawable(R.mipmap.img_bfdx1),
context.getResources().getDrawable(R.mipmap.img_bfdx2),
context.getResources().getDrawable(R.mipmap.img_bfdx3),
context.getResources().getDrawable(R.mipmap.img_bfdx4),
};
}
@NonNull
@Override
public Holder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new Holder(LayoutInflater.from(mContext).inflate(R.layout.item_unit, parent, false));
}
@Override
public void onBindViewHolder(@NonNull Holder holder, int position) {
holder.upateData(position);
}
@Override
public int getItemCount() {
return mDatas.size();
}
public class Holder extends RecyclerView.ViewHolder {
RelativeLayout rl_read;
TextView item_num, item_name, item_time;
ImageView img_play, img_playing;
public Holder(@NonNull View itemView) {
super(itemView);
rl_read = itemView.findViewById(R.id.rl_read);
item_num = itemView.findViewById(R.id.item_num);
item_name = itemView.findViewById(R.id.item_name);
item_time = itemView.findViewById(R.id.item_time);
img_play = itemView.findViewById(R.id.img_play);
img_playing = itemView.findViewById(R.id.img_playing);
itemView.setOnClickListener(mListener);
}
public void upateData(int position) {
itemView.setTag(position);
item_num.setText(position+1+"");
String unit = "";
if(mDatas.get(position).getShowName() != null){// 名称
unit = mDatas.get(position).getShowName();
}
item_name.setText(unit);
if(position==mPosSomeTime){ //播放中的背景
rl_read.setBackgroundResource(R.mipmap.img_med_play_list_bg);
item_name.setSelected(true);
}else {
rl_read.setBackgroundResource(R.drawable.shape_item_bg);
}
if(ConstantFd.isplay && position==mPosSomeTime){ //播放中
FrameAnimation.getInstance().setImageView(img_playing);
FrameAnimation.getInstance().setFrames(mFrames);
FrameAnimation.getInstance().startAnimation();
img_play.setVisibility(View.GONE);
img_playing.setVisibility(View.VISIBLE);
}else {
img_play.setVisibility(View.VISIBLE);
img_playing.setVisibility(View.GONE);
}
if(isFrom==2){ //AI问答过来 隐藏列表时间
item_time.setVisibility(View.GONE);
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) item_name.getLayoutParams();
params.topMargin = 20; // 单位是像素
item_name.setLayoutParams(params);
ViewGroup.MarginLayoutParams params2 = (ViewGroup.MarginLayoutParams) item_num.getLayoutParams();
params2.topMargin = 20; // 单位是像素
item_num.setLayoutParams(params2);
}else {
if(mDatas.get(position).getDuration() > 0){
int duration = mDatas.get(position).getDuration();
long time = duration * 1000;
item_time.setText(FormatMiss(time));
item_time.setVisibility(View.VISIBLE);
}else {
item_time.setVisibility(View.GONE);
}
}
}
}
}
//在列表上点击歌曲时 发送适配器的播放指针给服务
mUnitAdapter = new PlayUnitAdapter(ReadTextActivity.this, new View.OnClickListener() {
@Override
public void onClick(View v) { //播放
mUnitAdapter.mPosSomeTime = (int)v.getTag();
isplay = true;
PlayUnitPoiMsg.sendPoi(mUnitAdapter.mPosSomeTime, book_cover); //发送指针
}
});
mBinding.recyclerView.setAdapter(mUnitAdapter);
以上便是一个简易在线播放器的UI页面的重点部分,可供参考