鸿蒙HarmonyOS品牌主题引擎实战 —— 无限品牌主题 × 深浅色
2026/7/13 4:12:39 网站建设 项目流程

一、前言:品牌换肤的"组合爆炸"

先看一个真实的业务场景。

你的应用要上线企业版和个人版,企业版用品牌蓝,个人版用活力粉。之后又要加一个咖啡品牌合作主题、一个环保绿色主题。每个主题还要同时支持浅色和深色模式。

传统做法:

// 每个页面都写品牌判断 Text('标题').fontColor(brand === 'pink' ? '#D81B60' : '#0A59F7') // 加一个品牌就要改所有页面 Text('标题').fontColor( brand === 'pink' ? '#D81B60' : brand === 'coffee' ? '#6F4E37' : brand === 'green' ? '#2E7D32' : '#0A59F7' )

这导致两个严重问题:

  1. 组合爆炸:5 个品牌 × 2 种亮度 = 10 种组合,每个页面都要写一堆三元表达式。

  2. 改一处动全身:加一个品牌(比如紫色),要打开每个页面文件改一遍颜色判断。

E012 的研究问题就是:

能否在已有的浅色/深色地基之上,支持无限品牌主题,且不修改任何业务页面代码?

答案是:可以。关键在于把"品牌"和"结构"正交分离,用语义令牌做映射,让页面代码永远不知道当前是什么品牌。


二、五层主题架构

E012 把鸿蒙的主题能力整理成一个五层模型:

Layer 0 HarmonyOS Runtime $r('sys.color.*') 自动跟随系统 colorMode 切换深浅色 Layer 1 Brightness(HarmonyOS 拥有) WithTheme.colorMode: SYSTEM / LIGHT / DARK 来源:AppStorage(BRIGHTNESS_STORAGE_KEY) → ThemeEngine.buildColorMode() Layer 2 Brand(ArkUILab 拥有) WithTheme.theme: CustomTheme with CustomColors overrides 来源:AppStorage(BRAND_STORAGE_KEY) → ThemeEngine.buildCustomTheme() 品牌:Default / Pink / Coffee / Green / Purple Layer 3 Semantic Token(ArkUILab 拥有) ThemeTokens.ets - 结构锚定:ThemePageBackground / ThemeTextPrimary / ...(E011,不变) - 品牌锚定:ThemeBrandAccent / ThemeBrandButtonBackground / ThemeBrandGradientStart/End(E012 新增) Layer 4 Component Token(ArkUILab 拥有,部分) 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的品牌变化。

解法是用AppStorage+@StorageProp触发重新渲染,加上读取 AppStorage 的品牌感知令牌函数:

ThemePlayground 选择 Pink ↓ ThemeEngine.setBrand(BrandTheme.Pink) ↓ AppStorage(BRAND_STORAGE_KEY) 更新 ↓ 所有声明了 @StorageProp(BRAND_STORAGE_KEY) 的组件触发 rebuild ↓ build() 调用 ThemeBrandAccent() → 读 AppStorage → 返回粉色 hex ↓ WithTheme 也更新了(根组件的 @StorageProp) ↓ HDS 内部组件通过 WithTheme 作用域拿到新的 CustomColors

3.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 类,覆盖brandbackgroundEmphasizecompEmphasizePrimary等 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 品牌调色板注册

所有品牌注册到一个 Map:

export const BRAND_PALETTE: Map<BrandTheme, BrandPaletteEntry> = new Map<BrandTheme, 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.setOrCreate<BrandTheme>(BRAND_STORAGE_KEY, BrandTheme.Default); } if (!AppStorage.has(BRIGHTNESS_STORAGE_KEY)) { AppStorage.setOrCreate<ThemeBrightness>(BRIGHTNESS_STORAGE_KEY, ThemeBrightness.System); } } // 品牌 getter/setter static activeBrand(): BrandTheme { return AppStorage.get<BrandTheme>(BRAND_STORAGE_KEY) ?? BrandTheme.Default; } static setBrand(brand: BrandTheme): void { AppStorage.setOrCreate<BrandTheme>(BRAND_STORAGE_KEY, brand); } // 亮度 getter/setter(需要 context 来驱动系统 colorMode) static setBrightness(brightness: ThemeBrightness, context?: Context): void { AppStorage.setOrCreate<ThemeBrightness>(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 根组件初始化

在根组件(ApplicationShellIndex)的aboutToAppear里初始化:

aboutToAppear(): void { ThemeEngine.init(); }

5.3 根组件包裹 WithTheme

