Bootstrap

Android Studio实现音乐播放器2.0

一、简要介绍

我在20年初发布过一篇文章《Android Studio如何实现音乐播放器(简单易上手)》,播放器的功能也很简单:就是读取工程文件raw目录的音乐文件,然后设置了播放音乐、暂停播放、继续播放、停止播放四个功能按钮,还有图像旋转和进度条拖动等实现。如下图所示:
在这里插入图片描述
其实很久之前就想对它进行更新了,但是一直没有腾出连续的时间且没有心境去迭代,说白了就是太懒了。近期抽出时间,对它进行了思考和优化,原先版本我们称之为音乐1.0,更新版本称之为音乐2.0。主要更新点如下:

  1. 新增了欢迎、注册和登录页面,登录页有记住密码功能
  2. 新增了SQLite数据库,存储用户和歌手信息
  3. 新增了顺序播放、随机播放和单曲循环3种播放模式
  4. 新增了上一首和下一首歌曲切换功能,并且自动播放下一首
  5. 新增了读取模拟器SD卡的音乐文件,操作简单方便
  6. 新增了JSON格式文件的解析,显示歌手详细信息
  7. 新增了歌曲、歌手和我的3个底部导航栏,切换更加流畅
  8. 新增了搜索框,可以快捷搜索歌手信息
  9. 新增了切换头像、选择主题、清除缓存、分享软件和最近播放等用户功能
  10. 使用了四大组件、Fragment、Handler、RecyclerView、MediaPlayer等技术
  11. 新增了详细注释,优化了编码风格,遵循阿里巴巴Android开发手册

在这里插入图片描述

二、开发环境

只要是21年之后从Android Studio官网下载的AS,都可以运行该App。因为高版本IDE向下兼容,只需要修改Java环境

在这里插入图片描述

三、准备工作

3.1、上传音乐文件

音乐2.0是读取本地音乐文件进行播放的,因此在模拟器上想播放音乐必须要上传好音乐文件。我以夜神模拟器为例,讲解如何上传音乐文件到SD卡中。AS自带模拟器的上传文件操作更简单,只要会玩手机就会玩Android模拟器,可以自行摸索、网上搜索或者参考夜神操作。你可能会觉得我啰嗦,但是我真的怕有同学不会也不去思考,只知道问,不晓得去搜索答案。

电脑上传文件到夜神模拟器的默认存储位置为sdcard/Pictures(无法修改),也就是SD卡中的Pictures目录,而我们需要上传至SD卡中的Music目录,给大家1分钟时间思考一下怎么解决。
在这里插入图片描述
手机上我们移动文件是复制粘贴或者剪切,在模拟器上也是一样。从电脑上拖动音乐文件到模拟器的桌面,然后会自动跳转到sdcard/Pictures目录;然后选中文件,选择“剪切”;接着返回到上一级目录,找到Music目录;最后,在工具栏点击“粘贴”按钮,就移动成功了。

夜神模拟器上传音乐到本地SD卡的Music目录

3.2、工程目录介绍

  • Activity是主要活动,包括:欢迎、注册、登录、歌手详情等。
  • Adapter是适配器类,包括:头像、音乐和歌手的适配器。
  • App是应用基类,包括:活动控制类和活动基类。
  • Bean是实体类,包括:音乐、歌手和用户类。
  • Dao是数据库交互类,将业务逻辑与数据访问逻辑分离。
  • Fragment是碎片类,包括:我的、歌手和歌曲三大碎片。
  • Service是服务类,包含MyIntentService打印线程ID。

在这里插入图片描述

四、详细设计

4.1、读取本地音乐

loadLocalMusicData()方法先创建一个Uri对象,它指向Android媒体库中所有音频文件的内容地址,再使用ContentResolver和Cursor来查询媒体库,读取MP3音乐文件的相关信息,包括歌名、歌手、专辑、路径、音乐时长等,并将这些信息一起格式化为一个LocalMusicBean对象,作为后续播放音乐的传入参数。方法中还包含了一些错误处理和效率优化的考虑,例如:及时关闭Cursor以释放资源。

    /* 加载本地存储当中的音乐mp3文件到集合当中*/
    private void loadLocalMusicData() {
        // 1.获取ContentResolver对象
        ContentResolver resolver = getActivity().getContentResolver();
        // 2.获取本地音乐存储的Uri地址
        Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
        // 3 开始查询地址
        Cursor cursor = resolver.query(uri, null, null, null, null);
        // 4.遍历Cursor
        int id = 0;
        while (cursor.moveToNext()) {
            String song = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE));
            String singer = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST));
            String album = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM));
            id++;// 歌曲的编号
            String sid = String.valueOf(id);
            String path = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA));
            long duration = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));
            SimpleDateFormat sdf = new SimpleDateFormat("mm:ss");
            String time = sdf.format(new Date(duration));
            // 获取专辑图片主要是通过album_id进行查询
            String album_id = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID));
            String albumArt = getAlbumArt(album_id);
            // 将一行当中的数据封装到对象当中
            LocalMusicBean bean = new LocalMusicBean(sid, song, singer, album, time, path, albumArt);
            mDatas.add(bean);
        }
        cursor.close();
        // 数据源变化,提示适配器更新
        adapter.notifyDataSetChanged();
    }

