后屏投影:UI 与 Tab 渲染体系
本篇梳理「Tab 数据如何驱动 UI 渲染」,从
CarUiTabLayout.Tab数据结构到ThemeItemView视觉表现。 调研来源:UI/Tab 渲染体系 Agent。
一、Tab 数据结构(CarUiTabLayout.Tab)
源码:
base/settingsBaseUi/src/main/java/com/android/car/settings/miauto/common/CarUiTabLayout.java:684-1336
Tab<T> 是 Tab 项的纯数据描述,不持有 View。后屏投影用 Tab<Any>,data 字段存命令枚举值(ScreenControlCommand.CLOSE/PROJECTION/SCREEN)。
核心字段
| 字段 | 类型 | 行号 | 作用 |
|---|---|---|---|
isAvailable | Boolean | :708 | 是否可用(false→灰色禁用) |
mIsSelected | boolean | :751 | 是否选中(选中→高亮) |
isProcessing | boolean | :755 | 是否 loading 中(过渡态→转圈) |
allowClickWhenDisabled | boolean | :757 | 禁用态是否仍可点击 |
data | T | :722 | 业务数据(后屏投影存命令枚举) |
mIconDrawableRes | int | :715 | 主图标资源 |
mBgDrawableRes | int | :727 | 背景资源 |
mScaleType | ScaleType | :758 | 图标缩放方式 |
mText / mSummary | CharSequence | :709/712 | 标题/副标题 |
mStaticMap | HashMap | :773 | 埋点参数 |
Tab 三态
isAvailable/mIsSelected/isProcessing是整个 UI 的状态核心,由基类updateTabByPropertyChange计算(见05-核心类拆解/03-基类模板.md§5.3)。
二、字段 → 视觉表现映射
flowchart TD A[Tab 数据] --> B{isAvailable?} B -->|false 灰色 alpha=0.4| C[禁用态] B -->|true| D[正常态] C --> G{allowClickWhenDisabled?} G -->|true| H[可点击 → onDisabledTabClicked 弹原因] G -->|false| I[静默忽略] D --> E{mIsSelected?} E -->|true| F[选中高亮 背景+文字样式] D & F --> J{isProcessing?} J -->|true| K[Loading 转圈 ProgressBar] J -->|false| L[显示正常图标 mIconDrawableRes] K & L --> M[mScaleType CENTER_INSIDE 缩放] M --> N[最终 Tab 视图]
| 字段 | false | true |
|---|---|---|
isAvailable | 灰色禁用(alpha=0.4) | 正常 |
mIsSelected | 普通态 | 选中高亮(背景 drawable + 文字样式) |
isProcessing | 显示正常图标 | 隐藏图标,显示 ProgressBar 转圈 |
三、ImageTabPreference 渲染机制
3.1 类层次
classDiagram class CarUiPreferenceAuto class BaseImageTabPreference~T~ { +Tab~?~[] mTabArr +ITabCallback mTabCallback +onBindViewHolder(holder) +setData(tabArr) +notifyDataChanged() } class ImageTabPreference { extends BaseImageTabPreference~ThemeItemView~ } class ThemeItemView { <<IImageItem 实现>> +setImageResource(resId) +setBgResource(res) +setScaleType(type) +setProcessing(b) +setSelected(b) +setEnabled(b) } CarUiPreferenceAuto <|-- BaseImageTabPreference BaseImageTabPreference <|-- ImageTabPreference BaseImageTabPreference ..> ThemeItemView : onBindViewHolder 中绑定
ImageTabPreference(preferences/ImageTabPreference.java:13)是空壳,逻辑在BaseImageTabPreference<T extends IImageItem>(:31)。- 泛型
T决定每个 Tab 项用哪种 View,后屏投影用ThemeItemView。
3.2 setData / notifyDataChanged
sequenceDiagram participant Ctrl as Controller participant P as ImageTabPreference participant RV as RecyclerView participant V as ThemeItemView Ctrl->>P: setData(mTabs) 保存 Tab 数组 Ctrl->>P: notifyDataChanged() → notifyChanged() P->>RV: 触发重新绑定 onBindViewHolder RV->>V: 遍历 Tab 数组,逐个绑定到 ThemeItemView
setData()(BaseImageTabPreference.java:170)只保存数组;notifyDataChanged()(:196)调 notifyChanged() 触发 Preference 重新绑定;绑定发生在 onBindViewHolder()(:73-168)。
3.3 Tab → View 绑定(BaseImageTabPreference.java:98-152)
for (int i = 0; i < tabLayout.getChildCount(); i++) {
View itemView = tabLayout.getChildAt(i);
if (itemView instanceof Space) continue;
((T) itemView).setEnabled(mTabArr[tabIndex].isAvailable()); // :108
((T) itemView).setImageResource(mTabArr[tabIndex].mIconDrawableRes); // :115
((T) itemView).setBgResource(mTabArr[tabIndex].mBgDrawableRes); // :117
((T) itemView).setScaleType(mTabArr[tabIndex].mScaleType); // :120
((T) itemView).setSelected(mTabArr[tabIndex].mIsSelected); // :130
((T) itemView).setProcessing(mTabArr[tabIndex].isProcessing); // :131
itemView.setAllowClickWhenDisabled(mTabArr[tabIndex].allowClickWhenDisabled); // :132
}每个 Tab 的数据字段被原样映射到 ThemeItemView 的对应 setter。
四、isProcessing → Loading 显示
base/settingsBaseUi/.../common/views/imageItem/ThemeItemView.kt:78-92
override fun setProcessing(processing: Boolean) {
if (mIsProcessing == processing) return
mIsProcessing = processing
updateProcessingVisibility()
}
private fun updateProcessingVisibility() {
if (mIsProcessing) {
mThemeImgView?.setImageResource(0) // 隐藏图标
mLoadingView?.visibility = VISIBLE // 显示 ProgressBar
} else {
mThemeImgView?.setImageResource(mIconDrawableRes) // 显示图标
mLoadingView?.visibility = GONE // 隐藏 ProgressBar
}
}效果:点击 Tab 瞬间 → isProcessing=true → 图标消失、转圈出现;状态回包到稳态 → isProcessing=false → 图标恢复。loading 样式由 R.style.MiCarSettingsCircularProgressIndicator 定义。
五、ITabCallback 回调分发
base/settingsBaseUi/.../preferences/NewTabPreference.kt:60-80
private val mTabClickCallBack = { tab: Tab<*>, isDisableClickTrigger: Boolean ->
if (!tab.isAvailable) {
if (tab.allowClickWhenDisabled && isDisableClickTrigger)
mTabCallback?.onDisabledTabClicked(tab) // 禁用可点
// else 静默
} else if (tab.mIsSelected) {
mTabCallback?.onSelectedTabClicked(tab) // 已选中再点
} else {
mTabCallback?.onSelectTabChanged(tab) // 切换 Tab
}
}| 回调 | 触发时机 | 后屏投影用途 |
|---|---|---|
onSelectTabChanged | 点未选中的可用 Tab | 下发命令(核心) |
onDisabledTabClicked | 点禁用 Tab 且 allowClickWhenDisabled | 弹不可用原因 |
onSelectedTabClicked | 点已选中 Tab | 默认空实现 |
onBuildMiCarViewDescription | 绑定时 | 构建语音可见即可说 |
六、setAllowClickWhenDisabled 机制 ⭐
后屏投影三个 Tab 都设了 .setAllowClickWhenDisabled(true)(RearProjectionModeTabController.kt:44,50,56)。
为什么:点禁用(灰色)Tab 时要提示具体不可用原因(如”空间不足""系统故障”),而不是无响应。这对用户体验至关重要——用户知道”为什么不能点”。
链路:Tab 字段 → BaseImageTabPreference.java:132 传给 View → View 点击时带 isDisableClickTrigger=true → 走 onDisabledTabClicked → Controller 的 showDisableReason 弹 toast。
详见
05-核心类拆解/01-RearProjectionModeTabController.md§4.9。
七、icon / bg / scaleType 加载
- icon:
setImageResource(resId)(ThemeItemView.kt:63),内部根据isProcessing决定显示图标还是 loading。 - bg:
setBgResource(res)(:68),mThemeImgView.background = AppCompatResources.getDrawable(...)。后屏投影用micar_rear_projection_bg(分日/夜)。 - scaleType:
setScaleType(CENTER_INSIDE)(:72-75)。CENTER_INSIDE 保持宽高比、完整显示不裁剪、可能留白——适合投影示意图。