18、媒体应用开发:CarMediaApp 模板、音频焦点处理、播放列表管理

好,咱们今天聊聊车载媒体应用开发。说实话,这部分内容在 Android Automotive OS 里算是比较有「车味儿」的模块。你想想看,手机上的音乐 App 和车机上的音乐 App,虽然界面长得差不多,但背后的逻辑差别其实挺大的。

我个人习惯把车载媒体开发拆成三个核心块:模板架构音频焦点播放列表管理。这三块搞定了,一个能上车的媒体应用基本就成型了。

18.1 CarMediaApp 模板:别从零造轮子

Android Automotive OS 提供了一套现成的媒体应用模板,叫 CarMediaApp。说白了,Google 已经帮你搭好了架子,你只需要往里面填业务逻辑就行。

我在项目中遇到过不少团队,上来就自己写 MediaBrowserService,结果折腾两周发现跟系统的深色主题、方向盘控制、语音交互全对不上。嗯,这里要提醒你:能用模板就别自己造

模板的核心结构是这样的:

// 模板入口:CarMediaApp 继承自 Application
class MyMediaApp : CarMediaApp() {
    override fun createMediaBrowserService(): MediaBrowserServiceCompat {
        return MyMediaBrowserService()
    }

    override fun createNowPlayingController(): NowPlayingController {
        return MyNowPlayingController()
    }
}

你只需要实现两个方法:createMediaBrowserService()createNowPlayingController()。前者负责提供媒体数据,后者负责控制播放状态。

核心要点:CarMediaApp 模板会自动处理与系统 UI 的绑定,包括通知栏、锁屏、方向盘按键。你不需要手动注册任何 BroadcastReceiver。

模板还内置了分屏适配。我记得有一次测试,发现 App 在宽屏模式下布局错乱,后来发现是没调用模板提供的 onConfigurationChanged() 方法。其实模板已经帮你处理好了,你只需要确保 Activity 继承自 CarMediaActivity

18.2 音频焦点处理:别让 App 打架

车载环境里,音频资源是稀缺的。导航要说话、电话要响铃、音乐要播放——谁先谁后?这就是音频焦点要解决的问题。

Android Automotive OS 的音频焦点机制比手机版更严格。为什么?因为车里多个 App 可能同时请求音频,系统需要根据「优先级」来决定谁出声。

我画了一张图,帮你理清音频焦点的流转逻辑:

音频焦点流转逻辑 请求音频焦点 获得焦点(播放) 焦点被抢占(暂停) 释放焦点(停止) 使用 AudioManager.requestAudioFocus() AUDIOFOCUS_GAIN 回调 AUDIOFOCUS_LOSS 回调 abandonAudioFocus()

代码实现上,你需要注册一个 AudioManager.OnAudioFocusChangeListener

private val focusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
    when (focusChange) {
        AudioManager.AUDIOFOCUS_GAIN -> {
            // 获得焦点,恢复播放
            mediaPlayer?.start()
        }
        AudioManager.AUDIOFOCUS_LOSS -> {
            // 永久失去焦点,停止播放
            mediaPlayer?.stop()
            abandonFocus()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
            // 暂时失去焦点,暂停播放
            mediaPlayer?.pause()
        }
        AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
            // 可以降低音量继续播放(比如导航播报时)
            mediaPlayer?.setVolume(0.3f, 0.3f)
        }
    }
}

注意:车载环境下,AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK 的使用要谨慎。有些车机对音量变化很敏感,频繁「闪避」会让用户觉得声音忽大忽小。我曾经遇到过一个案例,导航每播报一次,音乐就「抖」一下,用户体验极差。

18.3 播放列表管理:别让用户等

播放列表管理,说白了就是怎么把一堆歌曲组织起来,让用户能切歌、能搜索、能记住播放位置。

Android Automotive OS 推荐使用 MediaBrowserServiceCompat 来管理媒体库。它的核心是一个树形结构:根节点下面有专辑、歌手、播放列表等子节点。

我习惯这样组织:

class MyMediaBrowserService : MediaBrowserServiceCompat() {
    override fun onGetRoot(
        clientPackageName: String,
        clientUid: Int,
        rootHints: Bundle?
    ): BrowserRoot? {
        // 返回根节点
        return BrowserRoot("__ROOT__", null)
    }

    override fun onLoadChildren(
        parentId: String,
        result: Result<List<MediaBrowserCompat.MediaItem>>
    ) {
        result.sendResult(when (parentId) {
            "__ROOT__" -> listOf(
                createCategoryItem("专辑", "albums"),
                createCategoryItem("歌手", "artists"),
                createCategoryItem("播放列表", "playlists")
            )
            "albums" -> loadAlbums()
            "playlists" -> loadPlaylists()
            else -> emptyList()
        })
    }
}

这里有个坑:不要在主线程加载媒体数据。车载设备的存储可能是 U 盘、SD 卡或者云端,读取速度差异很大。我建议用 AsyncTask 或者协程来异步加载。

小技巧:播放列表的持久化推荐使用 Room 数据库。SharedPreferences 存不了复杂结构,文件存储又不好做增量更新。Room 配合 LiveData 或 Flow,可以实现播放列表的实时刷新。

播放列表的排序也是个细节。用户期望的是:

  • 最近播放的排前面
  • 收藏的歌曲有特殊标记
  • 支持拖拽排序

嗯,这里我建议用 RecyclerView 配合 ItemTouchHelper 来实现拖拽。代码大概长这样:

val touchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
    ItemTouchHelper.UP or ItemTouchHelper.DOWN,
    0
) {
    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        val from = viewHolder.adapterPosition
        val to = target.adapterPosition
        // 交换播放列表中的位置
        playlistAdapter.swapItems(from, to)
        return true
    }

    override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
        // 不需要滑动删除
    }
})
touchHelper.attachToRecyclerView(recyclerView)

最后提一下「续播」功能。车载场景下,用户可能熄火下车,下次上车还想接着听。你需要保存当前播放的歌曲 ID 和播放位置(毫秒级)。我一般存在 DataStore 里,启动时读取并恢复。

存储方式 适用场景 我的建议
SharedPreferences 简单键值对 不推荐,类型不安全
DataStore 偏好设置、播放状态 推荐,支持协程
Room 复杂播放列表、历史记录 强烈推荐,功能最全

我曾经遇到过一个 Bug:用户熄火后立即启动车辆,播放位置恢复到了上一首歌的开头。排查后发现是 onSaveInstanceStateonRestoreInstanceState 的时机问题。后来改用 DataStore 实时写入,问题解决。

好了,媒体应用开发的核心就这些。模板帮你搭架子,音频焦点帮你管声音,播放列表帮你管数据。三件事做好,你的车载音乐 App 就能稳稳上路了。


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