playMusicInMusicBean(LocalMusicBean musicBean) 方法用于播放LocalMusicBean对象表示的音乐文件。首先,它调用stopMusic()方法确保之前的音乐播放被停止。接着,它更新UI,将音乐文件的歌手和歌曲名分别设置到singerTvsongTv文本视图中。然后,实例化一个新的MediaPlayer对象来处理音乐播放。在try块中,它尝试将最新播放的音乐名保存到SharedPreferences中,以便下次使用。之后,设置音乐播放器的数据源为musicBean中的音乐文件路径,并尝试获取专辑封面图片,如果存在则设置到albumIv图像视图中,否则使用默认音乐图标。最后,调用playMusic()方法来播放音乐,并通过addTimer()方法添加一个定时器来管理音乐播放。如果在尝试播放音乐时发生IOException,则捕获并打印异常堆栈跟踪。

public void playMusicInMusicBean(LocalMusicBean musicBean) {}

接下来就是音乐播放器的控制逻辑,包括播放、暂停和停止功能。

  • playMusic() 方法检查 mediaPlayer 对象是否非空且当前状态不是暂停。如果是第一次播放(currentPausePositionInSong 为0),它会准备播放器并开始播放音乐。如果是从暂停处继续播放,它会将音乐播放头移动到上次暂停的位置,然后开始播放。无论哪种情况,播放图标都会改变为表示正在播放的图标。

  • pauseMusic() 方法检查 mediaPlayer 对象是否正在播放音乐。如果是,它会记录当前的播放位置,然后暂停播放。播放图标也会相应地更改为表示暂停状态的图标。

  • stopMusic() 方法彻底停止音乐播放。它首先将位置变量重置为0,然后暂停并重新定位到0位置,接着停止播放。它还会重置播放状态标志 playState 和播放图标,并释放 MediaPlayer 资源,将其设置为空,以便可以进行垃圾回收。

mediaPlayer 是负责实际音乐播放的核心对象。currentPausePositionInSong 变量用于记录音乐播放的暂停位置。playState 变量是一个状态标志,用于区分播放、暂停和停止。playIv 是一个图像视图,用于显示播放和暂停的图标。

    /* 停止音乐的函数:暂停下标置零,播放器暂停,播放器拖到0,播放器停止,设置停止图标,释放播放器,置空*/
    private void stopMusic() {
        if (mediaPlayer != null) {
            currentPausePositionInSong = 0;
            mediaPlayer.pause();
            mediaPlayer.seekTo(0);
            mediaPlayer.stop();
            playState = 0;
            playIv.setImageResource(R.mipmap.icon_play);
            mediaPlayer.release();
            mediaPlayer = null;
        }
    }

4.2、播放模式

播放模式分为顺序播放、随机播放和单曲循环3种,相信大家非常熟悉,具体说明如下:

  • 顺序播放是指播放器按照音乐列表中的顺序依次播放音乐。

  • 随机播放则是播放器从音乐列表中随机选取一首歌曲开始播放,并在播放结束后再次随机选取下一首歌曲。这种方式可以让用户体验到不同的音乐组合,增加播放的趣味性。

  • 单曲循环是指播放器反复播放同一首歌曲。用户可以选择一首喜欢的歌曲,让播放器不断地播放这首歌,直到用户手动切换歌曲为止。

不同的播放模式对上一首、下一首切换,以及自动播放下一首都有不同的影响,需要注意一点:随机播放下随机生成的歌曲下标有可能就是相邻的下一首歌曲下标,会有种顺序播放的感觉,这是合理的逻辑。一定要理解随机播放的定义和代码的逻辑。

 // 0:顺序播放 1:随机播放 2:单曲循环
 case R.id.local_music_bottom_iv_mode:
     if (modeIndex == 0) {
         modeIv.setImageResource(R.mipmap.icon_random);
     } else if (modeIndex == 1) {
         modeIv.setImageResource(R.mipmap.icon_singleloop);
     } else {
         modeIv.setImageResource(R.mipmap.icon_sequence);
     }
     // 每次都加一
     modeIndex = (modeIndex + 1) % 3;
     break;

