后屏投影: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)。

核心字段

字段类型行号作用
isAvailableBoolean:708是否可用(false→灰色禁用)
mIsSelectedboolean:751是否选中(选中→高亮)
isProcessingboolean:755是否 loading 中(过渡态→转圈)
allowClickWhenDisabledboolean:757禁用态是否仍可点击
dataT:722业务数据(后屏投影存命令枚举)
mIconDrawableResint:715主图标资源
mBgDrawableResint:727背景资源
mScaleTypeScaleType:758图标缩放方式
mText / mSummaryCharSequence:709/712标题/副标题
mStaticMapHashMap: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 视图]
字段falsetrue
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 保持宽高比、完整显示不裁剪、可能留白——适合投影示意图。