文章目录
- 播放器 1.0
- 播放列表
- 基本 Music 类
- 读取 MP3 音频文件
- 音乐播放器
- 多线程执行音频 & 界面初始化
- 控制状态跳转
- 播放、暂停:
- 创建进度条:
- 释放音乐播放器:
- 更换音乐:
- 播放模式:
- 制作播放控制栏(使用建造者模式)
- 使用 MVP 模式
- 架构:
- 示例:
- 观察者模式
- 使用单例模式
播放器 1.0
播放列表
基本 Music 类
public class Music {
// id、曲名、创作者、专辑名、封面、存放路径、时间
private int musicId;
private String musicName;
private String musicWriter;
private String musicAlbum;
private int musicImageId;
private String musicPath;
private int musicDuring;
// 格式、音质、评论
public Music(int musicId, String musicName, String musicWriter, String musicAlbum, int musicImageId, String musicPath , int musicDuring) {
this.musicId = musicId;
this.musicAlbum = musicAlbum;
this.musicDuring = musicDuring;
this.musicImageId = musicImageId;
this.musicName = musicName;
this.musicPath = musicPath;
this.musicWriter = musicWriter;
}
public int getMusicId() {
return musicId;
}
public String getMusicAlbum() {
return musicAlbum;
}
public int getMusicDuring() {
return musicDuring;
}
public int getMusicImageId() {
return musicImageId;
}
public String getMusicName() {
return musicName;
}
public String getMusicPath() {
return musicPath;
}
public String getMusicWriter() {
return musicWriter;
}
}
读取 MP3 音频文件
添加权限:
获取储存文件需要读取手机文件权限:
Android 包含以下访问外部存储中的文件的权限:
READ_EXTERNAL_STORAGE:允许应用访问外部存储设备中的文件。
WRITE_EXTERNAL_STORAGE:允许应用在外部存储设备中写入和修改文件。拥有此权限的应用也会自动获得 READ_EXTERNAL_STORAGE 权限。
在 AndroidManifest.xml 中添加 READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
这里获取手机网易音乐下载文件夹里的文件,并没有做文件类别判定等特殊处理。
通过文件名生成 Music 对象,添进 MusicList 中
@Override
public void findAllLocalMusic() {
// 使用线程做 IO 操作
// 使用进度
new Thread(new Runnable() {
@Override
public void run() {
int musicId = 0;
mMusicList = new ArrayList<>();
File path = new File(Environment.getExternalStorageDirectory() + "/netease/cloudmusic/Music");
File[] files = path.listFiles();
if (files != null) {
for (File file: files) {
musicId++;
String fileName = file.getName();
String musicWriter = fileName.substring(0, fileName.indexOf(" - "));
String musicName = fileName.substring(fileName.indexOf(" - ") + 3, fileName.lastIndexOf("."));
String musicAlbum = "";
int musicImageId = 0;
int musicDuring = 0;
String musicPath = "";
try {
musicPath = file.getCanonicalPath();
Music music = new Music(musicId, musicName, musicWriter, musicAlbum, musicImageId, musicPath, musicDuring);
mMusicList.add(music);
} catch (IOException e) {
e.printStackTrace();
}
}
}
MyComunicationUtil.sendMessage(MediaPlayerPresenter.getInstance(), 1); // 列表生成,通知主线程
}
}).start();
}
一种媒体信息检索方式:
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();//多媒体信息检索器
mediaMetadataRetriever.setDataSource(path);
String name= mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
String author = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
String duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); // 播放时长单位为毫秒
mediaMetadataRetriever.release();
音乐播放器
Android带播放进度条的音乐播放器
用MediaPlayer做个带进度条可后台的音乐播放器
- 音频初始化
- 界面初始化(曲名、封面、拖动条长度、时间)
- 控制状态跳转(播放-暂停、换曲……)
多线程执行音频 & 界面初始化
public void initMediaPlayer(int position) {
Music music = mMusicList.get(position);
initTasks = new ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory);
InitMediaTask initMedia = new InitMediaTask(music.getMusicPath());
initTasks.schedule(initMedia, 0, TimeUnit.SECONDS);
InitMediaPlayerViewTask initMediaPlayerView = new InitMediaPlayerViewTask(music);
initTasks.schedule(initMediaPlayerView, 0, TimeUnit.SECONDS);
}
音频初始化任务:
class InitMediaTask implements Runnable {
private String path;
public InitMediaTask (String path) {
this.path = path;
}
@Override
public void run() {
try {
mMediaPlayer.reset();
mMediaPlayer.setDataSource(path); // 指定音频文件的路径
mMediaPlayer.prepare(); // 让 MediaPlayer 进入到准备状态
if (MODE == REVOLVE) { // 默认状态下单曲循环,与 PlayMode 保持一致
mMediaPlayer.setLooping(true);
}
startMusic(); // 已完成准备工作,开始播放 & 刷新拖动条
} catch (IOException e) {
e.printStackTrace();
}
}
}
界面初始化任务:
class InitMediaPlayerViewTask implements Runnable {
private Music music;
public InitMediaPlayerViewTask (String music) {
this.music = music;
}
@Override
public void run() {
// 曲名
mMediaPlayerView.showMusicName(music.getMusicName());
// 封面
mMediaPlayerView.showMusicImage(music.getMusicImageId());
// 拖动条长度
mMediaPlayerView.setSeekBarMax(mMediaPlayer.getDuration());
// 时间
SimpleDateFormat format = new SimpleDateFormat("mm:ss");
mMediaPlayerView.setDuring(format.format(mMediaPlayer.getDuration())+"");
}
}
开始播放 & 刷新拖动条:
private void startMusic() {
mMediaPlayer.start();
// 定时任务刷新进度条
updateProgressTask = new ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory);
updateProgressTask.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (mMediaPlayer.isPlaying()) {
mMediaPlayerView.updateProgress(mMediaPlayer.getCurrentPosition());
}
}, 0, 1, TimeUnit.SECONDS);
}
控制状态跳转
播放、暂停:
public void playMusic() {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.start(); // 开始播放
} else {
mMediaPlayer.pause(); // 暂停播放
}
}
创建进度条:
// 进度条初始化设置
seekBar = (SeekBar) findViewById(R.id.seekBar);
RelativeLayout.LayoutParams sbLayoutParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
sbLayoutParams.addRule(RelativeLayout.ABOVE, R.id.layout_music_control); // 设置锚位
seekBar.setLayoutParams(sbLayoutParams);
// 进度条拖动事件
seekBar.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) {
mediaPlayerPresenter.setProgress(seekBar.getProgress());
}
});
拖动改变 Music 进度:
@Override
public void setProgress(int progress) {
mMediaPlayer.seekTo(progress);
}
释放音乐播放器:
public void releaseMediaPlayerPresenter() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.release();
}
if (mMediaPlayerView != null) {
mMediaPlayer = null;
}
if (mMusicList != null) {
mMusicList.clearAll();
mMusicList = null;
}
// 关闭线程池
initTasks.shutdown();
boolean isDone;
// 等待线程池终止
do {
isDone = initTasks.awaitTermination(1, TimeUnit.DAYS);
} while(!isDone);
// 关闭线程池
updateProgressTask.shutdown();
boolean isDone;
// 等待线程池终止
do {
isDone = updateProgressTask.awaitTermination(1, TimeUnit.DAYS);
} while(!isDone);
}
更换音乐:
列表前后更换:
public void previousMusic() {
if (position > 0 && position < mMusicList.size()) {
initMediaPlayer(--position);
} else if(position == 0) {
// 列表循环,则重置 position
position = mMusicList.size() - 1;
initMediaPlayer(position);
}
}
public void nextMusic() {
if (position >= 0 && position < mMusicList.size() - 1) {
initMediaPlayer(++position);
} else if(position == mMusicList.size() - 1) {
// 列表循环,则重置 position
position = 0;
initMediaPlayer(position);
}
}
改进:
// 位置与第几首歌不匹配,mMusicList.size() - 1 就难以理解,真是一大 bug,希望少用 0 做位置
// 使用工具类匹配一下就好
public class MyUtil {
public static int adjustPosition(int position) {
return ++position
}
}
选择更换:
@Override
public void changeMusic(int musicId) {
// 使用 musicId 索引 music
// initMediaPlayer 使用 Music 参数
this.position = musicId;
if (position >= 0 && position < mMusicList.size()) {
initMediaPlayer(position);
}
}
播放模式:
// 播放模式:单曲循环、列表循环、随机播放
private static int MODE = 0;
public static final int REVOLVE = 0;
public static final int LISTCYCLE = 1;
public static final int RANDOM = 2;
播放过程中设置播放模式的方法,如果不设置单曲循环则应将播放器循环设置为 false,否则不回调 onCompletion 方法:
public void setPlayMode(int modeNum) {
MODE = modeNum;
if (MODE == REVOLVE) {
mMediaPlayer.setLooping(true);
} else {
mMediaPlayer.setLooping(false);
}
}
@Override
public void onCompletion(MediaPlayer mp) {
switch (MODE) {
case REVOLVE:
break;
case LISTCYCLE:
nextMusic();
break;
case RANDOM:
Random random = new Random();
int randomPosition = random.nextInt(songs.size());
changeMusic(randomPosition);
break;
default:
break;
}
}
需注册播放完成事件的监听:
mMediaPlayer.setOnCompletionListener(this);
制作播放控制栏(使用建造者模式)
public class MusicControlLayout extends LinearLayout {
// 一定要实现 3 个构造函数
public MusicControlLayout(Context context) {
super(context, null);
}
public MusicControlLayout(Context context, AttributeSet attrs) {
super(context, attrs, 0);
}
public MusicControlLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LayoutInflater.from(context).inflate(R.layout.layout_music_control, null);
}
public static class Builder implements View.OnClickListener {
private ImageView sMode;
private ImageView start;
private ImageView mList;
private ImageView lastS;
private ImageView nextS;
private MusicControlLayout musicControlLayout;
private MediaPlayerPresenter mediaPlayerPresenter = MediaPlayerPresenter.getInstance();
private WeakReference<Context> mContext; // 弱引用持 Context 引用
private int mode = 0;
private int play = 0;
private static final int[] PlayModeResIdList = new int[] {android.R.drawable.ic_media_ff, android.R.drawable.ic_media_rew, android.R.drawable.ic_media_pause};
private static final int[] PlayStateResIdList = new int[] {android.R.drawable.ic_media_play, android.R.drawable.ic_media_pause};
public Builder(Context context) {
musicControlLayout = new MusicControlLayout(context);
mContext = new WeakReference<>(context);
}
public Builder setModeControl() {
sMode = createButton(R.id.id_play_mode, android.R.drawable.ic_media_ff);
musicControlLayout.addView(sMode);
return this;
}
public Builder setPlayControl() {
start = createButton(R.id.id_play, android.R.drawable.ic_media_play);
musicControlLayout.addView(start);
return this;
}
public Builder setNextControl() {
nextS = createButton(R.id.id_play_next, android.R.drawable.ic_media_next);
musicControlLayout.addView(nextS);
return this;
}
public Builder setLastControl() {
lastS = createButton(R.id.id_play_prev, android.R.drawable.ic_media_previous);
musicControlLayout.addView(lastS);
return this;
}
public Builder setListControl() {
mList = createButton(R.id.id_play_list, android.R.drawable.ic_media_rew);
musicControlLayout.addView(mList);
return this;
}
public ImageView createButton(int id, int imageId) {
ImageView imageView = new ImageView(mContext);
imageView.setId(id);
imageView.setImageResource(imageId);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.0f);
imageView.setLayoutParams(layoutParams);
imageView.setOnClickListener(musicControlLayout);
return imageView;
}
public MusicControlLayout create() {
return musicControlLayout;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.id_play_mode:
mediaPlayerPresenter.setPlayMode(mode = ++mode%3);
sMode.setImageResource(PlayModeResIdList[mode]);
break;
case R.id.id_play:
mediaPlayerPresenter.playMusic(play = ++play%2);
start.setImageResource(PlayStateResIdList[play])
break;
case R.id.id_play_list:
MusicListActivity.actionStart(mContext);
break;
case R.id.id_play_prev:
mediaPlayerPresenter.previousMusic();
break;
case R.id.id_play_next:
mediaPlayerPresenter.nextMusic();
default:
break;
}
}
}
}
使用 MVP 模式
架构:
V 层:
- 显示进度条 & 更新进度、显示 Music 部分信息
- 通过滑动进度条对 Music 进行控制、在不同时期对 Music 进行控制(权限请求成功时初始化列表、退出时释放 Music)
- 通过音乐控制栏对 Music 进行控制(上一首、下一首、播放模式)
P 层:
- 逻辑实现:获取播放列表逻辑、Music 初始 & 释放逻辑、Music 控制(播放暂停、切换、模式、改变进度)逻辑
M 层:
- 从 SD 卡中获取音乐文件
示例:
只贴上接口定义,不贴具体的实现。
V 层:
public interface IMediaPlayerView {
void showInformation(String inf);
void showMusicName(String musicName);
void showMusicImage(int musicImageId);
void setSeekBarMax(int max);
void setDuring(String during);
void updateProgress(int progress);
}
P 层:
public interface IMediaPlayerPresenter {
void setMediaPlayerView(IMediaPlayerView mediaPlayerView); // 注册播放器视图,以便用来操作更新
void initMusicList(); // 使用 Model 获取本地下载音频,使用列表保存
void initMediaPlayer(int position);
void setPlayMode(int mode);
void setProgress(int progress);
void playMusic(boolean isPlay);
void lastMusic();
void nextMusic();
void changeMusic(int position);
void releaseMediaPlayerPresenter();
}
M 层:
public interface ILocalMusicModel {
void findAllLocalMusic();
}
Activity 实现 V 层接口,再创建 P 层实例,通过 setMediaPlayerView 将自身注册到 P 层,即可在 P 层的逻辑代码中使用 V 层显示视图;
P 层再创建实现了 M 层接口的类实例,就可以调用 M 层接口方法获取数据。
观察者模式
上述实现中 M 和 P 层之间没有双向的联系,通过再添加接口回调实现:
M 层接口新增方法:
public interface ILocalMusicModel {
...
void setDataListener(IDataListener dataListener);
void removeDataListener(IDataListener dataListener);
}
在 P 层中再创建一个内部类(DataListener)实现接口(IDataListener)。
在 P 层类创建 M 层类实例时,通过 setDataListener 设置 M 层对 P 层的通知回调实例(观察者模式);
修改后的 P 层接口:
public interface IMediaPlayerPresenter {
void setMediaPlayerView(IMediaPlayerView mediaPlayerView); // 注册播放器视图,以便用来操作更新
void initMusicList(); // 使用 Model 获取本地下载音频,使用列表保存
void initMediaPlayer(int position);
void setPlayMode(int mode);
void setProgress(int progress);
void playMusic(boolean isPlay);
void lastMusic();
void nextMusic();
void changeMusic(int position);
void releaseMediaPlayerPresenter();
public interface IDataListener {
void loadDataSuccess(List<Music> list);
}
}
注意:按注册的先后顺序逆序注销。
使用单例模式
由于只使用一个 MediaPlayer,MediaPlayer 在 P 层进行控制,有多个 View 需要 P 实现控制逻辑,就需要多次创建 P 实例,这里对 P 使用单例模式防止这种情况:
private static MediaPlayerPresenter mMediaPlayerPresenter;
public static MediaPlayerPresenter getInstance() {
if (MediaPlayerPresenter == null) {
mMediaPlayerPresenter = new MediaPlayerPresenter();
}
return mMediaPlayerPresenter;
}
由于 M 层不需要创建多个实例,这里也对 M 实现单例模式:
private static LocalMusicModel mLocalMusicModel;
public static LocalMusicModel getInstance() {
if (mLocalMusicModel == null) {
mLocalMusicModel = new LocalMusicModel();
}
return mLocalMusicModel;
}
参考:
音乐播放器–观察者模式+单例
Android MVP 十分钟入门!
ANDROID MVP 模式 简单易懂的介绍方式
MVP架构开发,一篇让你从看懂到会使用
Android中建造者模式自定义Dialog
更多推荐
Android 开发实战 - 音乐播放器
发布评论