4.3、切换歌曲

切换上一首和下一首其实就是修改playMusicInMusicBean()方法中传入的音乐对象,对应音乐列表中的下标。上一首就是-1,下一首就是+1,未了首尾相接,要加上列表长度,最后模列表长度即可。上一首和下一首的逻辑分别如下所示:

currentPlayPosition = (currentPlayPosition - 1 + mDatas.size()) % mDatas.size();
currentPlayPosition = (currentPlayPosition + 1) % mDatas.size();

当然,这是在顺序播放单曲循环两种模式下的切换逻辑。随机播放需要先生成随机下标,然后再播放。

  Random random = new Random();
  int seed = random.nextInt(mDatas.size());
  // 若随机数与当前歌曲下标相同,继续生成随机数
  while (currentPlayPosition == seed) {
      seed = random.nextInt(mDatas.size());
  }
  currentPlayPosition = seed;

4.4、自动播放

这里定义了一个名为 addTimer() 的方法,它的主要功能是创建并启动一个 Timer,用于定期更新播放进度并发送音乐时长信息。首先检查 timer 对象是否为 null,不为空跳过。如果为空,说明还没有创建 timer,那么创建一个新的 Timer 对象。接着,创建一个 TimerTask 匿名类的实例,重写其 run 方法。TimerTaskTimer 类的子类,用于定义要执行的任务。在 run 方法内部,首先检查 mediaPlayer 对象是否为 null。如果 mediaPlayernull,说明没有音乐播放器实例正在运行,因此直接返回。如果 mediaPlayer 不为 null,接下来检查 playState 是否等于1。 playState 是一个自定义的状态变量,1代表音乐正在播放。如果音乐确实正在播放,那么调用 sendMusicInfo 方法。最后,使用 timer 对象的 schedule 方法安排 task 任务在0毫秒后开始执行,之后每500毫秒执行一次。

   // 添加计时器用于显示播放进度条
   public void addTimer(){}

sendMusicInfo()通过Handler发送歌曲时长的Message,然后在handleMessage(Message msg)中根据消息头来执行setMusicInfo(Message msg)显示歌曲时长信息,也就是进度条控件上面的时间。

    // 当播放器正常播放时发送歌曲时长信息
    private void sendMusicInfo() {}
	// 当播放器正常播放时显示歌曲时长信息 
	private void setMusicInfo(Message msg) {}

为了实现自动播放,我为进度条添加事件监听,重写了 onProgressChanged 方法。简单来说就是当用户拖动进度条时,会调用此方法,并且传入当前进度 progress 和一个标志 fromUser,表示这个进度变化是由用户直接操作还是由程序自动触发的。当 progress 等于进度条的最大值时,即 progress == seekBar.getMax(),这意味着用户已经将进度条拖到了最右端,这通常表示用户想要跳到音乐的末尾或者想要快速前进到音乐的某个位置。在这种情况下,创建了一个新的 Message 对象,并设置了一个标识符 message.what = 1001;。然后将进度条的值重置为0 bar.setProgress(0);,是为了准备播放新的音乐。最后,通过 handler.sendMessage(message); 将自动播放下一首的消息发送到主线程的消息队列,以便处理后续逻辑。

    @Override
   public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
       // 播放器的播放时间点赋值为滑块的值
       // currentPausePositionInSong = progress;
       // 当进度条到末端时,发送message对象
       if (progress == seekBar.getMax()){
           Message message = new Message();
           message.what = 1001;
           bar.setProgress(0);
           handler.sendMessage(message);
       }
   }

铺垫了这么久,终于到了自动播放下一首音乐的函数,发现逻辑非常简单。其实autoPlay()的代码逻辑与切换下一首是相同的,依然是顺序播放下标加一,随机播放下标随机,单曲循环下标不变。

    /* 自动播放下一首音乐的函数*/
    private void autoPlay() {
        // 顺序播放下标加一,随机播放下标随机,单曲循环下标不变
        if (modeIndex == 0) {
            currentPlayPosition = (currentPlayPosition + 1) % mDatas.size();
        } else if (modeIndex == 1) {
            Random random = new Random();
            int seed = random.nextInt(mDatas.size());
            // 若随机数与当前歌曲下标相同,继续生成随机数
            while (currentPlayPosition == seed) {
                seed = random.nextInt(mDatas.size());
            }
            currentPlayPosition = seed;
        }
        LocalMusicBean musicBean = mDatas.get(currentPlayPosition);
        playMusicInMusicBean(musicBean);
    }

