系统应用定制:Launcher 定制、SystemUI 修改、设置应用扩展

说实话,车载系统里最让用户有感知的,就是这几个系统应用。Launcher 好不好用、SystemUI 顺不顺手、设置菜单合不合理,直接决定了用户对整台车的评价。我当年刚入行时,以为这些应用就是改改布局、换换图标,结果第一个项目就被狠狠教育了一顿——嗯,这里面的坑,比想象中深得多。

Launcher 定制:不只是换个壁纸

Launcher 是车载系统的门面。用户一上车,第一眼看到的就是它。但车载 Launcher 和手机 Launcher 完全是两码事。

核心差异在哪?

  • 交互方式:手机靠触控,车载靠旋钮、触摸板、语音。焦点移动逻辑要重写。
  • 安全限制:行驶中不能看视频、不能操作复杂菜单。Launcher 要感知车辆状态。
  • 多窗口:车载经常要分屏显示,导航+音乐+空调同时出现。

我在项目中遇到过最典型的场景:客户要求 Launcher 首页显示导航卡片、音乐卡片、天气卡片,还要支持拖拽排序。听起来简单?但车载的焦点导航和手机完全不同。手机靠手指点哪是哪,车载要用物理旋钮一圈一圈转过去。

关键点:车载 Launcher 的焦点管理,必须基于 RecyclerViewListViewfocusSearch() 重写。不要指望系统默认的焦点逻辑能搞定。

举个例子,我们当时要实现一个卡片式 Launcher,每张卡片是一个 CardView,里面嵌套了 TextViewImageView。结果焦点经常跳到卡片内部的子 View 上,用户旋钮一转,焦点莫名其妙消失了。

// 重写焦点搜索,确保焦点只在卡片级别移动
@Override
public View focusSearch(View focused, int direction) {
    // 如果焦点在卡片内部,先找到所属的卡片
    View cardView = findCardParent(focused);
    if (cardView != null) {
        // 基于卡片位置计算下一个焦点
        int nextPosition = calculateNextPosition(cardView, direction);
        return getChildAt(nextPosition);
    }
    return super.focusSearch(focused, direction);
}

另外,车载 Launcher 还有一个隐藏需求——冷启动速度。用户点火后,Launcher 必须在 2 秒内显示出来。我见过一个项目,Launcher 启动时去加载网络图片、解析 JSON,结果黑屏了 5 秒。后来改成预加载 + 本地缓存,才勉强达标。

我的建议:Launcher 的首页数据,用 SharedPreferencesRoom 本地持久化。网络请求异步执行,不要阻塞 UI 线程。启动时先显示骨架屏,数据到了再刷新。

SystemUI 修改:状态栏、导航栏、通知中心

SystemUI 是 Android 系统里最复杂的应用之一。它管理着状态栏、导航栏、锁屏、通知面板、音量面板等等。车载场景下,SystemUI 的修改量往往比 Launcher 还大。

常见的定制需求

  • 状态栏:显示车速、油量、温度、信号强度。图标位置和大小要重新排。
  • 导航栏:去掉返回键,增加 Home 键和自定义快捷键。
  • 通知中心:只显示高优先级通知,行驶中禁止展开。
  • 音量面板:增加媒体、导航、电话、语音助手的独立音量控制。

我记得有一次,客户要求在状态栏右侧显示一个「驾驶模式」图标,点击后弹出模式选择菜单。听起来简单?但 SystemUI 的架构是高度模块化的,状态栏的图标由 StatusBarIconController 管理,点击事件要走 StatusBaronClick 分发。

我当时踩了一个坑:直接在 StatusBar 里加了一个 ImageView,结果系统 OTA 升级后,Google 改了 SystemUI 的布局文件,我的代码全废了。

避坑指南:修改 SystemUI 时,尽量用 CollationManagerDynamicSystemUI 的方式注入自定义 View,不要直接改 status_bar.xml。我曾经因为直接改布局文件,导致后续三个版本都要手动合并代码。

来看一个实际例子:我们要在状态栏添加一个「蓝牙连接状态」图标。正确的做法是注册一个 BroadcastReceiver,监听 BluetoothDevice.ACTION_ACL_CONNECTEDACTION_ACL_DISCONNECTED,然后通过 StatusBarIconController 动态添加或移除图标。

// 动态添加状态栏图标
public class BluetoothStatusController {
    private Context mContext;
    private StatusBarIconController mIconController;
    private String mSlot = "bluetooth_status";

