03 - Controller 与 UI 刷新范式
本篇讲项目主流的信号驱动 UI 模式:基于 AndroidX Preference +
PreferenceController的回调模式。这是绝大多数设置页(灯光、锁车、车身、驾驶…)的写法。新的 Flow 模式见 04 篇。两套并存。
1. 核心思想
项目不用 DataBinding / ViewBinding,而是用 AndroidX Preference 体系(实际是定制版 micarx.preference.PreferenceFragment):
- 页面用 XML 声明一组
Preference,每个 Preference 用settings:controller="全限定类名"绑定一个 Controller - Fragment 在
onAttach时反射实例化所有 Controller,绑定到生命周期 - Controller 内部:声明关心的信号 → 收到信号回调时刷对应 Preference 视图;用户操作时下发信号
信号与 UI 的连接全部在 Controller 里,Fragment 本身不碰信号。
2. 类继承体系
2.1 Controller 继承链
PreferenceController<V> (settingsBaseUi,所有 Controller 之祖)
└─ CarPropertyMgrPreferenceController<V> (settingsVehicleLib,★ 车控信号脊梁)
├─ BaseVehicleNewSwitchPrefController (开关型,121 文件)
├─ BaseVehicleTabPreferenceController (文字 Tab)
├─ BaseVehicleImageTabPreferenceController (图标 Tab,与上"兄弟"非父子)
├─ BaseVehicleProgressPrefController (SeekBar/进度)
└─ BaseVehicleButtonGroupPropController (按钮组)
⚠️ 三种业务基类是兄弟关系:
BaseVehicleImageTabPreferenceController与BaseVehicleTabPreferenceController都直接继承CarPropertyMgrPreferenceController(BaseVehicleImageTabPreferenceController.kt:25-26注释明确说明),不是父子。
另有非 PreferenceController 体系的 Controller:
BaseCarPropertyController (Kotlin) ← Fragment/Dialog 用,更轻量
└─ FragmentVehicleController ← Fragment 级集中订阅(如灯光右侧车模页)
2.2 Fragment 继承链
androidx.Fragment
└─ BaseFragment (最薄,非 Preference 页才用)
└─ micarx.preference.PreferenceFragment
└─ BaseXmlParserSettingsFragment (★ 核心:XML 解析 + Controller 管理 + ViewModel)
├─ SettingsFragment (薄包装,仅 Title)
│ └─ TopLevelSettingsFragment (一级页:锚点跳转、EventBus、双栏)
│ └─ EnergyManagerFragment / LightsSettingsFragment / ...
└─ SecondLevelSettingsFragment (二级页"车机管家",自带 Toolbar)
└─ BaseRightFragment (右侧车模图容器,独立链)
2.3 新建页面时该继承谁
| 场景 | 继承 |
|---|---|
| 一级设置页(右侧车模 + Preference 列表 + 锚点) | TopLevelSettingsFragment |
| 二级设置页(“车机管家”风格,独立 Toolbar) | SecondLevelSettingsFragment |
| 普通 Preference 列表页 | SettingsFragment |
| 非 Preference 自定义 View 页 | BaseFragment |
| 单个开关设置项 | BaseVehicleNewSwitchPrefController |
| 文字 Tab 设置项 | BaseVehicleTabPreferenceController |
| 图标 Tab 设置项 | BaseVehicleImageTabPreferenceController |
| 进度条设置项 | BaseVehicleProgressPrefController |
| Fragment 级集中订阅多个信号 | FragmentVehicleController(或 BaseCarPropertyController) |
3. 关键 Base 类
3.1 PreferenceController(根基类)
路径:base/settingsBaseUi/src/main/java/com/android/car/settings/common/PreferenceController.java
- 实现
DefaultLifecycleObserver:onCreateInternal / onStartInternal / ... / onDestroyInternal自动派发 updateState(preference):单点刷新入口handlePreferenceChanged / handlePreferenceClicked:用户事件refreshUi()(L359-375)getAvailabilityStatus():控制显隐/可用requestDataFromHost / postDataToHost:走 Fragment 命令通道(Controller ↔ Fragment 通信)
3.2 CarPropertyMgrPreferenceController(车控脊梁)
路径:base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/common/CarPropertyMgrPreferenceController.java
- 实现
CarPropertyManagerCallback(L56) onCreateInternal(L511-518):new SettingsCarPropertyManager(ctx, settingsLifecycle),自动绑生命周期- 收到 CarService ready 后自动注册回调(
onPropertyManagerPreparedL294 →registerPropertyCallbackL269)
关键模板方法(子类可 override):
| 方法 | 行号 | 作用 |
|---|---|---|
getPropertyIdSet() | L191(abstract) | 子类必填:声明下行/主信号 ID 集合 |
getObservedPropertyIdSet() | L193 | 默认 = getPropertyIdSet();要监听额外依赖信号时 override |
getAreaId() | L206 | 多区域信号(车门、座椅)必须 override |
getConfigPropertyRates() | L198 | 配置采样率 |
getSubDependPropertyId() | L202 | 父子项依赖 |
onHandlePropertyChange(value, shouldUpdateUI) | L492 | 子类刷 UI 的入口 |
shouldIgnorePropertyFeedback(propId, areaId) | L688 | 是否忽略这次回包 |
shouldUpdateUIByPropVal(value) | L426 | 预估值/无效值场景返回 false(UI 保持上一态,只置灰) |
getCurrentIntermediateType(propId, propVal) | L661 | 中间态(loading)支持 |
obtainSetPropertyId() | L651 | 下行用 propId(下行/上行 ID 不一致时 override) |
setVehicleProperty(propId, propVal, areaId, isSync) | L620 | 子类下发入口,已封装类型分发 |
restorePropertyToPrevious(propId, areaId) | L125 | 失败/超时回滚 UI(基类自动处理) |
3.3 BaseVehicleNewSwitchPrefController(开关专用)
路径:base/settingsVehicleLib/.../miauto/vehicle/BaseVehicleNewSwitchPrefController.kt
开关类的”取反 + 写”被自动化了:
// L189-197 父类已实现
protected open fun setCarProperty(isChecked: Boolean) {
if (needIntermediateState()) preference.isEnabled = false
setVehicleProperty(obtainSetPropertyId(), getPropertyVal(isChecked), areaId)
updatePreferencesVisibleWhenStatusChange(isChecked)
}
// L48-68 信号回传刷 UI
override fun onHandlePropertyChange(propertyValue, shouldUpdateUI) {
// areaId 过滤后
preference.setChecked(isPropertyOpen(propVal))
}子类只需声明 getPropertyIdSet(),按需 override isPropertyOpen / getPropertyVal / needSecondConfirm / needIntermediateState / isValidPropVal。
枚举型开关(如 LockBeep)重写 getPropertyVal(boolean) 和 isPropertyOpen(int),见 LockBeepController → BaseLockBeepController → BaseVehicleNewSwitchPrefController 的”抽两层 Base”技巧。
3.4 BaseXmlParserSettingsFragment(Fragment 核心)
路径:base/settingsBaseUi/.../common/BaseXmlParserSettingsFragment.java
- XML 单次解析生成
PreferenceController列表(L301-322,反射settings:controller属性) - 生命周期分发给 Controller
getViewModel()/getDefaultViewModel()(L1406-1419)DefaultViewModel内置ArrayMap<Class, MutableLiveData>通用 LiveData 缓存(L1421-1449)——Controller 间通信的通用通道- 承载
BaseRightFragment作为子 Fragment(L554-567) use(Class, key)取 Controller(L225-242)
4. 信号注册链路(Controller 创建时)
CarPropertyMgrPreferenceController.onCreateInternal (L511)
└─► new SettingsCarPropertyManager(ctx, settingsLifecycle) // 自动绑生命周期
onPostCreateInternal (L521)
└─► mCarPropertyManager.setCarManagerCallback(this) // 注册 service listener
[CarService ready 回调]
└─► onPropertyManagerPrepared (L294)
└─► registerPropertyCallback() (L269)
└─► mCarPropertyManager.registerPropertyCallback(getObservedPropertyIdSet())
└─► VehicleControlManager.registerPropertyCallback
└─► CarPropertyCacheManager.registerPropertyCallback // 首次才向 server 注册
⚠️ 易错点:
registerPropertyCallback必须在onPropertyManagerPrepared之后调,否则服务没连上注册失败。基类已处理好,子类不要自己调。
5. 信号回传链路(车端 → UI)
车端 ECU → CarService → AOSP CarPropertyManager
└─► CarPropertyCacheManager.mInternalCarPropertyEventCallback.onChangeEvent (L61)
├─► mPropertiesCache.put(propId#areaId, value) // 全局缓存
└─► 遍历 mCallBackMap[propId](去重后的 dispatcher)
└─► SettingsCarPropertyEventCallback.onChangeEvent (L662)
├─► cachePropVal // per-instance 缓存
├─► processTimeout // 终止/重启超时
└─► mCallback.onPropertyChanged(value) // 命中观察集才回调
└─► CarPropertyMgrPreferenceController.onPropertyChanged (L346)
├─► shouldIgnorePropertyFeedback / shouldUpdateUIByPropVal 过滤
├─► IVehiclePerformFeature.needMapReceiveDependProperty → 依赖信号映射 (L360-380)
└─► onHandlePropertyChange(value, shouldUpdateUI) (L492)
└─► 子类刷新具体 UI(setChecked / setTab / setLoading…)
关键:车辆 Controller 的刷新逻辑在
onHandlePropertyChange里,不是在updateState里。
6. 信号回调接口:CarPropertyManagerCallback
路径:base/settingsVehicleLib/.../miauto/vehicle/CarPropertyManagerCallback.java(全 default 方法)
| 方法 | 触发时机 |
|---|---|
onPropertyManagerPrepared() | CarService 连接就绪 |
onPropertyManagerReleased() | CarService 断开 |
onPropertyChanged(value) | 信号变化(仅 observedSet 内的 propId) |
onSetPropertyTimeout(propId, areaId) | 下发超时未回弹 |
onSetPropertyError(propId, areaId) | 底层 onErrorEvent |
onDataVerifyFailed(propId, areaId, val) | 下发值 ≠ 回弹缓存值 |
getTimeoutFlag(value) | CONTINUE / RESTART / REMOVE |
isIgnoreCache(value) | 该值是否跳过缓存 |
reportOneTrackWhenSetProperty(...) | 下发埋点 |
⚠️ 常见坑:如果业务忘了把 propId
registerPropertyCallback进 observedSet,即使底层有事件,onPropertyChanged也不会回调,只打 w 级警告"un-excepted observe propId"。
7. UI 刷新动作
onHandlePropertyChange 里直接操作 Preference 视图:
// 开关勾选
preference.setChecked(isPropertyOpen(propVal))
// 中间加载态(需 needIntermediateState()=true)
preference.setLoading(isInLoadingState(value))
// 可用性(不走原生 AVAILABLE_FOR_VIEWING)
preference.isEnabled = isPropertyAvailable(...) && isInnerLogicAvailable(...)- areaId 过滤:
onHandlePropertyChange第一步检查areaId,避免一个信号触发多个开关 - 失败回滚:
onSetPropertyTimeout/onSetPropertyError→restorePropertyToPrevious(基类自动,子类无需关心) - 配置字驱动可见性:Fragment 的
getPreferenceKeyResIdsToRemove()按车型/配置字移除整个 Preference;Controller 的getAvailabilityStatus()处理静态判断 - 缓存兜底:
VehiclePropertyConfig.getPropValueForFirstCache(value)让 UI 首次拿到终态值避免抖动(前提是首次缓存前有getProperty过)
8. Feature 位接口:IVehiclePerformFeature
路径:base/settingsVehicleLib/.../com/android/micar/settings/carproperty/IVehiclePerformFeature.java(注意包名差异)
用位标志挂载可复用功能,避免重复造轮子:
int FEATURE_NONE = 0;
int FEATURE_REFRESH_SUMMARY_ON_RESUME = 1 << 1;
// 后续 1 << n 扩展Controller extends ... implements IVehiclePerformFeature,重写:
needMapReceiveDependProperty():把依赖信号(错误码)映射成主信号刷新featureShouldDoubleConfirm(boolean):二次确认featureDisableReason():禁用原因featureIgnorePropertyValues(propId):忽略的信号值
范例:CreepSpeedPreferenceController(settingsPage/micarDrivingSettings/.../CreepSpeedPreferenceController.java:24)
@Override
public ArraySet<Integer> getPropertyIdSet() {
return new ArraySet<>(Set.of(MiCarPropertyIds.DrivingCtrl.CREEP_SPEED_STATUS)); // 主信号
}
@Override
public ArraySet<Integer> getObservedPropertyIdSet() {
ArraySet<Integer> set = new ArraySet<>(getPropertyIdSet());
set.add(MiCarPropertyIds.DrivingCtrl.CREEP_SPEED_ERROR_REASON); // 追加依赖信号
return set;
}
@Override
public boolean needMapReceiveDependProperty() { return true; } // 基类自动映射9. 车模联动
点击设置项时同步切换右侧车模图:
CarPropertyMgrPreferenceController.java:743-768 changeCarModelWhenClick:
getDefaultViewModel()
.getLiveData(CarModelData.class)
.setValue(new CarModelData(...)); // 通过 DefaultViewModel 通用 LiveData 通道推给右侧 BaseRightFragmentCarModelData 路径:base/settingsBaseUi/.../common/livedata/CarModelData.kt
10. 范例:最小开关 Controller(26 行)
settingsPage/micarLockSettings/src/main/java/com/android/car/settings/miauto/lock/WalkAwayLockController.kt
class WalkAwayLockController(
context: Context, preferenceKey: String,
fragmentController: FragmentController, uxRestrictions: CarUxRestrictions
) : BaseVehicleNewSwitchPrefController(context, preferenceKey, fragmentController, uxRestrictions) {
override fun getPropertyIdSet(): ArraySet<Int> = ArraySet<Int>().apply {
add(MiCarPropertyIds.LockCtrl.WALK_AWAY_LOCK) // 唯一必填
}
}基类已做完:注册 callback、缓存、超时、回滚、UI 刷新、二次确认。
11. 范例:Fragment 级集中订阅(灯光车模页)
settingsPage/micarLightSettings/.../LightsRightFragmentController.kt 继承 FragmentVehicleController:
- 集中订阅一束灯光状态信号(
POSITION_BEAM_STATUS、HIGH_BEAM_STATUS、MOOD_LIGHT_TRIGGER等) - 在
onPropertyChanged里驱动右侧车模图片切换
这种模式适合”一个页面整体联动”,而非单个 Preference。
12. 关键路径速查
| 主题 | 路径 |
|---|---|
| Controller 根基类 | base/settingsBaseUi/.../common/PreferenceController.java |
| 车控脊梁 | base/settingsVehicleLib/.../miauto/common/CarPropertyMgrPreferenceController.java |
| 开关基类 | base/settingsVehicleLib/.../miauto/vehicle/BaseVehicleNewSwitchPrefController.kt |
| 文字 Tab 基类 | base/settingsVehicleLib/.../miauto/vehicle/BaseVehicleTabPreferenceController.kt |
| 图标 Tab 基类 | base/settingsVehicleLib/.../miauto/vehicle/BaseVehicleImageTabPreferenceController.kt |
| Fragment 级基类 | base/settingsVehicleLib/.../miauto/common/BaseCarPropertyController.kt |
| FragmentVehicleController | base/settingsVehicleLib/.../com/android/micar/settings/common/FragmentVehicleController.java |
| Fragment 核心 | base/settingsBaseUi/.../common/BaseXmlParserSettingsFragment.java |
| 一级页基类 | base/settingsBaseUi/.../miauto/TopLevelSettingsFragment.java |
| Feature 位接口 | base/settingsVehicleLib/.../com/android/micar/settings/carproperty/IVehiclePerformFeature.java |
| 最小开关范例 | settingsPage/micarLockSettings/.../lock/WalkAwayLockController.kt |
| 依赖信号范例 | settingsPage/micarDrivingSettings/.../driving/CreepSpeedPreferenceController.java |
新增 Controller 的完整步骤见 09 开发模板。