4.5、数据库

数据库名称为:musicplayer.db,一共有两张表UserSinger,详细定义代码如下:

    // 数据库名称
    public static final String DATABASE = "musicplayer.db";

    // 数据库版本号
    public static final int VERSION = 1;

    // 创建用户表User
    public static final String CREATE_USER = "create table User ("
            + "account text primary key,"
            + "password text)";
    // 创建歌手表Singer
    public static final String CREATE_SINGER = "create table Singer ("
            + "name text primary key,"
            + "sex text,"
            + "profile text,"
            + "work text,"
            + "success text,"
            + "imgId text)";

我们查看下这两张表,可以清晰地看到数据。

在这里插入图片描述

在这里插入图片描述

4.6、JSON制作与解析

因为歌手信息非常地多,直接保存到Java类中当然是可以的,但是用JSON文件存储更加简明。JSON格式的数据由一系列的键值对组成,这些键值对被成对的花括号 {} 包围,形成了一个对象。在对象内部,每个键后面跟着一个冒号,然后是对应的值。对象中的键是唯一的,并且必须是字符串,值的类型有很多。每个对象再使用[ ]包围,即JSON对象的数组。
在这里插入图片描述

parseJSONWithJSONObject()方法通过打开一个位于项目资源文件夹中名为 singer_info 的 JSON 格式文件,使用 BufferedReader 逐行读取文件内容,并构建一个字符串。随后,它将字符串解析为 JSONArray,遍历该数组,从每个 JSONObject 中提取歌手的相关信息,如姓名、性别等,并用这些信息创建 Singer 类的实例。最后,将所有 Singer 实例存储在 ArrayList 中并返回这个列表。

	// jsonobject解析json的字符串格式数据
	private List<Singer> parseJSONWithJSONObject() {}

4.7、四大组件应用

  • Activity:在欢迎、注册和登录等活动中都用到了Activity跳转传值等功能。
  • Service:在MyIntentService类中的onHandleIntent(Intent intent)中打印子线程的ID。
  • ContentProvider:在SongFragment类中通过ContentResolver对象获取安卓存储中的媒体文件。
  • Broadcast:在MineFragment类中发送自定义广播,在BaseActivity中定义和注册接收器接收广播。

五、运行演示

5.1、必要说明

  • 功能演示视频中鼠标位置在实际鼠标位置的右下方,应该是夜神录屏的小bug,大家看的时候不要觉得奇怪。理解详细设计中的代码逻辑会有助于大家使用App,因为所有的外在功能都是一个个内在函数的实现。

  • 在软件测试的过程中,只有《月亮惹的祸》这首歌进度条到末尾时会卡住不动,其他所有歌曲播放完都会正常切换到下一首。检查了很久的代码逻辑并未发现问题,如果你的本地音乐无法自动播放下一首,请直接更换歌曲文件。我分享了演示歌曲的百度网盘链接:

  • 链接:https://pan.baidu.com/s/1Epg2VWKVWmAi5wkWLT-LMQ?pwd=12h7 提取码:12h7

5.2、功能演示

Android Studio实现音乐播放器2.0

六、总结展望

在音乐2.0中,我们巧妙融合了Android开发核心技术,如四大组件、Fragment、Handler和RecyclerView,打造了一个流畅且稳定的App。同时,我们不断优化用户体验,实现了便捷的播放控制、搜索功能以及用户个性化设置,满足了用户的多元化需求。此外,通过引入SQLite数据库和SD卡音乐支持,我们进一步增强了App的实用性和便捷性。在以后的安卓开发中,希望大家能学会换位思考,多从用户的角度考虑急难愁盼问题,这样做出来的才是符合实际需求且受用户欢迎的应用。

七、源码获取

关注公众号 《萌新加油站》后台回复:音乐2.0

🚀这有你错过的精彩内容
Android Studio实现文艺阅读App
Android Studio实现志愿者系统
Android Studio实现多功能日记本
Android Studio实现推箱子小游戏
Android Studio实现五子棋小游戏

普劝青年烈士,黄卷名流,发觉悟之心,破色魔之障。芙蓉白面,须知带肉骷髅。美貌红妆,不过蒙衣漏厕。纵对如玉如花之貌,皆存若姊若母之心。未犯淫邪者,宜防失足。曾行恶事者,务劝回头。更祈展转流通,迭相化导。必使在在齐归觉路,人人共出迷津。——《安士全书》

;