6、Car App Library进阶:CarAppService生命周期、Screen与Pane、Input机制、通知与Toast

好,咱们继续往下挖。上一章我们把Car App Library的基本架子搭起来了,你大概知道怎么跑一个最简单的车载应用。但说实话,那些只是皮毛。真正上了车,你会发现事情没那么简单——生命周期怎么管?多页面怎么切?用户旋钮转了一下,代码怎么响应?还有通知,车载的通知和手机完全是两码事。

这一章,我就带你把这些硬骨头啃下来。我会结合我自己在项目里踩过的坑来讲,你听着会更实在。

6.1 CarAppService生命周期:别让Service死得不明不白

先说说CarAppService。它是你整个车载应用的“心脏”。手机端的Activity有onCreate、onStart、onResume那一套,车载这边呢?CarAppService也有自己的生命周期回调。

核心就这几个:

  • onCreate() —— Service创建时调用。我习惯在这里做全局初始化,比如创建ScreenManager、绑定数据源。
  • onDestroy() —— Service销毁时调用。记得释放资源,不然内存泄漏等着你。
  • onBind() —— 返回一个IBinder,供系统绑定。这个你一般不用动,Car App Library帮你封装好了。
  • createScreenManager() —— 关键方法!你必须在这里返回一个ScreenManager实例。它决定了你的应用有多少个Screen,以及它们怎么切换。

重点来了:CarAppService的生命周期和车载系统的“可见性”强相关。如果用户切走了,你的Service可能被系统杀掉。别指望它像手机后台Service那样一直活着。

我曾经在一个项目中,把大量数据缓存在CarAppService的静态变量里。结果用户切到地图再切回来,数据全没了。嗯,后来我改用持久化存储了。你想想看,车载系统对内存更敏感,系统杀Service毫不手软。

6.2 Screen与Pane:页面管理没那么玄乎

车载应用里没有Activity栈那一套。取而代之的是Screen和Pane。你可以把Screen理解成一个“页面”,而Pane是页面里的“面板”。

一个Screen可以包含多个Pane。比如一个导航页面,左边是地图Pane,右边是信息Pane。这种布局在车载上很常见。

看个简单的例子:

public class MyScreen extends Screen {

    public MyScreen(CarAppService service) {
        super(service);
    }

    @Override
    public Pane getPane() {
        // 返回一个Pane,也可以返回多个Pane(用PaneList)
        return new Pane.Builder()
                .setTitle("我的车载应用")
                .addRow(new Row.Builder()
                        .setTitle("点击开始导航")
                        .build())
                .build();
    }
}

这里有个坑:Screen的实例不能自己new。你必须通过ScreenManager来创建。为什么?因为系统需要管理Screen的生命周期和回退栈。我自己刚开始写的时候,直接new了一个Screen传进去,结果应用直接崩溃。后来看了文档才明白——所有Screen必须由ScreenManager管理。

我的建议:在createScreenManager()里,用ScreenManager.Builder来注册你的根Screen。比如:

return new ScreenManager.Builder()
        .setRootScreen(new MyScreen(service))
        .build();

多Screen切换呢?用ScreenManager的push()和pop()方法。push()把新Screen压入栈顶,pop()返回上一个。和Fragment栈很像,但更轻量。

6.3 Input机制:旋钮、触摸、语音,一个都不能少

车载的输入和手机完全不同。手机只有触摸,车载还有物理旋钮、方向盘按键、语音。你的应用必须同时支持这些。

Car App Library提供了一个统一的Input机制。你不需要关心用户用的是旋钮还是手指。你只需要处理点击事件焦点变化

举个例子,给一个Row添加点击响应:

new Row.Builder()
        .setTitle("播放音乐")
        .setOnClickListener(() -> {
            // 用户点击了这行
            startMusicPlayback();
        })
        .build();

你看,代码里没有区分是触摸还是旋钮。系统帮你做了适配。但有一个东西你得注意——焦点。旋钮操作依赖焦点移动。如果你的界面元素不支持获取焦点,旋钮就转不过去。

我曾经踩过的坑:自定义了一个View,忘了设置focusable。结果旋钮怎么转都选不到它。用户反馈说“你们的应用旋钮不好使”。排查了半天,其实就是少了一行 view.setFocusable(true)。嗯,从那以后我每次自定义组件,第一件事就是检查焦点支持。

另外,语音输入呢?Car App Library没有直接封装语音识别,但你可以通过Car API获取语音输入的结果。我一般是在Screen里监听语音回调,然后更新UI。这部分比较深,咱们后面章节再展开。

6.4 通知与Toast:别打扰驾驶员

车载通知和手机通知最大的区别是什么?安全。你不能像手机那样弹一个全屏通知,那会分散驾驶员注意力。Car App Library提供了专门的车载通知API。

先看Toast。车载的Toast和手机类似,但位置和时长有讲究。我建议用CarToast:

CarToast.makeText(getCarContext(), "导航已开始", CarToast.LENGTH_SHORT).show();

注意,这里用的是getCarContext(),不是普通的Context。CarToast会自动把Toast显示在安全区域,不会遮挡后视镜视野。

通知呢?用CarNotification。它比手机通知更简洁,通常只包含标题、一行文本和一个可选图标。没有大图,没有长文本。为什么?因为驾驶员没时间看。

CarNotification notification = new CarNotification.Builder(getCarContext())
        .setTitle("前方有事故")
        .setSubtitle("建议绕行")
        .setIcon(IconCompat.createWithResource(this, R.drawable.ic_warning))
        .build();

CarNotificationManager manager = CarNotificationManager.from(getCarContext());
manager.notify(1, notification);

关键点:车载通知不支持点击跳转。别想着“用户点击通知打开详情页”。在车上,任何需要用户交互的通知都是危险的。通知只是信息展示,不是入口。

我见过有些开发者把手机端的通知逻辑直接搬过来,结果被车载系统直接丢弃。嗯,系统会检查通知的合法性,不合规的就静默丢弃。所以,老老实实按车载规范来。

知识体系总览

下面这张图,我把这一章的核心逻辑串起来了。你看一遍,心里就有谱了。

Car App Library 进阶核心结构 CarAppService onCreate → createScreenManager ScreenManager push / pop 管理Screen栈 Screen 包含多个Pane Pane Input 机制 旋钮 / 触摸 / 语音统一 焦点管理 focusable / 焦点移动 通知与Toast CarNotification / CarToast 安全约束 不跳转 / 不干扰驾驶 所有组件围绕“安全驾驶”这一核心设计

这张图里,CarAppService是入口,它创建ScreenManager,ScreenManager管理Screen,Screen里放Pane。Input机制和通知Toast是独立模块,但它们都受安全约束影响。你写代码的时候,脑子里要有这张图。

好了,这一章的内容就到这儿。生命周期、页面管理、输入、通知——这四个点你掌握了,车载应用的骨架就有了。下一章我们会聊更具体的UI组件和布局技巧,到时候再细说。

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