车载应用开发实战:基于 CarAppLibrary 构建仪表盘应用、媒体应用、空调控制应用

好,我们终于到了动手环节。前面讲了那么多框架、服务、生命周期,说白了都是为了这一刻——真正写一个能跑在车机上的应用。

我个人习惯,学一个新框架时,先拿三个典型应用练手:仪表盘、媒体播放、空调控制。为什么是这三个?因为它们覆盖了 CarAppLibrary 最核心的能力——分层布局、媒体模板、以及自定义控件。你想想看,车机上最常见的场景不就是看状态、听歌、调温度吗?

今天我就带你一个一个把它们实现出来。代码我会贴关键部分,但更重要的是思路——为什么这么写,以及我在项目中踩过的坑。

6.1 仪表盘应用:用 InstrumentClusterTemplate 展示行车数据

仪表盘应用,说白了就是给司机看车速、转速、续航这些关键信息。在 CarAppLibrary 里,Google 专门提供了一个模板叫 InstrumentClusterTemplate,它专为仪表盘场景设计,支持左右分栏、上下分栏,还能嵌入自定义布局。

核心要点:仪表盘应用必须使用 CarAppService 作为入口,而不是普通的 Service。这是很多新手容易忽略的地方。

先看代码结构。我习惯把仪表盘应用拆成三层:

  • Screen 层:定义每个页面的布局和逻辑
  • Service 层:管理生命周期和模板切换
  • Manager 层:处理车辆数据源(比如 CAN 总线数据)

下面是一个最简单的仪表盘 Screen 实现:

public class InstrumentClusterScreen extends Screen {
    private InstrumentClusterTemplate mTemplate;

    public InstrumentClusterScreen(@NonNull CarContext carContext) {
        super(carContext);
        mTemplate = new InstrumentClusterTemplate.Builder()
                .setTitle("行车数据")
                .setSpeed(0)  // 初始速度
                .setRpm(0)    // 初始转速
                .setFuelLevel(50) // 油量百分比
                .build();
    }

    @NonNull
    @Override
    public Template onGetTemplate() {
        return mTemplate;
    }

    // 模拟数据更新
    public void updateSpeed(int speed) {
        mTemplate = new InstrumentClusterTemplate.Builder(mTemplate)
                .setSpeed(speed)
                .build();
        invalidate(); // 通知模板刷新
    }
}

嗯,这里要注意:invalidate() 方法必须调用,否则界面不会更新。我曾经在项目里调试了半天,发现数据明明变了但仪表盘不动,最后发现就是忘了调这个方法。

小技巧:如果你需要显示自定义的仪表盘元素(比如电池图标、故障灯),可以用 setCustomLayout() 传入一个 CarTextCarIcon 的组合。但注意,自定义布局的刷新频率不要超过 30fps,否则会卡顿。

6.2 媒体应用:用 MediaTemplate 实现音乐播放与控制

媒体应用是车载场景里使用频率最高的应用之一。CarAppLibrary 提供了 MediaTemplate,它内置了播放/暂停、上一首/下一首、进度条等控件,你只需要绑定数据源即可。

我建议你把媒体应用拆成两部分:

  1. 媒体浏览器(MediaBrowser):负责扫描和展示本地/在线音乐列表
  2. 媒体控制器(MediaController):负责播放、暂停、切歌等操作

下面是一个使用 MediaTemplate 的典型实现:

public class MediaScreen extends Screen {
    private MediaTemplate mTemplate;
    private List<MediaItem> mPlaylist;

    public MediaScreen(@NonNull CarContext carContext) {
        super(carContext);
        mPlaylist = loadPlaylist(); // 从本地或网络加载歌单

        mTemplate = new MediaTemplate.Builder()
                .setTitle("我的音乐")
                .setPlaylist(mPlaylist)
                .setOnItemClickListener((item) -> {
                    // 点击播放
                    playMedia(item);
                })
                .build();
    }

    @NonNull
    @Override
    public Template onGetTemplate() {
        return mTemplate;
    }

    private void playMedia(MediaItem item) {
        // 实际项目中这里会调用 MediaSession 的 play() 方法
        getCarContext().getCarService(MediaSession.class)
                .play(item.getMediaId());
    }
}

避坑指南:我曾经在项目里直接把整个歌单塞进 MediaTemplate,结果列表超过 200 首时,界面加载慢得像蜗牛。后来发现 MediaTemplate 内部用的是 RecyclerView,但数据绑定是同步的。解决方案是分页加载,每次只传 50 首,配合 setOnScrollListener 实现懒加载。