@StorageProp(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新增了四个品牌感知令牌函数,它们在渲染时读取 AppStorage:

function _activeBrandEntry(): BrandPaletteEntry | undefined { const brand: BrandTheme = AppStorage.get<BrandTheme>(BRAND_STORAGE_KEY) ?? BrandTheme.Default; return BRAND_PALETTE.get(brand); } function _isDark(): boolean { const brightness: ThemeBrightness = AppStorage.get<ThemeBrightness>(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 调试清单

当品牌色不更新时,按顺序检查:

  1. @StorageProp(BRAND_STORAGE_KEY)声明了没?

  2. 令牌是在@Builder里调用的吗?

  3. 如果在@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 被拒绝的方案:强制重挂载 HdsTabs

E012 尝试了用ForEach+@Watch递增 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 })显式传入。


九、亮度切换:为什么必须调 setColorMode

9.1 WithTheme.colorMode vs ApplicationContext.setColorMode

这两个 API 影响的范围完全不同

API

影响范围

WithTheme.colorMode

WithTheme 作用域内的 HDS 内部组件

ApplicationContext.setColorMode()

全 App$r('sys.color.*')$r('app.color.*')资源解析

如果你只设了WithTheme.colorMode,HDS 组件变暗了,但你的页面背景($r('sys.color.*'))还是浅色——因为系统级 colorMode 没变。

9.2 正确做法

ThemeEngine.setBrightness()同时做两件事:

static setBrightness(brightness: ThemeBrightness, context?: Context): void { // 1. 更新 AppStorage → 触发 @StorageProp 重渲染 → 品牌渐变令牌拿到 dark/light 变体 AppStorage.setOrCreate<ThemeBrightness>(BRIGHTNESS_STORAGE_KEY, brightness); // 2. 调用系统级 setColorMode → 切换 $r() 资源解析 if (context !== undefined) { try { context.getApplicationContext().setColorMode(ThemeEngine._toConfigColorMode(brightness)); } catch (_e) { /* best-effort */ } } }

调用时需要传 context:

const 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(品牌) ✓ 完成 │ ├── Default(HarmonyOS 系统蓝) │ ├── Pink(玫瑰粉) │ ├── Coffee(烤成棕) │ ├── Green(森林绿) │ └── Purple(深紫罗兰) │ ├── Semantic Token(语义令牌) ✓ 完成 │ ├── 结构令牌(E011) │ └── 品牌令牌(E012) │ ├── Typography(字体) ✗ 未来 ├── Radius(圆角) ✗ 未来 ├── Shadow(阴影) ~ 部分(ThemeCardShadowColor) ├── Material(材质) ~ 部分(API 26 systemMaterial 待定) └── Motion(动效) ✗ 未来

Typography、Radius、Motion 属于更广义的 Design System 范畴,留给未来实验。


十二、框架品牌就绪审计

框架

品牌状态

需要迁移吗

Navigation(HDS 内部)

WithTheme 自动

FloatingTabBar

E012 完成

已完成

ApplicationShell

E012 完成

已完成

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.DefaultbuildCustomTheme()里返回undefined,保留系统原色。

  • ✅ 亮度切换时调ThemeEngine.setBrightness(brightness, context),必须传 context。

13.4 BottomTabBarStyle

  • BottomTabBarStyle.of()不读WithTheme,必须显式传labelStyle.selectedColoriconStyle.selectedColor

  • ✅ 不要用强制重挂载 HdsTabs 来更新底栏色——太破坏性。


十四、常见错误对照表

错误做法

问题

正确做法

if (brand === 'pink')写在每个页面

加品牌改所有页面

ThemeBrandAccent()语义令牌

声明了@StorageProp但 Builder 不更新

@Builder 不跟踪模块级函数

实例方法先读this.activeBrand

HdsTabs 选中色不变

BottomTabBarStyle 不读 WithTheme

labelStyle.selectedColor显式传

setBrightness不传 context

$r()资源色不切换

getHostContext()

Default 品牌返回 CustomTheme

覆盖了系统原色

Default 返回undefined

用裸字符串'arkuilab_active_brand'

容易拼写错误

BRAND_STORAGE_KEY常量

品牌令牌用在结构 UI(页面背景)

品牌和结构耦合

结构 UI 用结构令牌

强制重挂载 HdsTabs

NavPathStack 重放

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或窗口 API

CI 审计脚本

自动扫描缺@StorageProp的组件

暂不创建

  • ThemeConfiguration——ThemeEngine已经覆盖了所有必要状态管理。

  • ThemeControl.setDefaultTheme()全局调用 —— 根组件用WithTheme足够且更明确。


写在最后

品牌换肤的本质不是"给每个品牌写一套颜色",而是建立一个映射层,让页面代码永远不知道当前是什么品牌

E012 的 Theme Engine v1 实现了这个目标:

  • 二维主题:Brand × Brightness 任意组合

  • 双轨传播:WithTheme 自动(HDS 组件)+ AppStorage 手动(自定义组件)

  • 零改动新增品牌:七步流程,不碰任何页面代码

  • 品牌和结构正交:品牌只管 accent/button/gradient,结构永远是系统令牌

但这个过程里踩了两个非常深的坑:

  1. @Builder 响应式陷阱:声明了@StorageProp还不够,Builder 体内必须读this.xxx才能触发重执行。

  2. BottomTabBarStyle 不继承 WithTheme:HdsTabs 选中色硬编码系统蓝,必须通过labelStyle/iconStyle显式传。

这两个坑的共同特点是:编译通过、类型安全,但运行时不更新。它们无法通过构建发现,只能通过真机切换品牌才能暴露。

记住这几条口诀:

品牌只管 accent 色,结构永远系统令牌。 WithTheme 管 HDS,AppStorage 管自定义。 Builder 里品牌不更新,实例方法读 this 才行。 BottomTabBar 不继承 WithTheme,labelStyle 显式传。 亮度切换要 context,setColorMode 不可少。 Default 返回 undefined,保留系统原色好。 新增品牌七步走,页面代码零改动。

本文的实现基于 ArkUILab 项目 E012 实验。如果你正在做一个需要多品牌主题的鸿蒙应用,可以直接复用这套 Brand × Brightness 二维引擎和双轨传播机制作为起点。

品牌换肤看似只是"换个颜色",背后却涉及令牌分层、双轨传播、响应式陷阱、组件 API 边界和亮度系统级控制等多个维度。把这些维度拆清楚,你的品牌主题引擎就不会"加了品牌要改全站"了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询