    public void addBluetoothIcon() {
        StatusBarIcon icon = new StatusBarIcon(
            mContext.getPackageName(),
            mContext,
            R.drawable.ic_bluetooth_connected,
            0, 0, 0
        );
        mIconController.setIcon(mSlot, icon);
    }

    public void removeBluetoothIcon() {
        mIconController.removeIcon(mSlot);
    }
}

另外,车载 SystemUI 还有一个特殊要求——安全模式。车辆行驶时,导航栏的某些按钮要禁用,通知面板不能下拉。这些逻辑要写在 KeyguardViewMediatorStatusBarWindowManager 里,判断车辆速度或档位状态。

核心思路:SystemUI 的修改,本质上是「监听车辆状态 + 动态更新 UI」。不要写死任何逻辑,所有状态都通过 CarServiceVehicle HAL 获取。

设置应用扩展:从简单列表到复杂配置

设置应用(Settings)是 Android 系统里代码量最大的应用之一。车载场景下,设置应用要增加很多车相关的配置项,比如:

  • 车辆设置:灯光、门锁、空调、座椅
  • 驾驶辅助:车道保持、自适应巡航
  • 账户管理:登录车厂账号、同步个人设置
  • 系统更新:OTA 升级、应用管理

扩展设置应用,最直接的方式是添加新的 FragmentPreferenceScreen。但这里有一个架构问题:Settings 应用本身是 Google 维护的,每次 AOSP 更新都会有大改动。如果你直接改 Settings 的源码,后续合并代码会非常痛苦。

我个人的习惯是:用插件化或模块化的方式扩展 Settings。具体来说,就是在 Settings 里预留一个「车辆设置」入口,点击后跳转到我们自己的 VehicleSettingsActivity,这个 Activity 完全独立于 Settings 应用,不依赖 Settings 的代码。

// 在 Settings 的 AndroidManifest.xml 中注册
<activity
    android:name=".VehicleSettingsActivity"
    android:exported="true"
    android:label="@string/vehicle_settings">
    <intent-filter>
        <action android:name="android.settings.VEHICLE_SETTINGS" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

// 在 Settings 的 DashboardFragment 中添加入口
Preference vehiclePref = new Preference(context);
vehiclePref.setKey("vehicle_settings");
vehiclePref.setTitle(R.string.vehicle_settings);
vehiclePref.setIntent(new Intent("android.settings.VEHICLE_SETTINGS"));
getPreferenceScreen().addPreference(vehiclePref);

这样做的好处很明显:Google 更新 Settings 时,我们只需要保证入口代码能编译通过,车辆设置部分的代码完全不受影响。

另外,车载设置应用还有一个特殊需求——多用户配置。一辆车可能被多个家庭成员使用,每个人的座椅位置、后视镜角度、空调偏好都不一样。设置应用要支持用户切换时,自动加载对应的配置。

我的经验:多用户配置用 UserManagergetUserProfiles() 获取用户列表,然后用 ContentProvider 存储每个用户的设置项。切换用户时,通过 CarService 下发配置到对应的 ECU。

知识体系总览

下面这张图,是我自己总结的系统应用定制核心逻辑。你看一眼,基本就知道该从哪里下手了。

车载系统应用定制核心架构 Launcher 定制 SystemUI 修改 设置应用扩展 焦点管理 · 卡片布局 · 冷启动优化 车辆状态感知 · 安全模式 状态栏图标 · 导航栏定制 通知中心 · 音量面板 · 安全限制 车辆设置 · 驾驶辅助 · 账户管理 多用户配置 · OTA 升级 公共基础设施:CarService · Vehicle HAL · 系统服务 所有定制都依赖底层车辆服务,不要直接操作硬件 模块化设计,降低与 AOSP 的耦合度

你看,这三个应用虽然功能不同,但底层都依赖 CarServiceVehicle HAL。说白了,系统应用定制就是「UI 层 + 车辆状态层」的组合。UI 层负责展示和交互,车辆状态层负责获取车速、档位、灯光等信息。

最后说一句:做车载系统应用,千万别想着一次性搞定所有需求。我见过太多团队,一开始就想做一个「完美」的 Launcher,结果开发周期拖了半年,最后被客户骂得狗血淋头。我的建议是:先跑通核心流程,再逐步迭代。Launcher 先保证能显示、能点击、能响应车辆状态;SystemUI 先保证状态栏和导航栏能用;设置应用先保证车辆设置能保存和加载。其他的,都是锦上添花。