文章目录

    • 播放器 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 开发实战 - 音乐播放器