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);
关键点:车载通知不支持点击跳转。别想着“用户点击通知打开详情页”。在车上,任何需要用户交互的通知都是危险的。通知只是信息展示,不是入口。
我见过有些开发者把手机端的通知逻辑直接搬过来,结果被车载系统直接丢弃。嗯,系统会检查通知的合法性,不合规的就静默丢弃。所以,老老实实按车载规范来。
知识体系总览
下面这张图,我把这一章的核心逻辑串起来了。你看一遍,心里就有谱了。
这张图里,CarAppService是入口,它创建ScreenManager,ScreenManager管理Screen,Screen里放Pane。Input机制和通知Toast是独立模块,但它们都受安全约束影响。你写代码的时候,脑子里要有这张图。
好了,这一章的内容就到这儿。生命周期、页面管理、输入、通知——这四个点你掌握了,车载应用的骨架就有了。下一章我们会聊更具体的UI组件和布局技巧,到时候再细说。