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

论坛 期权论坛 编程之家     
选择匿名的用户   2021-5-31 19:14   37   0

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' }
        jcenter()
        google()
    }
    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" }
        jcenter()
        google()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

repositories 闭包里面引入了阿里提供的镜像加速地址,以增加快速下载相关依赖信息。由于需要使用部分第三方库,故需要引入 https://jitpack.io 仓库地址。

app构建文件

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    buildToolsVersion "28.0.3"

    defaultConfig {
        applicationId 'com.hzsoft.musicdemo'
        minSdkVersion 21
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"

        vectorDrawables.useSupportLibrary true
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles 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_8
        targetCompatibility 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'
    //rxjava
    implementation "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">

    <LinearLayout
        android: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.RecyclerView
            android:id="@+id/mRecyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

    </LinearLayout>

    <SeekBar
        android:id="@+id/seekbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:max="100" />

    <TextView
        android: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" />

    <LinearLayout
        android:id="@+id/linearBtnGroup"
        android:layout_width="wrap_content"
        android:layout_height="40dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginBottom="40dp"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btnLast"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/btn_last" />

        <Button
            android:id="@+id/btnStar"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/btn_star" />

        <Button
            android:id="@+id/btnStop"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/btn_stop" />

        <Button
            android: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 所示),其中包括:

内容提供程序与其他组件的关系。

