鸿蒙HarmonyOS品牌主题引擎实战 —— 无限品牌主题 × 深浅色
一、前言品牌换肤的组合爆炸先看一个真实的业务场景。你的应用要上线企业版和个人版企业版用品牌蓝个人版用活力粉。之后又要加一个咖啡品牌合作主题、一个环保绿色主题。每个主题还要同时支持浅色和深色模式。传统做法// 每个页面都写品牌判断 Text(标题).fontColor(brand pink ? #D81B60 : #0A59F7) // 加一个品牌就要改所有页面 Text(标题).fontColor( brand pink ? #D81B60 : brand coffee ? #6F4E37 : brand green ? #2E7D32 : #0A59F7 )这导致两个严重问题组合爆炸5 个品牌 × 2 种亮度 10 种组合每个页面都要写一堆三元表达式。改一处动全身加一个品牌比如紫色要打开每个页面文件改一遍颜色判断。E012 的研究问题就是能否在已有的浅色/深色地基之上支持无限品牌主题且不修改任何业务页面代码答案是可以。关键在于把品牌和结构正交分离用语义令牌做映射让页面代码永远不知道当前是什么品牌。二、五层主题架构E012 把鸿蒙的主题能力整理成一个五层模型Layer 0 HarmonyOS Runtime $r(sys.color.*) 自动跟随系统 colorMode 切换深浅色 Layer 1 BrightnessHarmonyOS 拥有 WithTheme.colorMode: SYSTEM / LIGHT / DARK 来源AppStorage(BRIGHTNESS_STORAGE_KEY) → ThemeEngine.buildColorMode() Layer 2 BrandArkUILab 拥有 WithTheme.theme: CustomTheme with CustomColors overrides 来源AppStorage(BRAND_STORAGE_KEY) → ThemeEngine.buildCustomTheme() 品牌Default / Pink / Coffee / Green / Purple Layer 3 Semantic TokenArkUILab 拥有 ThemeTokens.ets - 结构锚定ThemePageBackground / ThemeTextPrimary / ...E011不变 - 品牌锚定ThemeBrandAccent / ThemeBrandButtonBackground / ThemeBrandGradientStart/EndE012 新增 Layer 4 Component TokenArkUILab 拥有部分 NavigationTitleBarTokens.ets 未来每个框架的专用令牌 Layer 5 Business / Demo 内容身份色、数据可视化、演示渐变谁拥有什么HarmonyOS 拥有Layer 0 运行时、Layer 1 亮度解析、Layer 2 通过 WithTheme 自动传播品牌到 HDS 内部组件。ArkUILab 拥有Layer 2 品牌调色板定义、Layer 2 AppStorage 状态管理驱动自定义组件、Layer 3-5 全部。最关键的判断品牌只管品牌令牌accent、button、gradient结构令牌background、text、surface、divider永远不变。品牌和结构正交任意品牌可以和任意亮度组合。Default Dark: 蓝色 accent 深色结构 Pink Dark: 粉色 accent 深色结构 Coffee Light: 棕色 accent 浅色结构三、双轨品牌传播机制这是 E012 最核心的技术发现。鸿蒙的WithTheme可以自动把品牌色传播给 HDS 内部组件但不能自动传播给 ArkUILab 自定义组件。必须用双轨机制3.1 轨道一WithTheme 自动传播HDS 内部组件WithTheme({ theme: pinkCustomTheme, colorMode: SYSTEM }) → HDS Button 自动变粉 → HDS TextField 自动变粉 → HDS Navigation bar 自动变粉WithTheme在其作用域内自动把CustomColors应用到所有 HDS 内部组件。你只需要在根组件包一层WithTheme。3.2 轨道二AppStorage StorageProp自定义组件但 ArkUILab 的自定义组件用的是ThemeTokens.ets里的$r(sys.color.*)或$r(app.color.*)——这些是静态资源引用在渲染时解析一次不会响应WithTheme的品牌变化。解法是用AppStorageStorageProp触发重新渲染加上读取 AppStorage 的品牌感知令牌函数ThemePlayground 选择 Pink ↓ ThemeEngine.setBrand(BrandTheme.Pink) ↓ AppStorage(BRAND_STORAGE_KEY) 更新 ↓ 所有声明了 StorageProp(BRAND_STORAGE_KEY) 的组件触发 rebuild ↓ build() 调用 ThemeBrandAccent() → 读 AppStorage → 返回粉色 hex ↓ WithTheme 也更新了根组件的 StorageProp ↓ HDS 内部组件通过 WithTheme 作用域拿到新的 CustomColors3.3 双轨对照表组件类型品牌响应方式HDS 内部组件Button 等WithTheme 自动HdsTabsBottomTabBarStyle不自动需要显式传.labelStyle({ selectedColor })和.iconStyle({ selectedColor })ArkUILab 自定义组件AppStorage StorageProp重新渲染 品牌感知令牌函数四、BrandTheme 实战五种品牌的定义4.1 品牌枚举和亮度枚举export const BRAND_STORAGE_KEY: string arkuilab_active_brand; export const BRIGHTNESS_STORAGE_KEY: string arkuilab_active_brightness; export enum BrandTheme { Default 0, Pink 1, Coffee 2, Green 3, Purple 4 } export enum ThemeBrightness { System 0, Light 1, Dark 2 }两个 Storage Key 用常量导出永远不要在业务代码里用裸字符串。4.2 CustomColors 实现HDS 内部组件品牌色每种品牌需要实现CustomColors接口这是WithTheme传播给 HDS 组件用的class PinkColors implements CustomColors { brand: ResourceColor #FFD81B60; backgroundEmphasize: ResourceColor #FFD81B60; compEmphasizePrimary: ResourceColor #FFD81B60; compEmphasizeSecondary: ResourceColor #33D81B60; iconEmphasize: ResourceColor #FFD81B60; fontEmphasize: ResourceColor #FFD81B60; }每种品牌一个 Colors 类覆盖brand、backgroundEmphasize、compEmphasizePrimary等 6 个核心令牌。CustomColors接口实际有 40 个可覆盖令牌但 v1 只覆盖最核心的几个。4.3 BrandPaletteEntry自定义组件品牌色每种品牌还需要一个BrandPaletteEntry这是 ArkUILab 自定义组件通过 AppStorage 读取品牌色用的export interface BrandPaletteEntry { name: string; description: string; accentLight: string; accentDark: string; buttonBgLight: string; buttonBgDark: string; gradientStartLight: string; gradientEndLight: string; gradientStartDark: string; gradientEndDark: string; customTheme: CustomTheme; } class PinkPaletteEntry implements BrandPaletteEntry { name: string 粉色; description: string 玫瑰粉色。生活方式和消费者应用程序的充满活力的品牌表达。; accentLight: string #FFD81B60; accentDark: string #FFE91E8C; buttonBgLight: string #FFD81B60; buttonBgDark: string #FFE91E8C; gradientStartLight: string #FFE4F0; gradientEndLight: string #FCE4EC; gradientStartDark: string #3D0A1E; gradientEndDark: string #2A0515; customTheme: CustomTheme new BrandCustomTheme(new PinkColors()); }注意每个颜色都有Light 和 Dark 两个版本——品牌色在深色模式下通常需要更亮如粉色#D81B60→#E91E8C渐变在深色模式下需要更暗如#FFE4F0→#3D0A1E。4.4 品牌调色板注册所有品牌注册到一个 Mapexport const BRAND_PALETTE: MapBrandTheme, BrandPaletteEntry new MapBrandTheme, BrandPaletteEntry([ [BrandTheme.Default, new DefaultPaletteEntry()], [BrandTheme.Pink, new PinkPaletteEntry()], [BrandTheme.Coffee, new CoffeePaletteEntry()], [BrandTheme.Green, new GreenPaletteEntry()], [BrandTheme.Purple, new PurplePaletteEntry()] ]);五、ThemeEngine 实战主题状态管理5.1 ThemeEngine 静态类ThemeEngine是品牌主题的中央状态管理器export class ThemeEngine { // 初始化 AppStorage 默认值 static init(): void { if (!AppStorage.has(BRAND_STORAGE_KEY)) { AppStorage.setOrCreateBrandTheme(BRAND_STORAGE_KEY, BrandTheme.Default); } if (!AppStorage.has(BRIGHTNESS_STORAGE_KEY)) { AppStorage.setOrCreateThemeBrightness(BRIGHTNESS_STORAGE_KEY, ThemeBrightness.System); } } // 品牌 getter/setter static activeBrand(): BrandTheme { return AppStorage.getBrandTheme(BRAND_STORAGE_KEY) ?? BrandTheme.Default; } static setBrand(brand: BrandTheme): void { AppStorage.setOrCreateBrandTheme(BRAND_STORAGE_KEY, brand); } // 亮度 getter/setter需要 context 来驱动系统 colorMode static setBrightness(brightness: ThemeBrightness, context?: Context): void { AppStorage.setOrCreateThemeBrightness(BRIGHTNESS_STORAGE_KEY, brightness); if (context ! undefined) { try { context.getApplicationContext().setColorMode(ThemeEngine._toConfigColorMode(brightness)); } catch (_e) { // setColorMode 是 best-effort } } } // 构建 WithTheme 需要的 CustomTheme static buildCustomTheme(brand: BrandTheme): CustomTheme | undefined { if (brand BrandTheme.Default) { return undefined; // Default 不应用 CustomTheme保留系统原色 } const entry: BrandPaletteEntry | undefined BRAND_PALETTE.get(brand); return entry?.customTheme; } // 构建 WithTheme 需要的 ThemeColorMode static buildColorMode(brightness: ThemeBrightness): ThemeColorMode { switch (brightness) { case ThemeBrightness.Light: return ThemeColorMode.LIGHT; case ThemeBrightness.Dark: return ThemeColorMode.DARK; default: return ThemeColorMode.SYSTEM; } } }5.2 根组件初始化在根组件ApplicationShell或Index的aboutToAppear里初始化aboutToAppear(): void { ThemeEngine.init(); }5.3 根组件包裹 WithThemeStorageProp(BRAND_STORAGE_KEY) private activeBrand: BrandTheme BrandTheme.Default; StorageProp(BRIGHTNESS_STORAGE_KEY) private activeBrightness: ThemeBrightness ThemeBrightness.System; build() { WithTheme({ theme: ThemeEngine.buildCustomTheme(this.activeBrand), colorMode: ThemeEngine.buildColorMode(this.activeBrightness) }) { // 应用内容 ApplicationShell() } }关键Default品牌返回undefined表示不应用任何CustomTheme保留 HarmonyOS 系统原色行为。六、品牌感知令牌ThemeBrandAccent 等6.1 品牌令牌函数E012 在ThemeTokens.ets新增了四个品牌感知令牌函数它们在渲染时读取 AppStoragefunction _activeBrandEntry(): BrandPaletteEntry | undefined { const brand: BrandTheme AppStorage.getBrandTheme(BRAND_STORAGE_KEY) ?? BrandTheme.Default; return BRAND_PALETTE.get(brand); } function _isDark(): boolean { const brightness: ThemeBrightness AppStorage.getThemeBrightness(BRIGHTNESS_STORAGE_KEY) ?? ThemeBrightness.System; return brightness ThemeBrightness.Dark; } export function ThemeBrandAccent(): ResourceColor { const entry: BrandPaletteEntry | undefined _activeBrandEntry(); if (entry undefined) { return $r(sys.color.ohos_id_color_primary); } return _isDark() ? entry.accentDark : entry.accentLight; } export function ThemeBrandButtonBackground(): ResourceColor { const entry: BrandPaletteEntry | undefined _activeBrandEntry(); if (entry undefined) { return $r(sys.color.ohos_id_color_activated); } return _isDark() ? entry.buttonBgDark : entry.buttonBgLight; } export function ThemeBrandGradientStart(): ResourceColor { /* 同上 */ } export function ThemeBrandGradientEnd(): ResourceColor { /* 同上 */ }6.2 什么时候用什么令牌场景正确令牌页面背景ThemePageBackground()← 结构令牌卡片表面ThemeSurfaceBackground()← 结构令牌主要文字ThemeTextPrimary()← 结构令牌分割线ThemeDividerColor()← 结构令牌品牌色图标 / 激活指示 / 选中态ThemeBrandAccent()← 品牌令牌填充主按钮ThemeBrandButtonBackground()← 品牌令牌Hero 渐变横幅ThemeBrandGradientStart/End()← 品牌令牌系统蓝按钮非品牌ThemePrimaryButtonBackground()← 结构令牌七、⚠️ 踩坑一Builder 响应式陷阱这是 E012最隐蔽、最耗时的坑。很多人声明了StorageProp却发现品牌色还是不更新。7.1 症状在ThemePlayground切换品牌后使用ThemeBrandAccent()的自定义组件不更新即使组件已经声明了StorageProp(BRAND_STORAGE_KEY)。7.2 根因ArkTS 的Builder依赖追踪只识别 Builder 体内对this.xxx状态变量的直接读取State/StorageProp/Prop/Link。它不会通过普通模块级函数调用传播响应式。品牌令牌函数ThemeBrandAccent()等是模块级函数内部读AppStorage.get()。但在Builder里编译器把它们当作返回常量的表达式永不重新执行 Builder。7.3 修复用实例方法读this.activeBrand// ❌ 错误Builder 内没有读 this.xxx品牌色永不更新 Builder private BrandChip() { Column().backgroundColor(ThemeBrandAccent()) } // ✅ 正确实例方法先读 this.activeBrand建立 StorageProp 依赖 private brandAccentColor(): ResourceColor { const brand this.activeBrand; // ← 建立 StorageProp 依赖 const brightness this.activeBrightness; // ← 可选用于深浅色变体 return ThemeBrandAccent(); } Builder private BrandChip() { Column().backgroundColor(this.brandAccentColor()) }关键在实例方法体内读一行this.activeBrand就能建立依赖关系让Builder在品牌变化时重新执行。7.4 调试清单当品牌色不更新时按顺序检查StorageProp(BRAND_STORAGE_KEY)声明了没令牌是在Builder里调用的吗如果在Builder里Builder 体内有没有读this.xxx没有就包一层实例方法。八、⚠️ 踩坑二BottomTabBarStyle 不继承 WithTheme这是 E012第二耗时的坑。8.1 症状切换品牌后HdsTabs 浮动底栏的选中色永远是系统蓝不跟随品牌。8.2 根因BottomTabBarStyle.of(icon, text)不读WithTheme作用域里的CustomColors.brand。它在 SDK 里硬编码了#FF007DFF作为默认selectedColor在tab_content.d.ts中确认。这意味着不管你怎么配WithTheme、怎么加StorageProp、怎么强制重挂载子树底栏选中色都不会变。8.3 被拒绝的方案强制重挂载 HdsTabsE012 尝试了用ForEachWatch递增 epoch 来强制重挂载HdsTabs。构建通过但真机测试后拒绝重挂载会销毁整个HdsTabs子树包括NavigationAppShell子组件导致NavPathStack重放在用户停留在ThemePlayground页面时每次品牌变化都把当前页面重新 push 一遍。太破坏性。8.4 正确修复显式传 labelStyle / iconStyle既然BottomTabBarStyle不读WithTheme就直接把品牌色喂给它private brandAccentColor(): ResourceColor { const brand this.activeBrand; // ← 同样要读 this.xxx 建立依赖 const brightness this.activeBrightness; return ThemeBrandAccent(); } private BottomTab(icon: Resource, text: string): BottomTabBarStyle { return BottomTabBarStyle.of(icon, text) .padding({ left: 4, right: 4, top: 4, bottom: 4 }) .layoutMode(LayoutMode.VERTICAL) .labelStyle({ selectedColor: this.brandAccentColor() }) // ← 品牌色 .iconStyle({ selectedColor: this.brandAccentColor() }); // ← 品牌色 }规则BottomTabBarStyle不继承WithTheme。迁移 HdsTabs 底栏品牌色时必须通过.labelStyle({ selectedColor })和.iconStyle({ selectedColor })显式传入。九、亮度切换为什么必须调 setColorMode9.1 WithTheme.colorMode vs ApplicationContext.setColorMode这两个 API 影响的范围完全不同API影响范围WithTheme.colorMode仅WithTheme 作用域内的 HDS 内部组件ApplicationContext.setColorMode()全 App的$r(sys.color.*)和$r(app.color.*)资源解析如果你只设了WithTheme.colorModeHDS 组件变暗了但你的页面背景$r(sys.color.*)还是浅色——因为系统级 colorMode 没变。9.2 正确做法ThemeEngine.setBrightness()同时做两件事static setBrightness(brightness: ThemeBrightness, context?: Context): void { // 1. 更新 AppStorage → 触发 StorageProp 重渲染 → 品牌渐变令牌拿到 dark/light 变体 AppStorage.setOrCreateThemeBrightness(BRIGHTNESS_STORAGE_KEY, brightness); // 2. 调用系统级 setColorMode → 切换 $r() 资源解析 if (context ! undefined) { try { context.getApplicationContext().setColorMode(ThemeEngine._toConfigColorMode(brightness)); } catch (_e) { /* best-effort */ } } }调用时需要传 contextconst ctx: Context | undefined this.getUIContext().getHostContext(); ThemeEngine.setBrightness(brightness, ctx);如果只调setBrightness(brightness)不传 context只有 AppStorage 状态更新了$r()资源色不会变。十、新增品牌的七步零改动流程E012 最强的能力新增一个品牌比如 MOKA零页面代码改动。七步流程1. 在BrandTheme枚举加一个值export enum BrandTheme { Default 0, Pink 1, Coffee 2, Green 3, Purple 4, MOKA 5 // 新增 }2. 实现CustomColors给 HDS 组件用class MOKAColors implements CustomColors { brand: ResourceColor #FF2D7DD2; backgroundEmphasize: ResourceColor #FF2D7DD2; compEmphasizePrimary: ResourceColor #FF2D7DD2; compEmphasizeSecondary: ResourceColor #332D7DD2; iconEmphasize: ResourceColor #FF2D7DD2; fontEmphasize: ResourceColor #FF2D7DD2; }3. 实现BrandPaletteEntry给自定义组件用class MOKAPaletteEntry implements BrandPaletteEntry { name: string MOKA; description: string MOKA brand theme.; accentLight: string #FF2D7DD2; accentDark: string #FF4A9EE8; buttonBgLight: string #FF2D7DD2; buttonBgDark: string #FF4A9EE8; gradientStartLight: string #E3F2FD; gradientEndLight: string #E8F5E9; gradientStartDark: string #0D2840; gradientEndDark: string #0D2E10; customTheme: CustomTheme new BrandCustomTheme(new MOKAColors()); }4. 加入BRAND_PALETTEMap[BrandTheme.MOKA, new MOKAPaletteEntry()]5. 加入ThemeEngine.allBrands()static allBrands(): BrandTheme[] { return [BrandTheme.Default, BrandTheme.Pink, BrandTheme.Coffee, BrandTheme.Green, BrandTheme.Purple, BrandTheme.MOKA]; }6. 在base/dark color.json加品牌色资源{ name: arkuilab_brand_moka_accent, value: #FF2D7DD2 }, { name: arkuilab_brand_moka_button, value: #FF2D7DD2 }7. 构建验证。完成。不需要改任何页面或框架代码。ThemePlayground自动显示新品牌选项所有用了品牌令牌的组件自动响应。十一、品牌主题能力地图Theme Engine v1 能力覆盖 ├── Brightness亮度 ✓ 完成 │ ├── System跟随系统 │ ├── Light强制浅色 │ └── Dark强制深色 │ ├── Brand品牌 ✓ 完成 │ ├── DefaultHarmonyOS 系统蓝 │ ├── Pink玫瑰粉 │ ├── Coffee烤成棕 │ ├── Green森林绿 │ └── Purple深紫罗兰 │ ├── Semantic Token语义令牌 ✓ 完成 │ ├── 结构令牌E011 │ └── 品牌令牌E012 │ ├── Typography字体 ✗ 未来 ├── Radius圆角 ✗ 未来 ├── Shadow阴影 ~ 部分ThemeCardShadowColor ├── Material材质 ~ 部分API 26 systemMaterial 待定 └── Motion动效 ✗ 未来Typography、Radius、Motion 属于更广义的 Design System 范畴留给未来实验。十二、框架品牌就绪审计框架品牌状态需要迁移吗NavigationHDS 内部WithTheme 自动无FloatingTabBarE012 完成已完成ApplicationShellE012 完成已完成Overlay Framework仅结构令牌无Menu Framework仅结构令牌 系统色无Selection Framework仅结构令牌无Feedback Framework仅结构令牌无Search Framework仅结构令牌无关键结论只用了结构令牌的框架不需要品牌迁移——结构令牌页面背景、文字、分割线不受品牌影响它们通过系统资源自动跟随深浅色。十三、最佳实践清单13.1 令牌选择✅ 品牌 iconName / link 文字 / 激活指示 / 选中态用ThemeBrandAccent()。✅ 填充主按钮用ThemeBrandButtonBackground()搭配对比度足够的前景色。✅ Hero 横幅 / 色调标题 / 特色卡片用ThemeBrandGradientStart/End()。✅ 结构 UI背景、文字、表面、分割线、阴影、蒙层必须用结构令牌不用品牌令牌。13.2 StorageProp 规则✅ 用了品牌令牌的组件必须声明StorageProp(BRAND_STORAGE_KEY)。✅ 品牌令牌在Builder内调用时必须用实例方法先读this.activeBrand。✅ 只用结构令牌的组件不需要StorageProp——它们通过系统资源自动更新。✅ 永远用导出的常量BRAND_STORAGE_KEY不用裸字符串。13.3 WithTheme 配置✅ 整个应用根组件用WithTheme包裹。✅ 在根组件aboutToAppear()里调ThemeEngine.init()。✅BrandTheme.Default在buildCustomTheme()里返回undefined保留系统原色。✅ 亮度切换时调ThemeEngine.setBrightness(brightness, context)必须传 context。13.4 BottomTabBarStyle✅BottomTabBarStyle.of()不读WithTheme必须显式传labelStyle.selectedColor和iconStyle.selectedColor。✅ 不要用强制重挂载 HdsTabs 来更新底栏色——太破坏性。十四、常见错误对照表错误做法问题正确做法if (brand pink)写在每个页面加品牌改所有页面用ThemeBrandAccent()语义令牌声明了StorageProp但 Builder 不更新Builder 不跟踪模块级函数实例方法先读this.activeBrandHdsTabs 选中色不变BottomTabBarStyle 不读 WithThemelabelStyle.selectedColor显式传setBrightness不传 context$r()资源色不切换传getHostContext()Default 品牌返回 CustomTheme覆盖了系统原色Default 返回undefined用裸字符串arkuilab_active_brand容易拼写错误用BRAND_STORAGE_KEY常量品牌令牌用在结构 UI页面背景品牌和结构耦合结构 UI 用结构令牌强制重挂载 HdsTabsNavPathStack 重放用labelStyle/iconStyleAPI品牌色只有 Light 值深色模式下品牌色刺眼每个品牌色都提供 Light Dark直接用$r(sys.color.primary)不可换品牌用ThemeBrandAccent()包装层十五、审计命令E012 沉淀了三条审计命令# 统计品牌令牌使用 rg ThemeBrandAccent|ThemeBrandButtonBackground|ThemeBrandGradient entry/src/main/ets -g *.ets -l # 找用了品牌令牌但缺 StorageProp 的组件 rg ThemeBrandAccent|ThemeBrandGradient entry/src/main/ets -g *.ets -l \ | xargs -I{} sh -c rg BRAND_STORAGE_KEY {} /dev/null || echo MISSING StorageProp: {} # 统计所有 ThemeToken 使用频率 rg Theme[A-Z][a-zA-Z]\(\) entry/src/main/ets -g *.ets -o | sort | uniq -c | sort -nr建议加入 CI自动检测缺StorageProp的品牌令牌消费者。十六、当前限制与后续方向E012 的 Brand Theme Engine v1 已经成立没有已知未解决的问题。仍需未来实验的能力待办项说明Typography 令牌层字体族、字体缩放、字重层级Radius 令牌层圆角比例Shadow 令牌层按亮度的海拔模型不只是单一颜色资源Motion 令牌层时长、曲线、品牌交互动效API 26systemMaterial/menuSystemMaterial等本地 SDK 暴露自适应状态栏/导航栏内容色uiAppearance或窗口 APICI 审计脚本自动扫描缺StorageProp的组件暂不创建ThemeConfiguration——ThemeEngine已经覆盖了所有必要状态管理。ThemeControl.setDefaultTheme()全局调用 —— 根组件用WithTheme足够且更明确。写在最后品牌换肤的本质不是给每个品牌写一套颜色而是建立一个映射层让页面代码永远不知道当前是什么品牌。E012 的 Theme Engine v1 实现了这个目标二维主题Brand × Brightness 任意组合双轨传播WithTheme 自动HDS 组件 AppStorage 手动自定义组件零改动新增品牌七步流程不碰任何页面代码品牌和结构正交品牌只管 accent/button/gradient结构永远是系统令牌但这个过程里踩了两个非常深的坑Builder 响应式陷阱声明了StorageProp还不够Builder 体内必须读this.xxx才能触发重执行。BottomTabBarStyle 不继承 WithThemeHdsTabs 选中色硬编码系统蓝必须通过labelStyle/iconStyle显式传。这两个坑的共同特点是编译通过、类型安全但运行时不更新。它们无法通过构建发现只能通过真机切换品牌才能暴露。记住这几条口诀品牌只管 accent 色结构永远系统令牌。 WithTheme 管 HDSAppStorage 管自定义。 Builder 里品牌不更新实例方法读 this 才行。 BottomTabBar 不继承 WithThemelabelStyle 显式传。 亮度切换要 contextsetColorMode 不可少。 Default 返回 undefined保留系统原色好。 新增品牌七步走页面代码零改动。本文的实现基于 ArkUILab 项目 E012 实验。如果你正在做一个需要多品牌主题的鸿蒙应用可以直接复用这套 Brand × Brightness 二维引擎和双轨传播机制作为起点。品牌换肤看似只是换个颜色背后却涉及令牌分层、双轨传播、响应式陷阱、组件 API 边界和亮度系统级控制等多个维度。把这些维度拆清楚你的品牌主题引擎就不会加了品牌要改全站了。