Android 本地播放器
概述
这是一款Android 端的本地音乐播放器,界面风格有模仿网易云音乐、bilibili、酷安、酷狗等。整体设计遵循了 Material Design 设计风格,界面美观,轻便实用,目前实现了基本的播放控制功能,还有主题切换, 可以扫描本地所有音乐文件并按歌单分类播放,目前已经上架到酷安应用市场。
详细
一、先看效果图
二、项目的播放流程简要介绍
1.首先我们需要一个常驻在后台的播放服务,在播放服务中绑定一个播放广播,我们在打开播放器的时候就启动这个播放服务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | public class MusicPlayerService extends Service { private static final String TAG = MusicPlayerService. class .getName(); public static final String PLAYER_MANAGER_ACTION = "com.lijunyan.blackmusic.service.MusicPlayerService.player.action" ; private PlayerManagerReceiver mReceiver; public MusicPlayerService() { } @Override public IBinder onBind(Intent intent) { throw new UnsupportedOperationException( "Not yet implemented" ); } @Override public void onCreate() { super .onCreate(); Log.e(TAG, "onCreate: " ); register(); } @Override public void onDestroy() { super .onDestroy(); Log.e(TAG, "onDestroy: " ); unRegister(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.e(TAG, "onStartCommand: " ); return super .onStartCommand(intent, flags, startId); } private void register() { mReceiver = new PlayerManagerReceiver(MusicPlayerService. this ); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(PLAYER_MANAGER_ACTION); registerReceiver(mReceiver, intentFilter); } private void unRegister() { if (mReceiver != null ) { unregisterReceiver(mReceiver); } } } |
2.播放服务中的广播可以接受各种音频控制操作,包括播放、暂停、切歌等。程序在响应用户的音频控制操作时向这个播放广播发送对应的播放、暂停、停止等指令。广播收到不同的指令做不同的功能实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | public class PlayerManagerReceiver extends BroadcastReceiver { private static final String TAG = PlayerManagerReceiver. class .getName(); public static final String ACTION_UPDATE_UI_ADAPTER = "com.lijunyan.blackmusic.receiver.PlayerManagerReceiver:action_update_ui_adapter_broad_cast" ; private MediaPlayer mediaPlayer; private DBManager dbManager; public static int status = Constant.STATUS_STOP; private int playMode; private int threadNumber; private Context context; public PlayerManagerReceiver() { } public PlayerManagerReceiver(Context context) { super (); this .context = context; dbManager = DBManager.getInstance(context); mediaPlayer = new MediaPlayer(); Log.d(TAG, "create" ); initMediaPlayer(); } @Override public void onReceive(Context context, Intent intent) { int cmd = intent.getIntExtra(Constant.COMMAND,Constant.COMMAND_INIT); Log.d(TAG, "cmd = " + cmd); switch (cmd) { case Constant.COMMAND_INIT: Log.d(TAG, "COMMAND_INIT" ); break ; case Constant.COMMAND_PLAY: Log.d(TAG, "COMMAND_PLAY" ); status = Constant.STATUS_PLAY; String musicPath = intent.getStringExtra(Constant.KEY_PATH); if (musicPath!= null ) { playMusic(musicPath); } else { mediaPlayer.start(); } break ; case Constant.COMMAND_PAUSE: mediaPlayer.pause(); status = Constant.STATUS_PAUSE; break ; case Constant.COMMAND_STOP: NumberRandom(); status = Constant.STATUS_STOP; if (mediaPlayer!= null ) { mediaPlayer.stop(); } initStopOperate(); break ; case Constant.COMMAND_PROGRESS: //拖动进度 int curProgress = intent.getIntExtra(Constant.KEY_CURRENT, 0 ); //异步的,可以设置完成监听来获取真正定位完成的时候 mediaPlayer.seekTo(curProgress); break ; case Constant.COMMAND_RELEASE: NumberRandom(); status = Constant.STATUS_STOP; if (mediaPlayer!= null ) { mediaPlayer.stop(); mediaPlayer.release(); } break ; } UpdateUI(); } private void initStopOperate(){ MyMusicUtil.setShared(Constant.KEY_ID,dbManager.getFirstId(Constant.LIST_ALLMUSIC)); } private void playMusic(String musicPath) { NumberRandom(); if (mediaPlayer!= null ) { mediaPlayer.release(); } mediaPlayer = new MediaPlayer(); mediaPlayer.setOnCompletionListener( new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { Log.d(TAG, "playMusic onCompletion: " ); NumberRandom(); //切换线程 onComplete(); //调用音乐切换模块,进行相应操作 UpdateUI(); //更新界面 } }); try { File file = new File(musicPath); if (!file.exists()){ Toast.makeText(context, "歌曲文件不存在,请重新扫描" ,Toast.LENGTH_SHORT).show(); MyMusicUtil.playNextMusic(context); return ; } mediaPlayer.setDataSource(musicPath); //设置MediaPlayer数据源 mediaPlayer.prepare(); mediaPlayer.start(); new UpdateUIThread( this , context, threadNumber).start(); } catch (Exception e) { e.printStackTrace(); } } //取一个(0,100)之间的不一样的随机数 private void NumberRandom() { int count; do { count =( int )(Math.random()* 100 ); } while (count == threadNumber); threadNumber = count; } private void onComplete() { MyMusicUtil.playNextMusic(context); } private void UpdateUI() { Intent playBarintent = new Intent(PlayBarFragment.ACTION_UPDATE_UI_PlayBar); //接收广播为MusicUpdateMain playBarintent.putExtra(Constant.STATUS, status); context.sendBroadcast(playBarintent); Intent intent = new Intent(ACTION_UPDATE_UI_ADAPTER); //接收广播为所有歌曲列表的adapter context.sendBroadcast(intent); } private void initMediaPlayer() { NumberRandom(); // 改变线程号,使旧的播放线程停止 int musicId = MyMusicUtil.getIntShared(Constant.KEY_ID); int current = MyMusicUtil.getIntShared(Constant.KEY_CURRENT); Log.d(TAG, "initMediaPlayer musicId = " + musicId); // 如果是没取到当前正在播放的音乐ID,则从数据库中获取第一首音乐的播放信息初始化 if (musicId == - 1 ) { return ; } String path = dbManager.getMusicPath(musicId); if (path == null ) { Log.e(TAG, "initMediaPlayer: path == null" ); return ; } if (current == 0 ) { status = Constant.STATUS_STOP; // 设置播放状态为停止 } else { status = Constant.STATUS_PAUSE; // 设置播放状态为暂停 } Log.d(TAG, "initMediaPlayer status = " + status); MyMusicUtil.setShared(Constant.KEY_ID,musicId); MyMusicUtil.setShared(Constant.KEY_PATH,path); UpdateUI(); } public MediaPlayer getMediaPlayer() { return mediaPlayer; } public int getThreadNumber() { return threadNumber; } } |
3.项目在播放一个音频的同时维护了一个线程实时去通知界面刷新,该线程从MediaPlayer中获取当前的播放进度、总时间等信息发送给播放界面,播放界面拿到数据就可以刷新播放显示信息了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | public class UpdateUIThread extends Thread { private static final String TAG = UpdateUIThread. class .getName(); private int threadNumber; private Context context; private PlayerManagerReceiver playerManagerReceiver; private int duration; private int curPosition; public UpdateUIThread(PlayerManagerReceiver playerManagerReceiver, Context context, int threadNumber) { Log.i(TAG, "UpdateUIThread: " ); this .playerManagerReceiver = playerManagerReceiver; this .context = context; this .threadNumber = threadNumber; } @Override public void run() { try { while (playerManagerReceiver.getThreadNumber() == this .threadNumber) { if (playerManagerReceiver.status == Constant.STATUS_STOP) { Log.e(TAG, "run: Constant.STATUS_STOP" ); break ; } if (playerManagerReceiver.status == Constant.STATUS_PLAY || playerManagerReceiver.status == Constant.STATUS_PAUSE) { if (!playerManagerReceiver.getMediaPlayer().isPlaying()) { Log.i(TAG, "run: getMediaPlayer().isPlaying() = " + playerManagerReceiver.getMediaPlayer().isPlaying()); break ; } duration = playerManagerReceiver.getMediaPlayer().getDuration(); curPosition = playerManagerReceiver.getMediaPlayer().getCurrentPosition(); Intent intent = new Intent(PlayBarFragment.ACTION_UPDATE_UI_PlayBar); intent.putExtra(Constant.STATUS, Constant.STATUS_RUN); intent.putExtra(Constant.KEY_DURATION, duration); intent.putExtra(Constant.KEY_CURRENT, curPosition); context.sendBroadcast(intent); } try { Thread.sleep( 100 ); } catch (InterruptedException e) { e.printStackTrace(); } } } catch (Exception e){ e.printStackTrace(); } } } |
三、项目文件结构
四、其他
如果对你有帮助的话,可以给我github个star,谢谢。
https://github.com/lijunyandev/MeetMusic
注:本文著作权归作者,由demo大师发表,拒绝转载,转载需要作者授权
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· Windows编程----内核对象竟然如此简单?