使用大白话来讲,就是移动端的每一个文件基本上都会相当于都在一个数据库当中进行注册,包括了这个文件的所有的详细信息,路径等等。我们可以通过获取这个数据库开放的接口进行像查询数据库一样的获取我们所需要的信息。更多详细的介绍可以参考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);
        }

        @Override
        public 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;
    }
    
    /**
     * 媒体资源的缓冲状态
     */
    @Override
    public void onBufferingUpdate(MediaPlayer mp, int percent) {
        
    }

    /**
     * 当前 Song 播放完毕
     */
    @Override
    public void onCompletion(MediaPlayer mp) {
        
    }

    /**
     * 当前 Song 已经准备好
     */
    @Override
    public void onPrepared(MediaPlayer mp) {
        
    }

    static class MusicPlayerHelperHanlder extends Handler {
        WeakReference<MusicPlayerHelper> weakReference;

        public MusicPlayerHelperHanlder(MusicPlayerHelper helper) {
            super(Looper.getMainLooper());
            this.weakReference = new WeakReference<>(helper);
        }

        @Override
        public 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;
    }
    
    /**
     * 媒体资源的缓冲状态
     */
    @Override
    public void onBufferingUpdate(MediaPlayer mp, int percent) {
        
    }

    /**
     * 当前 Song 播放完毕
     */
    @Override
    public void onCompletion(MediaPlayer mp) {
        
    }

    /**
     * 当前 Song 已经准备好
     */
    @Override
    public 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);
        }

        @Override
        public 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;
    }
    
    /**
     * 媒体资源的缓冲状态
     */
    @Override
    public 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 播放完毕
     */
    @Override
    public void onCompletion(MediaPlayer mp) {
        
    }

    /**
     * 当前 Song 已经准备好
     */
    @Override
    public 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);
        }

        @Override
        public 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;
    }
    
    /**
     * 媒体资源的缓冲状态
     */
    @Override
    public 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 播放完毕
     */
    @Override
    public void onCompletion(MediaPlayer mp) {
        
    }

    /**
     * 当前 Song 已经准备好
     */
    @Override
    public 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进度值的改变
     */
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {

    }

    /**
     * 用于监听SeekBar开始拖动
     */
    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
        mHandler.removeMessages(MSG_CODE);
    }

    /**
     * 用于监听SeekBar停止拖动  SeekBar停止拖动后的事件
     */
    @Override
    public 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);
        }

        @Override
        public 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;
    }
    
    /**
     * 媒体资源的缓冲状态
     */
    @Override
    public 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 播放完毕
     */
    @Override
    public void onCompletion(MediaPlayer mp) {
        Log.e(TAG, "onCompletion");
        if (mOnCompletionListener != null) {
            mOnCompletionListener.onCompletion(mp);
        }
    }

    /**
     * 当前 Song 已经准备好
     */
    @Override
    public 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进度值的改变
     */
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {

    }

    /**
     * 用于监听SeekBar开始拖动
     */
    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
        mHandler.removeMessages(MSG_CODE);
    }

    /**
     * 用于监听SeekBar停止拖动  SeekBar停止拖动后的事件
     */
    @Override
    public 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);
        }

        @Override
        public 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;
    }

    @Override
    public 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 播放完毕
     */
    @Override
    public void onCompletion(MediaPlayer mp) {
        Log.e(TAG, "onCompletion");
        if (mOnCompletionListener != null) {
            mOnCompletionListener.onCompletion(mp);
        }
    }

    /**
     * 当前 Song 已经准备好
     */
    @Override
    public 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进度值的改变
     */
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {

    }

    /**
     * 用于监听SeekBar开始拖动
     */
    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
        mHandler.removeMessages(MSG_CODE);
    }

    /**
     * 用于监听SeekBar停止拖动  SeekBar停止拖动后的事件
     */
    @Override
    public 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);
        }

        @Override
        public 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 播放 Helper
        helper = 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.SongViewHodler> {
    public SongAdapter(Context context) {
        super(context);
    }

    @Override
    protected int onBindLayout() {
        return R.layout.item_songs_list;
    }

    @Override
    protected SongAdapter.SongViewHodler onCreateHolder(View view) {
        return new SongViewHodler(view);
    }

    @Override
    protected void onBindData(SongAdapter.SongViewHodler 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 SongViewHodler extends RecyclerView.ViewHolder {

        ImageView ivSongImage;
        TextView tvSongName;

        public SongViewHodler(@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">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <ImageView
            android:id="@+id/ivSongImage"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="4dp" />

        <TextView
            android: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 Adapter
        mAdapter = 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 List<SongModel> songsList = new ArrayList<>();
    /**
     * 当前播放歌曲游标位置
     */
    private int mPosition = 0;


    /**
     * 设置页面标题
     */
    @Override
    public String getTootBarTitle() {
        return "音乐播放器";
    }

    @Override
    public int getToolBarRightImg() {
        return R.drawable.ic_baseline_autorenew_24;
    }

    /**
     * 点击右上角刷新数据
     */
    @Override
    public View.OnClickListener getToolBarRightImgClick() {
        return v -> {
            startAnimation(v);
            initData();
        };
    }

    /**
     * 绑定布局文件
     */
    @Override
    public int onBindLayout() {
        return R.layout.activity_main;
    }

    /**
     * 初始化页面组件
     */
    @Override
    public 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);

        // Init 播放 Helper
        helper = new MusicPlayerHelper(seekbar, tvSongName);
        helper.setOnCompletionListener(mp -> {
            Log.e(TAG, "next()");
            //下一曲
            next();
        });
        // Init Adapter
        mAdapter = 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);
        });
    }

    /**
     * 设置监听
     */
    @Override
    public void initListener() {
        btnStar.setOnClickListener(this::onClick);
        btnStop.setOnClickListener(this::onClick);
        btnLast.setOnClickListener(this::onClick);
        btnNext.setOnClickListener(this::onClick);
    }

    /**
     * 初始化数据局
     */
    @Override
    public void initData() {
        // 请求读写权限
        RxPermissions rxPermissions = new RxPermissions(this);
        rxPermissions.request(
                Manifest.permission.READ_EXTERNAL_STORAGE,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
        ).subscribe(aBoolean -> {
            if (!aBoolean) {
                showToast("缺少存储权限,将会导致部分功能无法使用");
            } else {
                showInitLoadView();
                List<SongModel> musicData = ScanMusicUtils.getMusicData(mContext);
                if (!musicData.isEmpty()) {
                    hideNoDataView();
                    songsList.addAll(musicData);
                    mAdapter.refresh(songsList);
                } else {
                    showNoDataView();
                }
                hideInitLoadView();
            }
        });
    }


    /**
     * 处理点击事件
     */
    private void onClick(View v) {
        switch (v.getId()) {
            // 上一曲
            case R.id.btnLast:
                last();
                break;
            // 播放/暂停
            case R.id.btnStar:
                play(songsList.get(mPosition), false);
                break;
            // 停止
            case R.id.btnStop:
                stop();
                break;
            // 下一曲
            case R.id.btnNext:
                next();
                break;
            default:
                break;
        }
    }

    /**
     * 播放歌曲
     *
     * @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.notifyDataSetChanged();
            }
        } 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();
    }

    /**
     * 开启动画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);
    }

    @Override
    protected 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 的队列当中

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

扫描进群方式

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:3875789
帖子:775174
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP