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 原生锁屏通知默认不显示进度条。怎么办?

我的做法是:利用 MediaSessionPlaybackState 来同步进度。系统锁屏界面会读取 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 通知栏进度条的两种实现

通知栏进度条有两种实现方式:

  1. 系统自带进度条:通过 setProgress(max, progress, indeterminate) 实现。优点是简单,缺点是样式固定。
  2. 自定义 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 是中心枢纽,连接了播放器、通知栏和主界面。所有数据流都经过它,保证了进度的一致性。

媒体通知栏知识体系 MediaSession 数据中枢 播放器服务 更新 PlaybackState 通知栏 MediaStyle 展示 主界面 拖拽进度条 setPlaybackState() 回调更新 同步进度 seekTo() 实线:数据推送 | 虚线:用户操作指令

总结一下:MediaStyle 通知是媒体应用的“门面”,锁屏控制是安全驾驶的保障,进度条同步是用户体验的基石。这三者环环相扣,缺一不可。

你在实际开发中,如果遇到通知不显示、锁屏没反应、进度条乱跳等问题,先检查 MediaSession 是否绑定成功,再检查 PlaybackState 的更新逻辑。80% 的问题都出在这两个地方。

好了,这一章就到这里。代码示例我都放在课程配套的 Demo 项目里了,你可以边看边练。有什么问题,欢迎在评论区交流。


公众号:蓝海资料掘金营,微信deep3321