8. 媒体通知栏:MediaStyle通知、锁屏控制、进度条同步
各位同学,今天我们来聊聊车载多媒体开发里一个特别“显眼”的部分——通知栏。你想想看,用户开车时,最常瞥一眼的就是屏幕顶部的通知区域。如果这里能显示歌曲封面、歌名、进度条,还能直接切歌、暂停,那体验感直接拉满。
我个人习惯把这块叫做“第二界面”。因为用户可能没在主界面操作,但通过通知栏,他依然能控制音乐播放。说白了,这就是一个轻量级的遥控器。
8.1 MediaStyle 通知:不只是显示文字
Android 从 5.0 开始引入了 MediaStyle,专门为媒体播放量身定制。它跟普通通知最大的区别是:可以展示大图标、添加媒体控制按钮、还能在锁屏上显示。
我在项目中遇到过一个问题:普通通知在锁屏上只能显示一个小图标,用户根本看不清是什么歌。换成 MediaStyle 后,封面图直接铺满锁屏,效果立竿见影。
8.1.1 核心 API 与构建流程
构建一个 MediaStyle 通知,你需要用到 NotificationCompat.MediaStyle。嗯,这里要注意:一定要用 androidx.core.app.NotificationCompat,别用老版本的 Notification,否则兼容性会出问题。
// 创建 MediaStyle 通知
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_music_note)
.setLargeIcon(albumArtBitmap) // 专辑封面,建议 512x512
.setContentTitle("晴天")
.setContentText("周杰伦")
.setSubText("叶惠美")
.setOngoing(true) // 不可滑动清除
.setShowWhen(false)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) // 锁屏可见
.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
.setMediaSession(mediaSession.getSessionToken()) // 绑定 MediaSession
.setShowActionsInCompactView(0, 1, 2) // 小视图显示哪些按钮
.setShowCancelButton(true) // 显示关闭按钮
.setCancelButtonIntent(buildPendingIntent(context, ACTION_STOP)));
// 添加控制按钮
builder.addAction(R.drawable.ic_skip_previous, "上一首", buildPendingIntent(context, ACTION_SKIP_PREVIOUS));
builder.addAction(R.drawable.ic_play, "播放", buildPendingIntent(context, ACTION_PLAY));
builder.addAction(R.drawable.ic_skip_next, "下一首", buildPendingIntent(context, ACTION_SKIP_NEXT));
Notification notification = builder.build();
notificationManager.notify(NOTIFICATION_ID, notification);
关键点:
setMediaSession()必须调用,否则锁屏控制不生效setShowActionsInCompactView()控制小视图显示哪些按钮,一般选 3 个setVisibility(VISIBILITY_PUBLIC)让锁屏也能看到完整内容
8.1.2 按钮布局的“潜规则”
你可能会问:为什么按钮最多只能加 5 个?其实这是 Android 系统的限制。通知栏空间有限,按钮太多反而拥挤。我建议你只放 3 个核心按钮:上一首、播放/暂停、下一首。其他功能(比如收藏、循环模式)放到主界面去。
我曾经试过加 5 个按钮,结果在低分辨率车机上,按钮挤成一团,用户点都点不准。后来果断砍到 3 个,反馈好多了。
8.2 锁屏控制:让用户不解锁也能操作
锁屏控制是车载场景的刚需。你想,司机正在开车,不可能解锁手机去切歌。通过锁屏通知,他瞄一眼就能点下一首,安全又方便。
8.2.1 锁屏可见性设置
Android 的锁屏通知有三级可见性:
| 可见性级别 | 效果 | 适用场景 |
|---|---|---|
VISIBILITY_PUBLIC |
锁屏显示完整内容 | 媒体播放、导航提示 |
VISIBILITY_PRIVATE |
锁屏只显示图标,隐藏详情 | 聊天消息、支付通知 |
VISIBILITY_SECRET |
锁屏完全不显示 | 密码、验证码 |
对于媒体应用,我建议用 VISIBILITY_PUBLIC。但要注意:如果用户设置了锁屏密码,系统可能会强制隐藏敏感内容。这时候你需要配合 setLockscreenVisibility() 做兜底处理。
8.2.2 锁屏上的进度条
锁屏上显示进度条,这个需求很常见。但 Android 原生锁屏通知默认不显示进度条。怎么办?
我的做法是:利用 MediaSession 的 PlaybackState 来同步进度。系统锁屏界面会读取 PlaybackState 中的位置信息,自动渲染进度条。
// 更新播放状态,包含进度信息
PlaybackState state = new PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_SKIP_TO_NEXT)
.setState(PlaybackState.STATE_PLAYING, currentPosition, 1.0f) // 当前进度,播放速度
.setBufferedPosition(bufferedPosition) // 缓冲进度
.build();
mediaSession.setPlaybackState(state);
嗯,这里有个坑:setState() 的第二个参数是当前播放位置(毫秒),第三个参数是播放速度(正常是 1.0f)。如果你传了 0,进度条就不会动。我曾经因为这个 bug 被测试追着问:“为什么进度条卡住了?” 后来才发现是速度参数写错了。
8.3 进度条同步:从通知到界面的双向通信
进度条同步是媒体通知栏最复杂的部分。它涉及三个角色:
- MediaSession:持有播放状态和进度
- 通知栏:展示进度条
- 主界面:用户拖拽进度条
这三者必须保持数据一致,否则用户会看到进度条乱跳。
8.3.1 使用 MediaSession 做数据中枢
我个人习惯把 MediaSession 作为唯一的数据源。所有进度更新都通过 PlaybackState 来传递,通知栏和主界面都监听 MediaSession 的回调。
// 在播放器服务中更新进度
private void updateProgress(long position, long duration) {
PlaybackState state = new PlaybackState.Builder()
.setActions(getAvailableActions())
.setState(isPlaying ? PlaybackState.STATE_PLAYING : PlaybackState.STATE_PAUSED,
position, 1.0f)
.setBufferedPosition(bufferedPosition)
.setActiveQueueItemId(mediaSession.getCurrentMediaItem().getMediaId())
.build();
mediaSession.setPlaybackState(state);
// 同时更新通知
updateNotification(position, duration);
}
小技巧:进度更新不要太频繁。我一般每 500ms 更新一次,既保证流畅,又不会过度消耗 CPU。如果你每 100ms 更新一次,车机可能会卡顿。
8.3.2 通知栏进度条的两种实现
通知栏进度条有两种实现方式:
- 系统自带进度条:通过
setProgress(max, progress, indeterminate)实现。优点是简单,缺点是样式固定。 - 自定义 RemoteViews:用
RemoteViews自定义布局,可以放一个SeekBar。优点是样式自由,缺点是交互复杂。
我推荐第一种。为什么?因为车载场景下,用户很少在通知栏拖拽进度条。他更习惯在主界面操作。通知栏的进度条只是“展示”作用,不需要交互。
如果你非要支持通知栏拖拽进度,那就要用 RemoteViews 配合 PendingIntent 发送 ACTION_SEEK_TO 给播放器。嗯,这个实现起来比较绕,我建议你只在主界面做拖拽。
8.3.3 避坑指南:进度条“回弹”问题
我曾经遇到一个经典问题:用户在主界面拖拽进度条到 2:30,松手后进度条又跳回 2:00。为什么?
原因是:拖拽结束后,主界面发送了 ACTION_SEEK_TO 给播放器,但播放器还没处理完,MediaSession 又发来了旧的进度值,覆盖了新的进度。
解决方案是:在拖拽过程中,暂停 MediaSession 的进度更新回调。等播放器真正跳转完成后,再恢复更新。
// 主界面拖拽开始
public void onStartTrackingTouch(SeekBar seekBar) {
isDragging = true; // 标记拖拽中
}
// 主界面拖拽结束
public void onStopTrackingTouch(SeekBar seekBar) {
isDragging = false;
long targetPosition = seekBar.getProgress();
// 发送跳转指令
transportControls.seekTo(targetPosition);
}
// 播放器服务更新进度时,检查是否在拖拽
private void updateProgress(long position, long duration) {
if (isDragging) {
return; // 拖拽中不更新,避免回弹
}
// 正常更新进度
...
}
注意:这个 isDragging 标志位必须是线程安全的。我建议用 AtomicBoolean 或者加 synchronized 关键字。否则多线程环境下,进度条还是会乱跳。
8.4 知识体系总览
下面这张图总结了本章的核心逻辑。你可以看到,MediaSession 是中心枢纽,连接了播放器、通知栏和主界面。所有数据流都经过它,保证了进度的一致性。
总结一下:MediaStyle 通知是媒体应用的“门面”,锁屏控制是安全驾驶的保障,进度条同步是用户体验的基石。这三者环环相扣,缺一不可。
你在实际开发中,如果遇到通知不显示、锁屏没反应、进度条乱跳等问题,先检查 MediaSession 是否绑定成功,再检查 PlaybackState 的更新逻辑。80% 的问题都出在这两个地方。
好了,这一章就到这里。代码示例我都放在课程配套的 Demo 项目里了,你可以边看边练。有什么问题,欢迎在评论区交流。