鸿蒙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 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 自动 |
|
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_PALETTE Map:
[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 边界和亮度系统级控制等多个维度。把这些维度拆清楚,你的品牌主题引擎就不会"加了品牌要改全站"了。
更多推荐




所有评论(0)