另外,媒体应用一定要处理好音频焦点。你想想看,如果用户在听歌时突然来了导航语音,两个声音同时播放,那体验得多糟糕。CarAppLibrary 的 AudioManager 已经帮你封装好了焦点请求,你只需要在播放前调用 requestAudioFocus() 即可。

6.3 空调控制应用:用 PaneTemplate 构建自定义控制面板

空调控制应用是展示 CarAppLibrary 灵活性的最佳例子。它不像仪表盘和媒体那样有现成的模板,而是需要你使用 PaneTemplate 配合自定义布局来实现。

PaneTemplate 允许你在一个页面里放置多个面板(Pane),每个面板可以包含按钮、滑块、开关等控件。我习惯把空调控制面板分成三个区域:

区域 功能 推荐控件
左侧面板 温度调节(驾驶位/副驾驶位) CarText + 加减按钮
中间面板 风量、风向、A/C 开关 Slider + ToggleButton
右侧面板 座椅加热、通风、自动模式 图标按钮 + 状态指示

下面是一个使用 PaneTemplate 的示例:

public class ClimateControlScreen extends Screen {
    private PaneTemplate mTemplate;

    public ClimateControlScreen(@NonNull CarContext carContext) {
        super(carContext);

        // 创建左侧面板:温度控制
        Pane leftPane = new Pane.Builder()
                .setTitle("驾驶位温度")
                .addAction(new Action.Builder()
                        .setTitle("降温")
                        .setOnClickListener(() -> decreaseTemp())
                        .build())
                .addAction(new Action.Builder()
                        .setTitle("升温")
                        .setOnClickListener(() -> increaseTemp())
                        .build())
                .build();

        // 创建中间面板:风量控制
        Pane centerPane = new Pane.Builder()
                .setTitle("风量")
                .addAction(new Action.Builder()
                        .setTitle("减小")
                        .setOnClickListener(() -> decreaseFanSpeed())
                        .build())
                .addAction(new Action.Builder()
                        .setTitle("增大")
                        .setOnClickListener(() -> increaseFanSpeed())
                        .build())
                .build();

        mTemplate = new PaneTemplate.Builder()
                .setTitle("空调控制")
                .addPane(leftPane)
                .addPane(centerPane)
                .build();
    }

    @NonNull
    @Override
    public Template onGetTemplate() {
        return mTemplate;
    }
}

个人经验:空调控制应用里,我最头疼的是状态同步。比如用户通过物理按键调了温度,应用界面也得跟着变。解决方案是在 CarAppService 里注册一个 CarPropertyManager 监听器,当车辆 HVAC 属性变化时,主动调用 invalidate() 刷新模板。

6.4 三个应用的架构对比与选择建议

写到这里,你可能已经发现了:三个应用虽然都用 CarAppLibrary,但架构侧重点完全不同。我画了一张图帮你理清思路:

CarAppLibrary 三种典型应用架构对比 仪表盘应用 模板:InstrumentClusterTemplate 数据源:CAN 总线 / 车辆信号 刷新方式:高频轮询(100ms) 布局:固定分栏 复杂度:★★☆☆☆ 关键点:数据实时性 媒体应用 模板:MediaTemplate 数据源:本地/在线音乐库 刷新方式:事件驱动 布局:列表 + 播放控制栏 复杂度:★★★☆☆ 关键点:音频焦点管理 空调控制应用 模板:PaneTemplate 数据源:HVAC 属性系统 刷新方式:属性变化回调 布局:多面板自定义 复杂度:★★★★☆ 关键点:状态同步 总结:选模板看场景,仪表盘重实时、媒体重交互、空调重自定义

从图中你可以看到,三个应用的复杂度是递增的。仪表盘最简单,因为模板固定、数据源单一;媒体应用中等,需要处理列表和音频焦点;空调控制最复杂,因为涉及多面板布局和车辆属性同步。

我个人建议,如果你是第一次接触 CarAppLibrary,先从仪表盘应用入手,跑通整个流程后再挑战媒体和空调。我在带团队时也是这个顺序,新人一般两周就能上手。

最后说一句:CarAppLibrary 的模板化设计,说白了就是 Google 帮你把车机上 80% 的常见场景都封装好了。你不需要从零开始画界面,只需要关注业务逻辑和数据源。但剩下的 20% 自定义场景(比如特殊的 HMI 需求),就需要你深入理解 ScreenTemplate 的生命周期了。这部分我们下一章会详细讲。


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