文章导读

  • 颜色、字号、间距、圆角先抽成主题 token,页面层只引用语义名称。
  • 会随系统外观变化的颜色,交给 Resourcebase / dark 资源目录处理。
  • 标签色、徽章色这类业务色保留为字符串,避免把数据模型和资源系统绑死。

页面效果

主题方案最怕只在一个页面里看起来成立,切到别的入口就失真。羽球联盟里,首页搜索框、Banner、功能宫格、列表卡片和底部导航都在复用同一组主色、文字层级、卡片背景和圆角;这意味着主题 token 已经不只是样式表,而是页面结构的一部分。

theme-home

实战拆解

如果页面里直接散落 #0F7B5F168 这类裸值,短期开发会很快,后面改主色、补暗色或统一卡片节奏时就会很痛。羽球联盟把这层公共样式拆到 AppColorsAppSizes,页面只表达“这是主色”“这是卡片背景”“这里用 16 的间距”,而不再关心底层具体值。

这里最值得单独讲清楚的是颜色分层。AppColors.primarytextPrimarybgPage 这类会随着系统外观变化的颜色,全部走 Resource;而赛事状态徽章、标签强调色这类更偏业务语义、常常需要从模型字段直接拼接到组件里的颜色,则继续保留为字符串。两者都叫“颜色”,但生命周期完全不同,混在一起后续维护会非常吃力。

另一个关键点是深色模式不是在页面末端补一层 if dark then ...。这个项目在 EntryAbility 启动时从 PrefStore 读取用户偏好,再调用 setColorMode(),让整个应用从资源解析层开始进入对应色表。这样首页、详情页、个人中心和设置页用到的 Resource 色会一起切换,而不是每页各补一套判断。

关键代码

export class AppColors {
  static readonly primary: Resource = $r('app.color.primary');
  static readonly primaryLight: Resource = $r('app.color.primary_light');
  static readonly primaryDark: Resource = $r('app.color.primary_dark');

  static readonly textPrimary: Resource = $r('app.color.text_primary');
  static readonly textSecondary: Resource = $r('app.color.text_secondary');
  static readonly textTertiary: Resource = $r('app.color.text_tertiary');

  static readonly bgPage: Resource = $r('app.color.bg_page');
  static readonly bgCard: Resource = $r('app.color.bg_card');
  static readonly bgGray: Resource = $r('app.color.bg_gray');
  static readonly divider: Resource = $r('app.color.divider');

  static readonly badgeOngoing: string = '#0F7B5F';
  static readonly badgeFinished: string = '#999999';
  static readonly badgePending: string = '#FF9F2D';
}

export class AppSizes {
  static readonly s8: number = 8;
  static readonly s12: number = 12;
  static readonly s16: number = 16;
  static readonly s24: number = 24;
  static readonly r8: number = 8;
  static readonly r12: number = 12;
  static readonly r16: number = 16;
  static readonly f14: number = 14;
  static readonly f16: number = 16;
  static readonly f20: number = 20;
}
PrefStore.getInstance().init(this.context).then(() => {
  const mode: string = AppStorage.get<string>('setting_darkMode') ?? 'system';
  let cm: ConfigurationConstant.ColorMode = ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET;
  if (mode === 'dark') {
    cm = ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
  } else if (mode === 'light') {
    cm = ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
  }
  this.context.getApplicationContext().setColorMode(cm);
});

这两段放在一起看,主题体系才完整:前者定义了“页面该用哪些语义 token”,后者保证这些 token 会在应用启动阶段解析到正确的浅色或深色资源值。

取舍分析

把 token 抽得太细,容易让页面代码读起来很绕;抽得太粗,又会回到满地散值。这个项目当前停在一个比较实用的层级:颜色、间距、圆角、字号统一收进主题类,页面只用语义名;而业务上波动更大的标签色、徽章色保留为字符串。这样既能吃到资源系统带来的深浅色适配,又不至于让所有颜色都必须先去资源表里登记一遍。

深色模式也没有做成一套完全独立的页面主题,而是沿用同样的 token 名称,只替换 basedark 目录下的资源值。这样新增页面时,不需要先考虑两套样式分支,只要遵守已有 token 体系,大部分适配会自动发生。

设计落点

  • 颜色 token 优先表达语义,不直接暴露裸值。
  • 会跟系统外观变化的颜色走 Resource,业务标签色保留为字符串。
  • 间距、圆角、字号和颜色一起收敛,避免“颜色统一了,布局节奏还是散的”。
  • 深色模式在应用入口设置,页面不重复写判断逻辑。

易踩坑

  • 不要把 Resource 色和字符串色混着往同一个组件接口里塞,后面很难分清谁应该跟随深色模式变化。
  • 不要只抽主色,不抽背景、分割线、文字层级。主题体系一旦只有一个绿色常量,实际维护价值会很弱。
  • 深色模式如果靠页面内部分支临时覆盖,页面一多就会失控,最好从资源层统一处理。

验证方式

  • 切换“跟随系统 / 浅色 / 深色”,确认首页和详情页的背景、文字和按钮都能一起变化。
  • 调整 primary 后检查 Banner 指示器、底部导航高亮、按钮和标签是否同步变化。
  • 打开设置页和个人中心,确认卡片背景、分割线和正文层级没有出现暗色下发灰或发脏的问题。

参考资料

  • [深浅色模式适配(官方文档)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ui-dark-light-color-adaptation-V5)
  • [资源分类与访问(官方文档)](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/resource-categories-and-access)
  • [UIAbilityContext.setColorMode(官方文档)](https://developer.huawei.com/consumer/en/doc/harmonyos-references/js-apis-inner-application-uiabilitycontext)

小结

这篇的核心不是“把颜色常量搬个家”,而是先把语义层建立起来,再让资源系统和入口生命周期共同接管深浅色切换。这样后续继续扩页面时,样式一致性和暗色适配都能跟着走,不需要每次重做一遍主题整理。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