Android音乐播放器的开发实例

介绍

该项目旨在引导喜爱 Android 开发爱好者入门教程实例,可以一步一步的跟着来完成属于自己的项目开发过程。

此项目为基于 Java 语言开发,使用 RecyclerView 多样式布局组件,Rxjava2 权限请求管理,与一些其他基础组件开发完成

实现上一曲、下一曲、开始/暂停、停止以及拖动进度条可以试试快进退正在播放的歌曲内容

第一版博客地址:点击我哦~~~ https://blog.csdn.net/youxun1312/article/details/80356060

基于 Java 语言版本

更新内容 v2.1.0 2020-11-20

  • 1.整体架构进行重写重构。封装基础页面类,基础适配器等
  • 2.使用最新的 RecyclerView 流式布局 + RecyclerViewAdapter 更灵活的进行控制渲染图层
  • 3.对手机目录文件不再单一指向 Music 文件夹,全盘扫描手机路径含有 music 文件夹。例如,music/qqmusic/kgmusci/cloudmusic
  • 4.支持播放音乐格式 AACAMRFLACMP3MIDIOGGPCM
  • 5.音乐媒体播放封装 MusicPlayerHelper 帮助 Android 学习爱好者直接调用
  • 6.增加扫描无数据时候展示无数据页面
  • 7.增加可以刷新列表页面
  • 8.增加可以点击列表进行播放当前歌曲功能
  • 9.优化界面布局

内容

开发工具

构建环境

Gradle-6.1.1

Gradle 插件版本 gradle:4.0.2

项目构建文件build.gradle

// Top-level build file where you can add configuration options common to all sub-projects/modules.buildscript {repositories {maven { url 'https://maven.aliyun.com/repository/public' }maven { url 'https://maven.aliyun.com/repository/google' }maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }maven { url 'https://jitpack.io' }maven { url 'https://repo1.maven.org/maven2/' }google()mavenCentral()}dependencies {classpath "com.android.tools.build:gradle:4.0.2"// NOTE: Do not place your application dependencies here; they belong// in the individual module build.gradle files}
}allprojects {repositories {maven { url 'https://maven.aliyun.com/repository/public' }maven { url 'https://maven.aliyun.com/repository/google' }maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }maven { url 'https://jitpack.io' }maven { url 'https://repo1.maven.org/maven2/' }google()mavenCentral()}
}task clean(type: Delete) {delete rootProject.buildDir
}

repositories 闭包里面引入了阿里提供的镜像加速地址,以增加快速下载相关依赖信息。由于需要使用部分第三方库,故需要引入 JitPack | Publish JVM and Android libraries 仓库地址。

app构建文件

