一、前言:品牌换肤的"组合爆炸"
先看一个真实的业务场景。
你的应用要上线企业版和个人版,企业版用品牌蓝,个人版用活力粉。之后又要加一个咖啡品牌合作主题、一个环保绿色主题。每个主题还要同时支持浅色和深色模式。
传统做法:
// 每个页面都写品牌判断 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 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 作用域拿到新的 CustomColors3.3 双轨对照表
组件类型 | 品牌响应方式 |
|---|---|
HDS 内部组件(Button 等) | WithTheme 自动 |
HdsTabs | 不自动!需要显式传 |
ArkUILab 自定义组件 | AppStorage + |
四、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 品牌调色板注册
所有品牌注册到一个 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 根组件初始化
在根组件(ApplicationShell或Index)的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 什么时候用什么令牌
场景 | 正确令牌 |
|---|---|
页面背景 |
|
卡片表面 |
|
主要文字 |
|
分割线 |
|
品牌色图标 / 激活指示 / 选中态 |
|
填充主按钮 |
|
Hero 渐变横幅 |
|
系统蓝按钮(非品牌) |
|
七、⚠️ 踩坑一:@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 被拒绝的方案:强制重挂载 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 作用域内的 HDS 内部组件 |
| 全 App的 |
如果你只设了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.Default在buildCustomTheme()里返回undefined,保留系统原色。✅ 亮度切换时调
ThemeEngine.setBrightness(brightness, context),必须传 context。
13.4 BottomTabBarStyle
✅
BottomTabBarStyle.of()不读WithTheme,必须显式传labelStyle.selectedColor和iconStyle.selectedColor。✅ 不要用强制重挂载 HdsTabs 来更新底栏色——太破坏性。
十四、常见错误对照表
错误做法 | 问题 | 正确做法 |
|---|---|---|
| 加品牌改所有页面 | 用 |
声明了 | @Builder 不跟踪模块级函数 | 实例方法先读 |
HdsTabs 选中色不变 | BottomTabBarStyle 不读 WithTheme |
|
|
| 传 |
Default 品牌返回 CustomTheme | 覆盖了系统原色 | Default 返回 |
用裸字符串 | 容易拼写错误 | 用 |
品牌令牌用在结构 UI(页面背景) | 品牌和结构耦合 | 结构 UI 用结构令牌 |
强制重挂载 HdsTabs | NavPathStack 重放 | 用 |
品牌色只有 Light 值 | 深色模式下品牌色刺眼 | 每个品牌色都提供 Light + Dark |
直接用 | 不可换品牌 | 用 |
十五、审计命令
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 26 | 等本地 SDK 暴露 |
自适应状态栏/导航栏内容色 |
|
CI 审计脚本 | 自动扫描缺 |
暂不创建:
ThemeConfiguration——ThemeEngine已经覆盖了所有必要状态管理。ThemeControl.setDefaultTheme()全局调用 —— 根组件用WithTheme足够且更明确。
写在最后
品牌换肤的本质不是"给每个品牌写一套颜色",而是建立一个映射层,让页面代码永远不知道当前是什么品牌。
E012 的 Theme Engine v1 实现了这个目标:
二维主题:Brand × Brightness 任意组合
双轨传播:WithTheme 自动(HDS 组件)+ AppStorage 手动(自定义组件)
零改动新增品牌:七步流程,不碰任何页面代码
品牌和结构正交:品牌只管 accent/button/gradient,结构永远是系统令牌
但这个过程里踩了两个非常深的坑:
@Builder 响应式陷阱:声明了
@StorageProp还不够,Builder 体内必须读this.xxx才能触发重执行。BottomTabBarStyle 不继承 WithTheme:HdsTabs 选中色硬编码系统蓝,必须通过
labelStyle/iconStyle显式传。
这两个坑的共同特点是:编译通过、类型安全,但运行时不更新。它们无法通过构建发现,只能通过真机切换品牌才能暴露。
记住这几条口诀:
品牌只管 accent 色,结构永远系统令牌。 WithTheme 管 HDS,AppStorage 管自定义。 Builder 里品牌不更新,实例方法读 this 才行。 BottomTabBar 不继承 WithTheme,labelStyle 显式传。 亮度切换要 context,setColorMode 不可少。 Default 返回 undefined,保留系统原色好。 新增品牌七步走,页面代码零改动。
本文的实现基于 ArkUILab 项目 E012 实验。如果你正在做一个需要多品牌主题的鸿蒙应用,可以直接复用这套 Brand × Brightness 二维引擎和双轨传播机制作为起点。
品牌换肤看似只是"换个颜色",背后却涉及令牌分层、双轨传播、响应式陷阱、组件 API 边界和亮度系统级控制等多个维度。把这些维度拆清楚,你的品牌主题引擎就不会"加了品牌要改全站"了。