apply plugin: 'com.android.application'android {compileSdkVersion 28buildToolsVersion "29.0.3"defaultConfig {applicationId 'com.hzsoft.musicdemo'minSdkVersion 21targetSdkVersion 28versionCode 1versionName "1.0"vectorDrawables.useSupportLibrary truetestInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'}}lintOptions {checkReleaseBuilds false// Or, if you prefer, you can continue to check for errors in release builds,// but continue the build even when errors are found:abortOnError false}compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}
}dependencies {implementation fileTree(dir: 'libs', include: ['*.jar'])implementation 'com.android.support:appcompat-v7:28.0.0'implementation 'com.android.support:recyclerview-v7:28.0.0'implementation 'com.android.support:cardview-v7:28.0.0'testImplementation 'junit:junit:4.13.1'androidTestImplementation 'com.android.support.test:runner:1.0.2'androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'//rxjavaimplementation "io.reactivex.rxjava2:rxjava:2.2.19"implementation "io.reactivex.rxjava2:rxandroid:2.1.1"//rxpermissions 动听请求权限implementation "com.github.tbruyelle:rxpermissions:0.10.2"}

当前版本基于 SupportLibrary 开发,故需要引入 support-v7 包下相关依赖,关于AndroidX与SupportLib库的区别可以参考当前博客 Android Support v4\v7\v13和AndroidX的区别及应用场景 ,本次重构项目当中使用到了 卡片式布局 CardView ,RecyclerView组件 ,RecyclerView是Android一个更强大的控件,其不仅可以实现和ListView同样的效果,还有优化了ListView中的各种不足。其可以实现数据纵向滚动,也可以实现横向滚动(ListView做不到横向滚动)。其中使用到 RxJava 与 第三方开源库进行动态权限请求 RxPermissions。

准备工作已经结束,马上发车啦!!!

预览

老规矩,话不多说,先上图

先来分析一波,当前页面最顶层使用的是一个Toolbar 用来显示标题等部分菜单信息。标题的最右边是一个 刷新的按钮,可以动态的进行刷新读取本机当中新增的音乐文件。中间布局使用的便是Recycleview纵向布局结合卡片式布局渲染称的 Item ,是不是比上一版本使用的 ListView要好看呀,嘻嘻!中间依然使用的是一个 SeekBar 用来显示当前的播放进度,实现拖动可以快进或者快退。下面依次摆放四个按钮实现歌曲控制。

主页的布局

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><LinearLayoutandroid:id="@+id/linearRecyclerView"android:layout_width="match_parent"android:layout_height="0dp"android:layout_marginBottom="30dp"android:layout_weight="1"android:orientation="vertical"><android.support.v7.widget.RecyclerViewandroid:id="@+id/mRecyclerView"android:layout_width="match_parent"android:layout_height="wrap_content" /></LinearLayout><SeekBarandroid:id="@+id/seekbar"android:layout_width="match_parent"android:layout_height="wrap_content"android:max="100" /><TextViewandroid:id="@+id/tvSongName"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginStart="10dp"android:layout_marginEnd="10dp"android:layout_marginBottom="20dp" /><LinearLayoutandroid:id="@+id/linearBtnGroup"android:layout_width="wrap_content"android:layout_height="40dp"android:layout_gravity="center_horizontal"android:layout_marginBottom="40dp"android:orientation="horizontal"><Buttonandroid:id="@+id/btnLast"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/btn_last" /><Buttonandroid:id="@+id/btnStar"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/btn_star" /><Buttonandroid:id="@+id/btnStop"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/btn_stop" /><Buttonandroid:id="@+id/btnNext"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="@string/btn_next" /></LinearLayout></LinearLayout>

非常的清晰的布局,使用了常规线性布局 LinearLayout 进行嵌套。中间牵扯到一点点的小知识,及线性布局里面的 layout_weight 属性,即带代表当前子组件的权重值,使用当前属性的时候根据当前线性布局当前的布局方向进行可等分布局,当父级布局当中只有一个 layout_weight 子组件的时候,则可以自动撑满空下来的所有空间。

权限申请

Google在 Android 6.0 开始引入了权限申请机制,将所有权限分成了正常权限和危险权限。应用的相关功能每次在使用危险权限时需要动态的申请并得到用户的授权才能使用。

权限分类

系统权限分为两类:正常权限和危险权限。

  • 正常权限不会直接给用户隐私权带来风险。如果您的应用在其清单中列出了正常权限,系统将自动授予该权限。
  • 危险权限会授予应用访问用户机密数据的权限。如果您的应用在其清单中列出了正常权限,系统将自动授予该权限。如果您列出了危险权限,则用户必须明确批准您的应用使用这些权限。

由于我们需要读取本地手机存储的音乐文件,所以需要动态获取读写权限才可以。

    <!-- 向SD卡写入数据权限 --><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><!-- 在SD卡中创建与删除文件权限 --><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><!-- 对SD卡中的文件进行操作的权限--><uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" /><uses-permission android:name="android.permission.MOUNT_FORMAT_FILESYSTEMS" />

获取权限前还需要在 AndroidManifest.xml 进行静态注册所需权限信息。

        // 请求读写权限RxPermissions rxPermissions = new RxPermissions(this);rxPermissions.request(Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE).subscribe(aBoolean -> {if (!aBoolean) {showToast("缺少存储权限,将会导致部分功能无法使用");} else {// 获取到读写权限 进行业务操作// ...}});

读取音乐信息

此次我们将使用 四大组件之一 ContentResolver 进行获取本机数据库当中的所有的音频文件信息。相信很多初学开发者并不是很了解当前组件。下面我简单为大家介绍下。

ContentResolver

内容提供程序以一个或多个表的形式将数据呈现给外部应用,这些表与关系型数据库中的表类似。行表示提供程序收集的某种类型数据的实例,行中的每一列表示为一个实例所收集的单个数据。

内容提供程序协调很多不同的 API 和组件对应用数据存储层的访问(如图 1 所示),其中包括:

  • 与其他应用共享对应用数据的访问
  • 向微件发送数据
  • 使用 SearchRecentSuggestionsProvider,通过搜索框架返回对应用的自定义搜索建议
  • 通过实现 AbstractThreadedSyncAdapter,将应用数据与服务器同步
  • 使用 CursorLoader 在界面中加载数据

使用大白话来讲,就是移动端的每一个文件基本上都会相当于都在一个数据库当中进行注册,包括了这个文件的所有的详细信息,路径等等。我们可以通过获取这个数据库开放的接口进行像查询数据库一样的获取我们所需要的信息。更多详细的介绍可以参考Android开发者官网 内容提供程序基础知识

扫描本地音乐文件

为了我们便利的获取到本机当中的音乐文件,我们通过 ContentResolver 简单封装一个使用工具 ScanMusicUtils.java

/*** Describe:* <p>扫描本地音乐文件</p>** @author zhouhuan* @Date 2020/11/20*/
public class ScanMusicUtils {/*** 扫描系统里面的音频文件,返回一个list集合*/public static List<SongModel> getMusicData(Context context) {List<SongModel> list = new ArrayList<>();String[] selectionArgs = new String[]{"%Music%"};String selection = MediaStore.Audio.Media.DATA + " like ? ";// 媒体库查询语句(写一个工具类MusicUtils)Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, selection,selectionArgs, MediaStore.Audio.AudioColumns.IS_MUSIC);if (cursor != null) {while (cursor.moveToNext()) {SongModel songModel = new SongModel();songModel.setName(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)));songModel.setSinger(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)));songModel.setPath(cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA)));songModel.setDuration(cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)));songModel.setSize(cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)));if (songModel.getSize() > 1000 * 800) {// 注释部分是切割标题,分离出歌曲名和歌手 (本地媒体库读取的歌曲信息不规范)String name = songModel.getName();if (name != null && name.contains("-")) {String[] str = name.split("-");songModel.setSinger(str[0]);songModel.setName(str[1]);}list.add(songModel);}}// 释放资源cursor.close();}return list;}/*** 定义一个方法用来格式化获取到的时间*/public static String formatTime(int time) {if (time / 1000 % 60 < 10) {return (time / 1000 / 60) + ":0" + time / 1000 % 60;} else {return (time / 1000 / 60) + ":" + time / 1000 % 60;}}
}

创建实体 SongModel.java 用来存放音乐信息,一如既往的做上 set/get 方法。

/*** Describe:* <p>歌曲实体模型</p>** @author zhouhuan* @Date 2020/11/20*/
public class SongModel {/*** 歌曲名字*/private String name;/*** 歌曲照片*/private String imagePath;/*** 作家*/private String singer;/*** 路径*/private String path;/*** 时长*/private int duration;/*** 文件大小*/private long size;/*** 是否正在播放*/private Boolean isPlaying = false;public SongModel() {}public String getName() {return name;}public void setName(String name) {this.name = name;}public String getImagePath() {return imagePath;}public void setImagePath(String imagePath) {this.imagePath = imagePath;}public String getSinger() {return singer;}public void setSinger(String singer) {this.singer = singer;}public String getPath() {return path;}public void setPath(String path) {this.path = path;}public int getDuration() {return duration;}public void setDuration(int duration) {this.duration = duration;}public long getSize() {return size;}public void setSize(long size) {this.size = size;}public Boolean getPlaying() {return isPlaying;}public void setPlaying(Boolean playing) {isPlaying = playing;}
}

接下来我们仅仅可以使用一句话

List<SongModel> musicData = ScanMusicUtils.getMusicData(mContext);

便可以得到了本机当中所有已经注册过的音乐信息的集合 。

注册组件

    private RecyclerView mRecyclerView;private SeekBar seekbar;private TextView tvSongName;private Button btnLast;private Button btnStar;private Button btnStop;private Button btnNext;private void initView(){mRecyclerView = (RecyclerView) findViewById(R.id.mRecyclerView);seekbar = (SeekBar) findViewById(R.id.seekbar);tvSongName = (TextView) findViewById(R.id.tvSongName);btnLast = (Button) findViewById(R.id.btnLast);btnStar = (Button) findViewById(R.id.btnStar);btnStop = (Button) findViewById(R.id.btnStop);btnNext = (Button) findViewById(R.id.btnNext);}/*** 设置监听*/public void initListener() {btnStar.setOnClickListener(this::onClick);btnStop.setOnClickListener(this::onClick);btnLast.setOnClickListener(this::onClick);btnNext.setOnClickListener(this::onClick);}/*** 处理点击事件*/private void onClick(View v) {switch (v.getId()) {// 上一曲case R.id.btnLast:break;// 播放/暂停case R.id.btnStar:break;// 停止case R.id.btnStop:break;// 下一曲case R.id.btnNext:break;default:break;}}

使用findViewById进行注册当前的组件。此处设置监听处使用了 Java8 的特性 Lambda 表达式注入到下面的 OnCLick 方法当中,通过 Switch 分发相关的响应事件,至此相信很多学习开发者都可以看得懂。哈哈,接下来涉及到了稍微复杂一些的逻辑啦!!!做好准备啦。

MusicPlayerHelper 音乐播放帮助类

相信很多的同学都知道,接下肯定是该 MediaPlayer 登场啦,没错,接下来有请 MediaPlayer 闪亮登场。哈哈!!!

我们在写代码的时候一定需要注意代码的耦合性,时刻保持代码更好的阅读性,可维护性,低耦合性等等,这样子才会写出更高质量的代码。我们项目严格遵守低耦性等特性进行开发。

首先创建一个空的 MusicPlayerHelper 类。由于回想一下我们首页是不是需要显示当前歌曲的播放进度以及当前播放歌曲的信息,我们使用注册的方式,让其在 MusicPlayrHelper 里面注册 SeekBar 和 TextView 用来显示播放进度,播放信息。

public class MusicPlayerHelper{/*** 进度条*/private SeekBar seekBar;/*** 显示播放信息*/private TextView text;public MusicPlayerHelper(SeekBar seekBar, TextView text) {this.seekBar = seekBar;this.text = text;}
}

我们接下来将要进行异步更新 SeekBar 与 TextView ,接下来引入 Handler 进行异步更新页面的 UI 信息,为了防止App内存泄露我们使用弱连接的方式创建 Handler

public class MusicPlayerHelper{private MusicPlayerHelperHanlder mHandler;/*** 进度条*/private SeekBar seekBar;/*** 显示播放信息*/private TextView text;public MusicPlayerHelper(SeekBar seekBar, TextView text) {mHandler = new MusicPlayerHelperHanlder(this);this.seekBar = seekBar;this.text = text;}static class MusicPlayerHelperHanlder extends Handler {WeakReference<MusicPlayerHelper> weakReference;public MusicPlayerHelperHanlder(MusicPlayerHelper helper) {super(Looper.getMainLooper());this.weakReference = new WeakReference<>(helper);}@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);}}}
}

接下来我们该引入我们的主角啦,MediaPlayer 的创建,设置媒体资源准备进度的监听,媒体资源准备播放完毕监听(进行播放),媒体资源播放完毕监听(方便下一曲)

public class MusicPlayerHelper implements MediaPlayer.OnBufferingUpdateListener,MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener{private MusicPlayerHelperHanlder mHandler;/*** 播放器*/private MediaPlayer player;/*** 进度条*/private SeekBar seekBar;/*** 显示播放信息*/private TextView text;public MusicPlayerHelper(SeekBar seekBar, TextView text) {mHandler = new MusicPlayerHelperHanlder(this);player = new MediaPlayer();// 设置媒体流类型player.setAudioStreamType(AudioManager.STREAM_MUSIC);player.setOnBufferingUpdateListener(this);player.setOnPreparedListener(this);player.setOnCompletionListener(this);this.seekBar = seekBar;this.text = text;}/*** 媒体资源的缓冲状态*/@Overridepublic void onBufferingUpdate(MediaPlayer mp, int percent) {}/*** 当前 Song 播放完毕*/@Overridepublic void onCompletion(MediaPlayer mp) {}/*** 当前 Song 已经准备好*/@Overridepublic void onPrepared(MediaPlayer mp) {}static class MusicPlayerHelperHanlder extends Handler {WeakReference<MusicPlayerHelper> weakReference;public MusicPlayerHelperHanlder(MusicPlayerHelper helper) {super(Looper.getMainLooper());this.weakReference = new WeakReference<>(helper);}@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);}}}
}

播放,暂停,下一曲,上一曲

public class MusicPlayerHelper implements MediaPlayer.OnBufferingUpdateListener,MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener{private MusicPlayerHelperHanlder mHandler;/*** 播放器*/private MediaPlayer player;/*** 进度条*/private SeekBar seekBar;/*** 显示播放信息*/private TextView text;/*** 当前的播放歌曲信息*/private SongModel songModel;public MusicPlayerHelper(SeekBar seekBar, TextView text) {mHandler = new MusicPlayerHelperHanlder(this);player = new MediaPlayer();// 设置媒体流类型player.setAudioStreamType(AudioManager.STREAM_MUSIC);player.setOnBufferingUpdateListener(this);player.setOnPreparedListener(this);player.setOnCompletionListener(this);this.seekBar = seekBar;this.text = text;}/*** 媒体资源的缓冲状态*/@Overridepublic void onBufferingUpdate(MediaPlayer mp, int percent) {}/*** 当前 Song 播放完毕*/@Overridepublic void onCompletion(MediaPlayer mp) {}/*** 当前 Song 已经准备好*/@Overridepublic void onPrepared(MediaPlayer mp) {}/*** 播放** @param songModel    播放源* @param isRestPlayer true 切换歌曲 false 不切换*/public void playBySongModel(@NonNull SongModel songModel, @NonNull Boolean isRestPlayer) {this.songModel = songModel;Log.e(TAG, "playBySongModel Url: " + songModel.getPath());if (isRestPlayer) {//重置多媒体player.reset();// 设置数据源if (!TextUtils.isEmpty(songModel.getPath())) {try {player.setDataSource(songModel.getPath());} catch (IOException e) {e.printStackTrace();}}// 准备自动播放 同步加载,阻塞 UI 线程// player.prepare()// 建议使用异步加载方式,不阻塞 UI 线程player.prepareAsync();} else {player.start();}}/*** 暂停*/public void pause() {Log.e(TAG, "pause");if (player.isPlaying()) {player.pause();}}/*** 停止*/public void stop() {Log.e(TAG, "stop");player.stop();text.setText("停止播放");}static class MusicPlayerHelperHanlder extends Handler {WeakReference<MusicPlayerHelper> weakReference;public MusicPlayerHelperHanlder(MusicPlayerHelper helper) {super(Looper.getMainLooper());this.weakReference = new WeakReference<>(helper);}@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);}}}
}

此处实现了四种功能的方案,可能涉及到比较难理解的地方在与播放的方法,为了区当前播放是从 0 开始播放一首新歌还是从暂停中恢复进行继续播放,我们根据 play() 方法当中设置标志位,主动得知。若是从 0 开始进行重新播放的话,则 isRestPlayer 为 true,重置当前播放对象,重新准备当前的需要播放的资源信息,异步加载不阻塞UI线程。

当面帮助类我们已经实现了播放、暂停、停止等功能,有同学会说道,你到目前还没讲怎么更新一开始引入进来的进度条与播放信息显示。哈哈!!!别着急,下面咱就用到了这两个地方。

public class MusicPlayerHelper implements MediaPlayer.OnBufferingUpdateListener,MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener{public static String TAG = MusicPlayerHelper.class.getSimpleName();private static int MSG_CODE = 0x01;private static long MSG_TIME = 1_000L;private MusicPlayerHelperHanlder mHandler;/*** 播放器*/private MediaPlayer player;/*** 进度条*/private SeekBar seekBar;/*** 显示播放信息*/private TextView text;/*** 当前的播放歌曲信息*/private SongModel songModel;public MusicPlayerHelper(SeekBar seekBar, TextView text) {mHandler = new MusicPlayerHelperHanlder(this);player = new MediaPlayer();// 设置媒体流类型player.setAudioStreamType(AudioManager.STREAM_MUSIC);player.setOnBufferingUpdateListener(this);player.setOnPreparedListener(this);player.setOnCompletionListener(this);this.seekBar = seekBar;this.text = text;}/*** 媒体资源的缓冲状态*/@Overridepublic void onBufferingUpdate(MediaPlayer mp, int percent) {seekBar.setSecondaryProgress(percent);int currentProgress =seekBar.getMax() * player.getCurrentPosition() / player.getDuration();Log.e(TAG, currentProgress + "% play --> " + percent + "% buffer");}/*** 当前 Song 播放完毕*/@Overridepublic void onCompletion(MediaPlayer mp) {}/*** 当前 Song 已经准备好*/@Overridepublic void onPrepared(MediaPlayer mp) {Log.e(TAG, "onPrepared");mp.start();}/*** 播放** @param songModel    播放源* @param isRestPlayer true 切换歌曲 false 不切换*/public void playBySongModel(@NonNull SongModel songModel, @NonNull Boolean isRestPlayer) {this.songModel = songModel;Log.e(TAG, "playBySongModel Url: " + songModel.getPath());if (isRestPlayer) {//重置多媒体player.reset();// 设置数据源if (!TextUtils.isEmpty(songModel.getPath())) {try {player.setDataSource(songModel.getPath());} catch (IOException e) {e.printStackTrace();}}// 准备自动播放 同步加载,阻塞 UI 线程// player.prepare()// 建议使用异步加载方式,不阻塞 UI 线程player.prepareAsync();} else {player.start();}//发送更新命令mHandler.sendEmptyMessage(MSG_CODE);}/*** 暂停*/public void pause() {Log.e(TAG, "pause");if (player.isPlaying()) {player.pause();}//移除更新命令mHandler.removeMessages(MSG_CODE);}/*** 停止*/public void stop() {Log.e(TAG, "stop");player.stop();seekBar.setProgress(0);text.setText("停止播放");//移除更新命令mHandler.removeMessages(MSG_CODE);}private String getCurrentPlayingInfo(int currentTime, int maxTime) {String info = String.format("正在播放:  %s\t\t", songModel.getName());return String.format("%s %s / %s", info, ScanMusicUtils.formatTime(currentTime), ScanMusicUtils.formatTime(maxTime));}static class MusicPlayerHelperHanlder extends Handler {WeakReference<MusicPlayerHelper> weakReference;public MusicPlayerHelperHanlder(MusicPlayerHelper helper) {super(Looper.getMainLooper());this.weakReference = new WeakReference<>(helper);}@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);if (msg.what == MSG_CODE) {int pos = 0;//如果播放且进度条未被按压if (weakReference.get().player.isPlaying() && !weakReference.get().seekBar.isPressed()) {int position = weakReference.get().player.getCurrentPosition();int duration = weakReference.get().player.getDuration();if (duration > 0) {// 计算进度(获取进度条最大刻度*当前音乐播放位置 / 当前音乐时长)pos = (int) (weakReference.get().seekBar.getMax() * position / (duration * 1.0f));}weakReference.get().text.setText(weakReference.get().getCurrentPlayingInfo(position, duration));}weakReference.get().seekBar.setProgress(pos);sendEmptyMessageDelayed(MSG_CODE, MSG_TIME);}}}
}

我们在 MusicPlayerHelperHanlder 内部类里面重写了 handlerMessage 方法,我们通过Handler 的消息机制来更新 UI 组件信息,每隔 1000 ms 发送一次消息即可实现每隔一秒更新一次 UI 信息,目前我们已经实现了播放歌曲可以显示信息了,但是我们还有实现拖动 SeekBar 实现音乐快进快退呀,怎么搞呢,哈哈,别着急。马上揭晓。

public class MusicPlayerHelper implements MediaPlayer.OnBufferingUpdateListener,MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener,SeekBar.OnSeekBarChangeListener{public static String TAG = MusicPlayerHelper.class.getSimpleName();private static int MSG_CODE = 0x01;private static long MSG_TIME = 1_000L;private MusicPlayerHelperHanlder mHandler;/*** 播放器*/private MediaPlayer player;/*** 进度条*/private SeekBar seekBar;/*** 显示播放信息*/private TextView text;/*** 当前的播放歌曲信息*/private SongModel songModel;public MusicPlayerHelper(SeekBar seekBar, TextView text) {mHandler = new MusicPlayerHelperHanlder(this);player = new MediaPlayer();// 设置媒体流类型player.setAudioStreamType(AudioManager.STREAM_MUSIC);player.setOnBufferingUpdateListener(this);player.setOnPreparedListener(this);player.setOnCompletionListener(this);this.seekBar = seekBar;this.text = text;}/*** 媒体资源的缓冲状态*/@Overridepublic void onBufferingUpdate(MediaPlayer mp, int percent) {seekBar.setSecondaryProgress(percent);int currentProgress =seekBar.getMax() * player.getCurrentPosition() / player.getDuration();Log.e(TAG, currentProgress + "% play --> " + percent + "% buffer");}/*** 当前 Song 播放完毕*/@Overridepublic void onCompletion(MediaPlayer mp) {}/*** 当前 Song 已经准备好*/@Overridepublic void onPrepared(MediaPlayer mp) {Log.e(TAG, "onPrepared");mp.start();}/*** 播放** @param songModel    播放源* @param isRestPlayer true 切换歌曲 false 不切换*/public void playBySongModel(@NonNull SongModel songModel, @NonNull Boolean isRestPlayer) {this.songModel = songModel;Log.e(TAG, "playBySongModel Url: " + songModel.getPath());if (isRestPlayer) {//重置多媒体player.reset();// 设置数据源if (!TextUtils.isEmpty(songModel.getPath())) {try {player.setDataSource(songModel.getPath());} catch (IOException e) {e.printStackTrace();}}// 准备自动播放 同步加载,阻塞 UI 线程// player.prepare()// 建议使用异步加载方式,不阻塞 UI 线程player.prepareAsync();} else {player.start();}//发送更新命令mHandler.sendEmptyMessage(MSG_CODE);}/*** 暂停*/public void pause() {Log.e(TAG, "pause");if (player.isPlaying()) {player.pause();}//移除更新命令mHandler.removeMessages(MSG_CODE);}/*** 停止*/public void stop() {Log.e(TAG, "stop");player.stop();seekBar.setProgress(0);text.setText("停止播放");//移除更新命令mHandler.removeMessages(MSG_CODE);}/*** 用于监听SeekBar进度值的改变*/@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}/*** 用于监听SeekBar开始拖动*/@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {mHandler.removeMessages(MSG_CODE);}/*** 用于监听SeekBar停止拖动  SeekBar停止拖动后的事件*/@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {int progress = seekBar.getProgress();Log.i(TAG, "onStopTrackingTouch " + progress);// 得到该首歌曲最长秒数int musicMax = player.getDuration();// SeekBar最大值int seekBarMax = seekBar.getMax();//计算相对当前播放器歌曲的应播放时间float msec = progress / (seekBarMax * 1.0F) * musicMax;// 跳到该曲该秒player.seekTo((int) msec);mHandler.sendEmptyMessageDelayed(MSG_CODE, MSG_TIME);}private String getCurrentPlayingInfo(int currentTime, int maxTime) {String info = String.format("正在播放:  %s\t\t", songModel.getName());return String.format("%s %s / %s", info, ScanMusicUtils.formatTime(currentTime), ScanMusicUtils.formatTime(maxTime));}static class MusicPlayerHelperHanlder extends Handler {WeakReference<MusicPlayerHelper> weakReference;public MusicPlayerHelperHanlder(MusicPlayerHelper helper) {super(Looper.getMainLooper());this.weakReference = new WeakReference<>(helper);}@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);if (msg.what == MSG_CODE) {int pos = 0;//如果播放且进度条未被按压if (weakReference.get().player.isPlaying() && !weakReference.get().seekBar.isPressed()) {int position = weakReference.get().player.getCurrentPosition();int duration = weakReference.get().player.getDuration();if (duration > 0) {// 计算进度(获取进度条最大刻度*当前音乐播放位置 / 当前音乐时长)pos = (int) (weakReference.get().seekBar.getMax() * position / (duration * 1.0f));}weakReference.get().text.setText(weakReference.get().getCurrentPlayingInfo(position, duration));}weakReference.get().seekBar.setProgress(pos);sendEmptyMessageDelayed(MSG_CODE, MSG_TIME);}}}
}

我们需要实现 SeekBar 的监听接口,讲要实现三个方法,分别是 SeekBar 监听改变的值,监听 SeekBar 开始拖动的改变的值 ,监听SeekBar停止拖动 SeekBar停止拖动后的事件,我们用到 第三个 onStopTrackingTouch 方法监听手指拖动 SeekBar 停止后的监听。然后计算当前 SeekBar 停止位置的百分比然后获取当前歌曲播放的秒数得到目标值,直接使用 player.seekTo(int msec) 方法跳转到目前值。然后通知更新 UI 。

到此是不是就完事啦。原则上是这样的,但是,你忘记了一个事,就是当前歌曲播放完了以后,为了提升用户的体验感,是不是最好监听其播放完以后咱们让它继续播放下一首呢,我们做一个接口回调函数,回调到主页面进行获取数据,告诉其我播放完啦。然后你感觉给我下一首的信息吧。

public class MusicPlayerHelper implements MediaPlayer.OnBufferingUpdateListener,MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener,SeekBar.OnSeekBarChangeListener{public static String TAG = MusicPlayerHelper.class.getSimpleName();private static int MSG_CODE = 0x01;private static long MSG_TIME = 1_000L;private MusicPlayerHelperHanlder mHandler;/*** 播放器*/private MediaPlayer player;/*** 进度条*/private SeekBar seekBar;/*** 显示播放信息*/private TextView text;/*** 当前的播放歌曲信息*/private SongModel songModel;public MusicPlayerHelper(SeekBar seekBar, TextView text) {mHandler = new MusicPlayerHelperHanlder(this);player = new MediaPlayer();// 设置媒体流类型player.setAudioStreamType(AudioManager.STREAM_MUSIC);player.setOnBufferingUpdateListener(this);player.setOnPreparedListener(this);player.setOnCompletionListener(this);this.seekBar = seekBar;this.text = text;}/*** 媒体资源的缓冲状态*/@Overridepublic void onBufferingUpdate(MediaPlayer mp, int percent) {seekBar.setSecondaryProgress(percent);int currentProgress =seekBar.getMax() * player.getCurrentPosition() / player.getDuration();Log.e(TAG, currentProgress + "% play --> " + percent + "% buffer");}/*** 当前 Song 播放完毕*/@Overridepublic void onCompletion(MediaPlayer mp) {Log.e(TAG, "onCompletion");if (mOnCompletionListener != null) {mOnCompletionListener.onCompletion(mp);}}/*** 当前 Song 已经准备好*/@Overridepublic void onPrepared(MediaPlayer mp) {Log.e(TAG, "onPrepared");mp.start();}/*** 播放** @param songModel    播放源* @param isRestPlayer true 切换歌曲 false 不切换*/public void playBySongModel(@NonNull SongModel songModel, @NonNull Boolean isRestPlayer) {this.songModel = songModel;Log.e(TAG, "playBySongModel Url: " + songModel.getPath());if (isRestPlayer) {//重置多媒体player.reset();// 设置数据源if (!TextUtils.isEmpty(songModel.getPath())) {try {player.setDataSource(songModel.getPath());} catch (IOException e) {e.printStackTrace();}}// 准备自动播放 同步加载,阻塞 UI 线程// player.prepare()// 建议使用异步加载方式,不阻塞 UI 线程player.prepareAsync();} else {player.start();}//发送更新命令mHandler.sendEmptyMessage(MSG_CODE);}/*** 暂停*/public void pause() {Log.e(TAG, "pause");if (player.isPlaying()) {player.pause();}//移除更新命令mHandler.removeMessages(MSG_CODE);}/*** 停止*/public void stop() {Log.e(TAG, "stop");player.stop();seekBar.setProgress(0);text.setText("停止播放");//移除更新命令mHandler.removeMessages(MSG_CODE);}/*** 用于监听SeekBar进度值的改变*/@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}/*** 用于监听SeekBar开始拖动*/@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {mHandler.removeMessages(MSG_CODE);}/*** 用于监听SeekBar停止拖动  SeekBar停止拖动后的事件*/@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {int progress = seekBar.getProgress();Log.i(TAG, "onStopTrackingTouch " + progress);// 得到该首歌曲最长秒数int musicMax = player.getDuration();// SeekBar最大值int seekBarMax = seekBar.getMax();//计算相对当前播放器歌曲的应播放时间float msec = progress / (seekBarMax * 1.0F) * musicMax;// 跳到该曲该秒player.seekTo((int) msec);mHandler.sendEmptyMessageDelayed(MSG_CODE, MSG_TIME);}private String getCurrentPlayingInfo(int currentTime, int maxTime) {String info = String.format("正在播放:  %s\t\t", songModel.getName());return String.format("%s %s / %s", info, ScanMusicUtils.formatTime(currentTime), ScanMusicUtils.formatTime(maxTime));}private OnCompletionListener mOnCompletionListener;/*** Register a callback to be invoked when the end of a media source* has been reached during playback.** @param listener the callback that will be run*/public void setOnCompletionListener(@NonNull OnCompletionListener listener) {this.mOnCompletionListener = listener;}/*** Interface definition for a callback to be invoked when playback of* a media source has completed.*/interface OnCompletionListener {/*** Called when the end of a media source is reached during playback.** @param mp the MediaPlayer that reached the end of the file*/void onCompletion(MediaPlayer mp);}    static class MusicPlayerHelperHanlder extends Handler {WeakReference<MusicPlayerHelper> weakReference;public MusicPlayerHelperHanlder(MusicPlayerHelper helper) {super(Looper.getMainLooper());this.weakReference = new WeakReference<>(helper);}@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);if (msg.what == MSG_CODE) {int pos = 0;//如果播放且进度条未被按压if (weakReference.get().player.isPlaying() && !weakReference.get().seekBar.isPressed()) {int position = weakReference.get().player.getCurrentPosition();int duration = weakReference.get().player.getDuration();if (duration > 0) {// 计算进度(获取进度条最大刻度*当前音乐播放位置 / 当前音乐时长)pos = (int) (weakReference.get().seekBar.getMax() * position / (duration * 1.0f));}weakReference.get().text.setText(weakReference.get().getCurrentPlayingInfo(position, duration));}weakReference.get().seekBar.setProgress(pos);sendEmptyMessageDelayed(MSG_CODE, MSG_TIME);}}}
}

到此才算基本上结束,接下来我们添加几个完善一点的方法哈,下面贴出当前类的全部代码


/*** Describe:* <p>音乐播放器帮助类</p>* 可播放格式:AAC、AMR、FLAC、MP3、MIDI、OGG、PCM** @author zhouhuan* @Date 2020/11/19*/
public class MusicPlayerHelper implements MediaPlayer.OnBufferingUpdateListener,MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener,SeekBar.OnSeekBarChangeListener {public static String TAG = MusicPlayerHelper.class.getSimpleName();private static int MSG_CODE = 0x01;private static long MSG_TIME = 1_000L;private MusicPlayerHelperHanlder mHandler;/*** 播放器*/private MediaPlayer player;/*** 进度条*/private SeekBar seekBar;/*** 显示播放信息*/private TextView text;/*** 当前的播放歌曲信息*/private SongModel songModel;public MusicPlayerHelper(SeekBar seekBar, TextView text) {mHandler = new MusicPlayerHelperHanlder(this);player = new MediaPlayer();// 设置媒体流类型player.setAudioStreamType(AudioManager.STREAM_MUSIC);player.setOnBufferingUpdateListener(this);player.setOnPreparedListener(this);player.setOnCompletionListener(this);this.seekBar = seekBar;this.seekBar.setOnSeekBarChangeListener(this);this.text = text;}@Overridepublic void onBufferingUpdate(MediaPlayer mp, int percent) {seekBar.setSecondaryProgress(percent);int currentProgress =seekBar.getMax() * player.getCurrentPosition() / player.getDuration();Log.e(TAG, currentProgress + "% play --> " + percent + "% buffer");}/*** 当前 Song 播放完毕*/@Overridepublic void onCompletion(MediaPlayer mp) {Log.e(TAG, "onCompletion");if (mOnCompletionListener != null) {mOnCompletionListener.onCompletion(mp);}}/*** 当前 Song 已经准备好*/@Overridepublic void onPrepared(MediaPlayer mp) {Log.e(TAG, "onPrepared");mp.start();}/*** 播放** @param songModel    播放源* @param isRestPlayer true 切换歌曲 false 不切换*/public void playBySongModel(@NonNull SongModel songModel, @NonNull Boolean isRestPlayer) {this.songModel = songModel;Log.e(TAG, "playBySongModel Url: " + songModel.getPath());if (isRestPlayer) {//重置多媒体player.reset();// 设置数据源if (!TextUtils.isEmpty(songModel.getPath())) {try {player.setDataSource(songModel.getPath());} catch (IOException e) {e.printStackTrace();}}// 准备自动播放 同步加载,阻塞 UI 线程// player.prepare()// 建议使用异步加载方式,不阻塞 UI 线程player.prepareAsync();} else {player.start();}//发送更新命令mHandler.sendEmptyMessage(MSG_CODE);}/*** 暂停*/public void pause() {Log.e(TAG, "pause");if (player.isPlaying()) {player.pause();}//移除更新命令mHandler.removeMessages(MSG_CODE);}/*** 停止*/public void stop() {Log.e(TAG, "stop");player.stop();seekBar.setProgress(0);text.setText("停止播放");//移除更新命令mHandler.removeMessages(MSG_CODE);}/*** 是否正在播放*/public Boolean isPlaying() {return player.isPlaying();}/*** 消亡 必须在 Activity 或者 Frament onDestroy() 调用 以防止内存泄露*/public void destroy() {// 释放掉播放器player.release();mHandler.removeCallbacksAndMessages(null);}/*** 用于监听SeekBar进度值的改变*/@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {}/*** 用于监听SeekBar开始拖动*/@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {mHandler.removeMessages(MSG_CODE);}/*** 用于监听SeekBar停止拖动  SeekBar停止拖动后的事件*/@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {int progress = seekBar.getProgress();Log.i(TAG, "onStopTrackingTouch " + progress);// 得到该首歌曲最长秒数int musicMax = player.getDuration();// SeekBar最大值int seekBarMax = seekBar.getMax();//计算相对当前播放器歌曲的应播放时间float msec = progress / (seekBarMax * 1.0F) * musicMax;// 跳到该曲该秒player.seekTo((int) msec);mHandler.sendEmptyMessageDelayed(MSG_CODE, MSG_TIME);}private String getCurrentPlayingInfo(int currentTime, int maxTime) {String info = String.format("正在播放:  %s\t\t", songModel.getName());return String.format("%s %s / %s", info, ScanMusicUtils.formatTime(currentTime), ScanMusicUtils.formatTime(maxTime));}private OnCompletionListener mOnCompletionListener;/*** Register a callback to be invoked when the end of a media source* has been reached during playback.** @param listener the callback that will be run*/public void setOnCompletionListener(@NonNull OnCompletionListener listener) {this.mOnCompletionListener = listener;}/*** Interface definition for a callback to be invoked when playback of* a media source has completed.*/interface OnCompletionListener {/*** Called when the end of a media source is reached during playback.** @param mp the MediaPlayer that reached the end of the file*/void onCompletion(MediaPlayer mp);}static class MusicPlayerHelperHanlder extends Handler {WeakReference<MusicPlayerHelper> weakReference;public MusicPlayerHelperHanlder(MusicPlayerHelper helper) {super(Looper.getMainLooper());this.weakReference = new WeakReference<>(helper);}@Overridepublic void handleMessage(Message msg) {super.handleMessage(msg);if (msg.what == MSG_CODE) {int pos = 0;//如果播放且进度条未被按压if (weakReference.get().player.isPlaying() && !weakReference.get().seekBar.isPressed()) {int position = weakReference.get().player.getCurrentPosition();int duration = weakReference.get().player.getDuration();if (duration > 0) {// 计算进度(获取进度条最大刻度*当前音乐播放位置 / 当前音乐时长)pos = (int) (weakReference.get().seekBar.getMax() * position / (duration * 1.0f));}weakReference.get().text.setText(weakReference.get().getCurrentPlayingInfo(position, duration));}weakReference.get().seekBar.setProgress(pos);sendEmptyMessageDelayed(MSG_CODE, MSG_TIME);}}}
}

这才看起来更加的舒服一些。

主要是增加了两个方法,当页面消亡的时候一代要调用 destroy() 这个方法,释放掉播放器和 Handler ,停止播放音乐。

    /*** 是否正在播放*/public Boolean isPlaying() {return player.isPlaying();}/*** 消亡 必须在 Activity 或者 Frament onDestroy() 调用 以防止内存泄露*/public void destroy() {// 释放掉播放器player.release();mHandler.removeCallbacksAndMessages(null);}

创建 MusicPlayerHelper

        // Init 播放 Helperhelper = new MusicPlayerHelper(seekbar, tvSongName);helper.setOnCompletionListener(mp -> {Log.e(TAG, "next()");//下一曲next();});

RecyclerView 适配器

/*** Describe:* <p>歌曲适配器</p>** @author zhouhuan* @Date 2020/11/20*/
public class SongAdapter extends BaseAdapter<SongModel, SongAdapter.SongViewHolder> {public SongAdapter(Context context) {super(context);}@Overrideprotected int onBindLayout() {return R.layout.item_songs_list;}@Overrideprotected SongViewHolder onCreateHolder(View view) {return new SongViewHolder(view);}@Overrideprotected void onBindData(SongViewHolder holder, SongModel songModel, int positon) {holder.tvSongName.setText(songModel.getName());holder.ivSongImage.setTag(songModel.getName());if (TextUtils.equals((String) holder.ivSongImage.getTag(), songModel.getName()) && songModel.getPlaying()) {holder.ivSongImage.setImageResource(R.drawable.ic_baseline_headset_24);} else {holder.ivSongImage.setImageResource(R.drawable.ic_baseline_music_note_24);}}static class SongViewHolder extends RecyclerView.ViewHolder {ImageView ivSongImage;TextView tvSongName;public SongViewHolder(@NonNull View itemView) {super(itemView);ivSongImage = itemView.findViewById(R.id.ivSongImage);tvSongName = itemView.findViewById(R.id.tvSongName);}}
}

此处着重讲一下 onBinddata() 方法,主要做的是布局文件与数据的绑定赋值。此处容易若是处理不好的话,容易出现图标错位的问题,最好的方案就是为每一个 holder 的 ivSingImage 设置 Tag 标签,以此进行判断此时是否播放,显示相应的图标。其他的就是重建 ViewHodler 做视图的组件注册绑定,继承 BaseAdapter 。此处的 BaseAdapter 是我进行封装的一套使用模板,若是感兴趣的同学可以仔细看看。此处不做赘述。

Item 布局文件信息,使用了一个卡片式布局,喜欢的同学可以学习一下子

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginLeft="12dp"android:layout_marginTop="10dp"android:layout_marginRight="12dp"android:layout_marginBottom="10dp"app:cardCornerRadius="10dp"app:cardElevation="3dp"app:contentPaddingBottom="15dp"app:contentPaddingLeft="10dp"app:contentPaddingRight="10dp"app:contentPaddingTop="15dp"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"><ImageViewandroid:id="@+id/ivSongImage"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="4dp" /><TextViewandroid:id="@+id/tvSongName"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignTop="@+id/ivSongImage"android:layout_marginLeft="12dp"android:layout_toRightOf="@+id/ivSongImage"android:ellipsize="end"android:singleLine="true"android:textColor="#222222"android:textSize="15sp" /></RelativeLayout>
</android.support.v7.widget.CardView>

在 initView() 方法里面对 Adapter 进行初始化设置 Item 监听。

        // Init AdaptermAdapter = new SongAdapter(mContext);//添加数据源mAdapter.addAll(songsList);// RecyclerView 增加适配器mRecyclerView.setAdapter(mAdapter);// RecyclerView 增加布局管理器mRecyclerView.setLayoutManager(new LinearLayoutManager(this));//增加渲染特效mRecyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(this, R.anim.layout_anim_item_right_slipe));// 需要重新启动布局时调用此方法mRecyclerView.scheduleLayoutAnimation();// Adapter 增加 Item 监听mAdapter.setItemClickListener((object, position) -> {mPosition = position;//播放歌曲play((SongModel) object, true);});

最后贴出页面核心页面布局代码


/*** Describe:* <p>播放器的主页</p>** @author zhouhuan* @Date 2020/11/20*/
public class MainActivity extends BaseActivity {private RecyclerView mRecyclerView;private SeekBar seekbar;private TextView tvSongName;private Button btnLast;private Button btnStar;private Button btnStop;private Button btnNext;private SongAdapter mAdapter;private MusicPlayerHelper helper;/*** 歌曲数据源*/private final List<SongModel> songsList = new ArrayList<>();/*** 当前播放歌曲游标位置*/private int mPosition = 0;/*** 设置页面标题*/@Overridepublic String getTootBarTitle() {return "音乐播放器";}@Overridepublic int getToolBarRightImg() {return R.drawable.ic_baseline_autorenew_24;}/*** 点击右上角刷新数据*/@Overridepublic View.OnClickListener getToolBarRightImgClick() {return v -> {startAnimation(v);initData();};}/*** 绑定布局文件*/@Overridepublic int onBindLayout() {return R.layout.activity_main;}/*** 初始化页面组件*/@Overridepublic void initView() {mRecyclerView = findViewById(R.id.mRecyclerView);seekbar = findViewById(R.id.seekbar);tvSongName = findViewById(R.id.tvSongName);btnLast = findViewById(R.id.btnLast);btnStar = findViewById(R.id.btnStar);btnStop = findViewById(R.id.btnStop);btnNext = findViewById(R.id.btnNext);initPlayHelper();initRecycleView();}/*** 初始化音乐帮助类*/private void initPlayHelper() {// Init 播放 Helperhelper = new MusicPlayerHelper(seekbar, tvSongName);helper.setOnCompletionListener(mp -> {Log.e(TAG, "next()");//下一曲next();});}/*** 初始化列表*/private void initRecycleView() {// Init AdaptermAdapter = new SongAdapter(mContext);//添加数据源mAdapter.addAll(songsList);// RecyclerView 增加适配器mRecyclerView.setAdapter(mAdapter);// RecyclerView 增加布局管理器mRecyclerView.setLayoutManager(new LinearLayoutManager(this));//增加渲染特效mRecyclerView.setLayoutAnimation(AnimationUtils.loadLayoutAnimation(this, R.anim.layout_anim_item_right_slipe));// 需要重新启动布局时调用此方法mRecyclerView.scheduleLayoutAnimation();// Adapter 增加 Item 监听mAdapter.setItemClickListener((object, position) -> {mPosition = position;//播放歌曲play((SongModel) object, true);});}/*** 设置监听*/@Overridepublic void initListener() {btnStar.setOnClickListener(this::onClick);btnStop.setOnClickListener(this::onClick);btnLast.setOnClickListener(this::onClick);btnNext.setOnClickListener(this::onClick);}/*** 初始化数据局*/@Overridepublic void initData() {// 请求读写权限RxPermissions rxPermissions = new RxPermissions(this);Disposable subscribe = rxPermissions.request(Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_PHONE_STATE).subscribe(aBoolean -> {if (!aBoolean) {showToast("缺少存储权限,将会导致部分功能无法使用");} else {refreshMusic();}});boolean disposed = subscribe.isDisposed();Log.d(TAG, "initData: " + disposed);}/*** 刷新音乐*/private void refreshMusic() {// 刷新多媒体数据库MediaScannerConnection.scanFile(this,new String[]{Environment.getExternalStorageDirectory().getAbsolutePath()},new String[]{"audio/aac", "audio/amr", "audio/flac", "audio/mpeg", "audio/midi", "audio/ogg"},new MediaScannerConnection.MediaScannerConnectionClient() {@Overridepublic void onMediaScannerConnected() {}@Overridepublic void onScanCompleted(String path, Uri uri) {runOnUiThread(() -> {showInitLoadView();List<SongModel> musicData = ScanMusicUtils.getMusicData(mContext);if (!musicData.isEmpty()) {hideNoDataView();if (!songsList.isEmpty()) {songsList.clear();}songsList.addAll(musicData);mAdapter.refresh(songsList);} else {showNoDataView();}hideInitLoadView();});}});}/*** 处理点击事件*/private void onClick(View v) {int id = v.getId();if (id == R.id.btnLast) {// 上一曲last();} else if (id == R.id.btnStar) {// 播放/暂停play(songsList.get(mPosition), false);} else if (id == R.id.btnStop) {// 停止stop();} else if (id == R.id.btnNext) {// 下一曲next();}}/*** 播放歌曲** @param songModel    播放源* @param isRestPlayer true 切换歌曲 false 不切换*/private void play(SongModel songModel, Boolean isRestPlayer) {if (!TextUtils.isEmpty(songModel.getPath())) {Log.e(TAG, String.format("当前状态:%s  是否切换歌曲:%s", helper.isPlaying(), isRestPlayer));// 当前若是播放,则进行暂停if (!isRestPlayer && helper.isPlaying()) {btnStar.setText(R.string.btn_play);pause();} else {//进行切换歌曲播放helper.playBySongModel(songModel, isRestPlayer);btnStar.setText(R.string.btn_pause);// 正在播放的列表进行更新哪一首歌曲正在播放 主要是为了更新列表里面的显示for (int i = 0; i < songsList.size(); i++) {songsList.get(i).setPlaying(mPosition == i);mAdapter.notifyItemChanged(i);}}} else {showToast("当前的播放地址无效");}}/*** 上一首*/private void last() {mPosition--;//如果上一曲小于0则取最后一首if (mPosition < 0) {mPosition = songsList.size() - 1;}play(songsList.get(mPosition), true);}/*** 下一首*/private void next() {mPosition++;//如果下一曲大于歌曲数量则取第一首if (mPosition >= songsList.size()) {mPosition = 0;}play(songsList.get(mPosition), true);}/*** 暂停播放*/private void pause() {helper.pause();}/*** 停止播放*/private void stop() {btnStar.setText(R.string.btn_star);helper.stop();songsList.get(mPosition).setPlaying(false);mAdapter.notifyItemChanged(mPosition);}/*** 开启动画360度旋转特效*/private void startAnimation(View v) {Animation loadAnimation =AnimationUtils.loadAnimation(mContext, R.anim.ic_baseline_autorenew_24_rotate);// 设置速度器 LinearInterpolator是匀速加速器loadAnimation.setInterpolator(new LinearInterpolator());// 设置动画时长,以毫秒为单位loadAnimation.setDuration(1_000);// 参数为true时,动画播放完后,view会维持在最终的状态。而默认值是false,也就是动画播放完后,view会恢复原来的状态loadAnimation.setFillAfter(false);v.startAnimation(loadAnimation);}@Overrideprotected void onDestroy() {super.onDestroy();helper.destroy();}
}

版本说明

音乐播放器 v2.0.0 Java版本(老版本)

音乐播放器 v2.0.0 Java版本 https://gitee.com/shandong_zhaotai_network_sd_zhaotai/MusicDemo/releases/v2.0.0

音乐播放器 v2.1.0 Java版本 (新版本)

音乐播放器 v2.1.0 Java版本 (新版本)https://gitee.com/shandong_zhaotai_network_sd_zhaotai/MusicDemo/releases/v2.1.0

音乐播放器 v1.0.0 Kotlin版本 (新版本)

音乐播放器 v1.0.0 Kotlin版本 (新版本)https://gitee.com/shandong_zhaotai_network_sd_zhaotai/MusicDemo/releases/v1.0.0

备注

热烈欢迎感兴趣的同学加入学习 Android 的队列当中

讲述有不足的地方敬请指出

扫描进群方式

Android音乐播放器的开发实例(2021新版-Java版)相关推荐

  1. Android 音乐播放器的开发教程(三) 小卷毛播放器的主界面开发 ---- 小达

    Android 音乐播放器的开发教程(三) 小卷毛播放器的主界面开发 拿好素材之后,打开你们的开发工具,小达这里用的是android studio1.0, 新建一个项目,打开activity_main ...

  2. Android 音乐播放器的开发教程(二)反编译apk ----- 小达

    Android 音乐播放器的开发教程(二)基本布局 在上一篇中简单的介绍了下小卷毛播放器的基本情况,现在就正式的开始一步一步的做播放器.首先想要一个漂亮的UI(不是我的这个...),就需要好的素材,没 ...

  3. android音乐播放器的开发与设计,Android音乐播放器的设计与实现

    内容简介: Android音乐播放器的设计与实现,毕业论文,共21页,7729字. 摘要:本文主要介绍了一个基于Andriod的音乐播放器的设计与实现.主要包括可行性分析,需求分析,App功能设计及实 ...

  4. Android音乐播放器eclipse,简单的Android音乐播放器 eclipse开发的基于Android平台的音乐播放器 - 下载 - 搜珍网...

    压缩包 : 音乐播放器.zip 列表 音乐播放器/ 音乐播放器/.classpath 音乐播放器/.project 音乐播放器/.settings/ 音乐播放器/.settings/org.eclip ...

  5. Android 音乐播放器的开发教程(六)service的运用及音乐列表点击播放 ----- 小达

    service的运用及音乐列表点击播放 按照前几篇博客的步骤,应该能看到自己手机里的音乐列表了,但是现在还只能看,不能点,还需要再给ListView添加点击事件的监听,接着启动一个Service来播放 ...

  6. android 小卷毛播放器,Android 音乐播放器的开发教程(四)Activity和Fragment的通信以及Fragment的切换 ----- 小达...

    Activity和Fragment的通信以及Fragment的切换 在上一篇的博客中讲到了,播放器的主界面布局,是由一个activity和一个fragment构成的,activity启动的时候,在其o ...

  7. Android 音乐播放器的开发教程(十)通知栏Notification的使用 ----- 小达

    通知栏Notification的使用         在这一片博客中,小达将自己学习Notification的一些想法和大家分享一哈,学的不是很深,所有有些东西可能解释的不是特别到位,还请各位谅解哈. ...

  8. Android音乐播放器高级开发

    获取手机音乐的信息 1. 先在布局文件中添加一个ListView <ListView xmlns:android="http://schemas.android.com/apk/res ...

  9. android播放器实例,android音乐播放器实例

    郑州app开发android音乐播放器实例.布局代码是一个imagebutton和seekbar. 下面是java代码 MainActivity.java package cn.xhhkj.music ...

最新文章

  1. IE打印控件推荐-4fang pazu
  2. 【树莓派学习笔记】二、(无屏幕)SSH远程登录、图形界面及系统配置
  3. 区块链基础学习(二)
  4. python map 多参数_python – multiprocessing.pool.map和带有两个参数的函数
  5. php在线编辑器fckeditor,[原创]继续给力:PHP中使用FckEditor在线编辑器详解
  6. cesium 动态水面效果
  7. 安装WPS办公软件后广告特别多怎么办?【wps广告】全网最详细!!!
  8. php 接口类,抽象类 的实际作用
  9. android 单位选择器,Android-PickerView
  10. python学习笔记(1) - python操作mysql数据库【持续更新中...】
  11. 浅析智慧城市顶层设计的相关概念
  12. 锂电池电量百分比计算_锂电池容量计算的方法
  13. Golang 企业级web后端框架
  14. 这几个习惯,让我成为了高阶网络工程师。
  15. HTML5页面元素figure与figcaption标记的应用
  16. Java中两个字符串进行大小比较
  17. 如何查看电脑是什么时候购买的
  18. Flink 算子状态与键控状态总结
  19. 全球与中国4-叔戊基苯酚市场深度研究分析报告
  20. java根据名称生成头像_js实现文字头像的生成代码

热门文章

  1. 市值1000亿的“占卜”生意:20玩家相继获投,最高融资3000万
  2. 传统工厂布局数字化的核心因素——智能工厂
  3. 完美世界联席首席执行官廉洁过世:年仅48岁 天妒英才
  4. [喵咪开源软件推荐(5)]开源DNS服务-bind
  5. php制作学生卡片,抖音私信名片卡片消息链接图文xml生成制作方法php代码插件软件解析...
  6. 7 .opencv中把鼠标当画笔使用
  7. 2022-2028年中国地板行业市场全景调查及投资潜力研究报告
  8. bigdecimal除法不四舍五入_BigDecimal四舍五入与保留位
  9. 基于51单片机 数控恒流源设计 可调电流源
  10. 关于应用~试玩,你想知道的都在这儿了----超详细总结(下篇)