【时光清单|17】HarmonyOS ArkTS 亮暗色与视觉令牌实战:集中颜色、间距和交互状态避免页面割裂

**本轮证据边界:**本文基于 D:\huawei\one8 当前生产源码、亮暗资源与项目错误记录做静态复核。本轮没有执行构建、安装、模拟器、真机、截图基线、服务卡片验证或 AGC 检查;文中的当前结论不替代产物与设备验收。

阅读约定:“当前事实”来自文中点名的源码与资源;“历史证据”只复述 PROJECT_ERRORS.md 已有记录;三态模式、主题持久化、完整 ThemeTokens 和交互状态矩阵均为建议实现,不代表当前工程已经落地。

亮暗色适配最难的部分不是把白色背景换成深色,而是保证同一个语义在所有页面、所有主题和所有交互状态下都一致。首页把主色写成 #2C5F7C,详情页从资源里读取 brand_primary,主题页又从 AppTheme.primaryColor 取值,服务卡片继续固定白底。每一段代码单独看都能显示,组合起来却可能出现主题切换只改了页面背景、卡片仍保持旧颜色,深色模式下状态栏图标看不清,选中态和禁用态没有统一对比度的情况。

时光清单 的真实源码同时包含三套与视觉有关的机制。第一套是资源型语义颜色:AppColors 通过 $r('app.color.xxx') 引用 base 与 dark 目录中的同名资源。第二套是用户可选主题:Theme.ets 定义国风山水、治愈日落、极简白、深夜星空、樱花树五组配色,AppStore.applyTheme() 根据系统明暗模式把背景、卡片、主色和文字色写入 AppStorage。第三套是尺寸令牌:AppSpacingAppRadiusAppFontSizeAppFontWeight 集中管理间距、圆角和字号。

这三套机制已经构成视觉系统骨架,但也存在真实边界:部分页面读取动态主题字符串,部分组件只使用资源型 AppColorsThemeService 维护自己的静态主题 ID,却没有在其他源码中被调用;服务卡片与少数组件仍保留硬编码颜色;当前主题选择在本文复核的代码中没有展示跨启动持久化。本文从这个真实 ArkTS 工程出发,说明怎样划清颜色所有权、统一亮暗色链路、补齐交互状态,并用对比度与页面矩阵验证结果。

时光清单亮暗色与视觉令牌封面

本文将解决:

  1. 资源颜色、用户主题和运行时共享状态如何分工。
  2. base/dark 同名资源怎样自动解析。
  3. 系统明暗变化如何驱动 AppStorage 与系统栏更新。
  4. 为什么颜色、间距、圆角、字号都必须语义化。
  5. 选中、按下、禁用、错误和空状态如何纳入令牌。
  6. 怎样发现硬编码颜色、双主题真源和对比度风险。

本文唯一标记:CSDN-SERIES:ALL-163209540

一、先把“主题”拆成三层

项目里“主题”至少有三种含义:

层次真实实现适合解决的问题
系统颜色模式base/dark 资源、DARK_MODE亮色与暗色可读性
用户风格主题AppTheme 五套配色国风、日落、星空等品牌风格
组件视觉令牌AppColorsAppSpacingAppRadius跨页面一致性

如果把三层都压进一个 isDark 布尔值,就无法表达“暗色模式下的樱花主题”;如果每个页面自己组合,又会形成大量分支。更清晰的规则是:

系统模式决定亮/暗表面
用户主题决定品牌色与风格
语义令牌决定组件应该使用哪类颜色
页面只消费最终令牌

页面不应该知道某个深色背景到底是 #1A1A2E 还是 #121212,只应知道这里需要 bgPage;按钮不应该判断当前是樱花还是星空,只应请求 actionPrimarytextOnPrimarypressedOverlay

二、AppColors:把资源名变成 ArkTS 语义入口

Colors.ets 没有直接保存十六进制字符串,而是引用资源:

export class AppColors {
  static readonly brandPrimary: Resource =
    $r('app.color.brand_primary');
  static readonly bgPage: Resource =
    $r('app.color.bg_page');
  static readonly bgSurface: Resource =
    $r('app.color.bg_surface');
  static readonly textPrimary: Resource =
    $r('app.color.text_primary');
  static readonly divider: Resource =
    $r('app.color.divider');
  static readonly danger: Resource =
    $r('app.color.danger');
}

这样的好处是调用点表达语义:

Text('主题切换')
  .fontColor(AppColors.textPrimary)

Column()
  .backgroundColor(AppColors.bgSurface)
  .borderRadius(AppRadius.md)

开发者看到代码就能判断用途,不必记忆色值。设计调整时也不需要逐页替换。资源型令牌还能利用 HarmonyOS 资源限定目录,让系统模式变化后解析同名资源的另一组值。

从系统模式到页面令牌的应用流程

三、base 与 dark 必须同名,语义必须相同

项目在 base 和 dark 下定义了相同名称:

{ "name": "bg_page", "value": "#F5F0E8" }
{ "name": "bg_surface", "value": "#FFFFFF" }
{ "name": "text_primary", "value": "#1A1A1A" }
{ "name": "text_secondary", "value": "#4A4A4A" }

对应 dark:

{ "name": "bg_page", "value": "#1A1A2E" }
{ "name": "bg_surface", "value": "#2D2D44" }
{ "name": "text_primary", "value": "#E8E8F0" }
{ "name": "text_secondary", "value": "#D0D0E4" }

关键不只是“名字一致”,还要保证语义一致。text_secondary 在两套资源中都应表示次要正文,而不是亮色里表示说明文字、暗色里突然变成禁用文字。否则组件虽然自动换色,视觉层级却会变化。

新增令牌时应成对提交:

base/element/color.json
dark/element/color.json
theme/Colors.ets
组件使用点
对比度验证记录

漏掉 dark 同名资源通常不会在亮色开发阶段暴露,却可能在系统切换后产生不可读文本或退回错误默认值。

本轮还对资源中几组代表性前景色和背景色做了独立计算。亮色的 text_primary / bg_page 为 15.34:1,text_secondary / bg_surface 为 8.86:1,text_tertiary / bg_surface 为 8.19:1;暗色对应组合分别为 14.00:1、8.80:1 和 7.79:1。它们说明当前抽样组合在数值上有较充足余量,但只能证明所列色值组合,不能外推到透明度叠加、图片背景、按钮所有状态或设备最终像素。历史记录里的“约 6:1 到 11:1”与本轮计算口径也不同,不能混成同一批结果。

四、AppTheme:五套风格配色不是资源暗色的替代品

Theme.ets 定义了五个 ThemeId

export type ThemeId =
  'chinese_ink' |
  'sunset' |
  'minimal_white' |
  'starry_night' |
  'sakura';

每个主题包含亮色主色、背景、卡片和文字,以及暗色主色、背景和卡片:

export interface AppTheme {
  id: ThemeId;
  name: string;
  primaryColor: string;
  secondaryColor: string;
  backgroundColor: string;
  cardColor: string;
  textPrimary: string;
  textSecondary: string;
  darkPrimaryColor: string;
  darkBackgroundColor: string;
  darkCardColor: string;
}

这是一套用户风格数据。它允许“日落亮色”和“日落暗色”共享品牌语义。资源型 AppColors 则负责通用表面、状态色和组件默认值。二者应该有明确合并点,而不是让页面随机选择。

当前合并点主要在 AppStore.applyTheme()。这正是值得保留并继续收敛的架构位置。

五、AppStore.applyTheme 是运行时令牌解析器

真实实现先读取系统暗色状态,再从 AppTheme 中选择对应字段:

static applyTheme(themeId: ThemeId): void {
  const theme = getThemeById(themeId);
  const isDark =
    AppStorage.get<boolean>(StateKeys.DARK_MODE) ?? false;

  const bg = isDark
    ? theme.darkBackgroundColor
    : theme.backgroundColor;
  const card = isDark
    ? theme.darkCardColor
    : theme.cardColor;
  const primary = isDark
    ? theme.darkPrimaryColor
    : theme.primaryColor;

  const textPrimary =
    isDark ? '#E8E8F0' : theme.textPrimary;
  const textSecondary =
    isDark ? '#D0D0E4' : theme.textSecondary;

  AppStore.setStorageString(StateKeys.CURRENT_THEME, themeId);
  AppStore.setStorageString(StateKeys.THEME_BG, bg);
  AppStore.setStorageString(StateKeys.THEME_CARD, card);
  AppStore.setStorageString(StateKeys.THEME_PRIMARY, primary);
  AppStore.setStorageString(
    StateKeys.THEME_TEXT_PRIMARY,
    textPrimary
  );
  AppStore.setStorageString(
    StateKeys.THEME_TEXT_SECONDARY,
    textSecondary
  );
  AppStore.updateStatusBarStyle(bg);
}

它做了两件事:把“主题 ID + 明暗模式”解析成最终色值,并把这些值分发到 AppStorage。页面通过 @StorageLink 响应变化,不必逐个接收回调。

不过当前动态令牌只覆盖背景、卡片、主色和两级文字。边框、分割线、成功、警告、危险、禁用、按下蒙层仍主要来自资源型 AppColors。这就形成了混合系统,需要进一步定义优先级。

六、混合令牌的真实风险:背景变了,卡片未必跟着变

根页面和部分壳层读取:

@StorageLink(StateKeys.THEME_BG)
themeBg: string = '#F5F0E8';

@StorageLink(StateKeys.THEME_PRIMARY)
themePrimary: string = '#2C5F7C';

而很多业务卡片使用:

.backgroundColor(AppColors.bgSurface)
.fontColor(AppColors.textPrimary)

用户从国风切到樱花时,themeBgthemePrimary 会变化,但 AppColors.bgSurface 仍由 base/dark 资源决定,不一定等于樱花主题的 cardColor。页面可能因此出现“主题背景已切换,卡片仍是默认体系”的割裂。

解决方法不是删掉资源令牌,而是统一最终消费路径。可以选择:

  1. 用户主题只控制品牌色与页面背景,通用表面始终由资源决定。
  2. 用户主题完整控制背景、表面、文字和品牌色,资源只做启动默认。
  3. 建立 ThemeTokens 对象,AppStore 每次计算完整语义集合。

无论选哪一种,都要写成明确规则。当前源码更接近第二种与第一种混用,优先工作是梳理所有消费点。

资源令牌、用户主题与组件消费分层

七、建议把最终视觉值收敛成完整 ThemeTokens

可以为运行时主题建立语义对象:

export interface ThemeTokens {
  bgPage: string;
  bgSurface: string;
  bgSurfaceAlt: string;
  textPrimary: string;
  textSecondary: string;
  textTertiary: string;
  actionPrimary: string;
  textOnPrimary: string;
  borderDefault: string;
  divider: string;
  success: string;
  warning: string;
  danger: string;
  pressedOverlay: string;
  disabledFill: string;
  disabledText: string;
}

随后只有主题解析器负责生成它:

function resolveThemeTokens(
  theme: AppTheme,
  dark: boolean
): ThemeTokens {
  return {
    bgPage: dark
      ? theme.darkBackgroundColor
      : theme.backgroundColor,
    bgSurface: dark
      ? theme.darkCardColor
      : theme.cardColor,
    bgSurfaceAlt: dark ? '#3A3A52' : '#F0EDE5',
    textPrimary: dark ? '#E8E8F0' : theme.textPrimary,
    textSecondary: dark ? '#D0D0E4' : theme.textSecondary,
    textTertiary: dark ? '#C4C4D8' : '#4F4F4F',
    actionPrimary: dark
      ? theme.darkPrimaryColor
      : theme.primaryColor,
    textOnPrimary: dark ? '#101018' : '#FFFFFF',
    borderDefault: dark ? '#3A3A52' : '#E5E0D8',
    divider: dark ? '#4A4A63' : '#D8D2C8',
    success: dark ? '#5CD07A' : '#2E7D32',
    warning: dark ? '#F0C040' : '#7A5A00',
    danger: dark ? '#FF79A8' : '#B61B64',
    pressedOverlay: dark ? '#24FFFFFF' : '#14000000',
    disabledFill: dark ? '#3A3A52' : '#E8E8E8',
    disabledText: dark ? '#8E8EA0' : '#777777'
  };
}

这是演进示例,不声称当前工程已经存在。它的价值是让所有页面消费同一套最终语义,避免一半走资源、一半走 AppStorage 字符串。

八、系统明暗变化的主链路已连接,但消费端仍需验收

EntryAbility.onConfigurationUpdate() 读取新的颜色模式:

onConfigurationUpdate(newConfig: Configuration): void {
  const isDark =
    newConfig.colorMode ===
    ConfigurationConstant.ColorMode.COLOR_MODE_DARK;
  const currentDark =
    AppStorage.get<boolean>(StateKeys.DARK_MODE) ?? false;

  if (isDark !== currentDark) {
    AppStore.onDarkModeChanged(isDark);
  }
}

onDarkModeChanged() 写入共享状态并重新应用当前主题:

static onDarkModeChanged(isDark: boolean): void {
  AppStorage.set<boolean>(StateKeys.DARK_MODE, isDark);
  const themeId = (
    AppStorage.get<string>(StateKeys.CURRENT_THEME)
      ?? 'chinese_ink'
  ) as ThemeId;
  AppStore.applyTheme(themeId);
}

这条链保证系统从亮切暗时,不只是资源目录变化,运行时用户主题也会重新计算。应用启动时还调用 setColorMode(COLOR_MODE_NOT_SET),默认跟随系统。

需要验证的细节是:每个组件到底使用资源型颜色还是动态颜色,以及硬编码颜色是否绕开了两条链。

九、当前“跟随系统、浅色、深色”三态入口仍有断点

ProfileView 同时展示“跟随系统”“浅色”“深色”三个模式项,但当前 ModeChip 的点击分支只处理 darklight

if (mode === 'dark') {
  AppStore.applyColorMode(true);
} else if (mode === 'light') {
  AppStore.applyColorMode(false);
}

当参数为 auto 时没有执行任何操作,因此“跟随系统”现在只是一个可见标签,不能从强制亮色或强制暗色恢复到 COLOR_MODE_NOT_SET。三个 Chip 也使用相同背景、文字和圆角,没有根据当前模式渲染选中标记。DARK_MODE 又是布尔值,只能表示当前亮或暗,无法区分“正在跟随系统且系统为暗色”与“用户强制暗色”。这三种状态在产品语义上并不相同。

建议将用户偏好定义为 auto | light | dark,把“偏好模式”和“系统当前解析结果”分成两个状态。点击 auto 时显式调用 setColorMode(COLOR_MODE_NOT_SET);配置变化时只在偏好为 auto 的前提下跟随;界面使用勾选、边框或图标展示当前偏好。以上是建议实现,当前源码尚未具备这条完整三态链路。

十、历史证据只能解释修复背景,不能替代本轮验证

PROJECT_ERRORS.md 记录了 2026 年 5 月 20 日两类相关问题。第一类是审核指出 Text 控件对比度为 1.59,记录中的修复包括加深亮色次级、三级文字,提亮暗色对应文字,并调整底部 Tab 与设置页弱文本。第二类是“深夜星空”导致 Tab 字体看不清,记录把原因归到主题传播不完整、setOrCreate 未覆盖已有状态和 Tab 外壳没有消费主题文字色,随后改为显式覆盖并让 MainTabShell 绑定主题次级文字。

记录还写有当时 assembleHap 成功,但那是历史修复批次的结果。本轮没有重新构建,也没有在 Mate X5、其他设备、深浅色切换和五套主题组合上复测。因此本文可以引用这些记录解释为什么源码出现显式覆盖与 Tab 绑定,不能写成“当前所有主题已经通过构建和真机审核”。

十一、主题设置页展示了正确的单向更新

ThemeSettingsView 读取共享主题 ID:

@StorageLink(StateKeys.CURRENT_THEME)
currentThemeId: string = 'chinese_ink';

@State
selectedTheme: ThemeId = 'chinese_ink';

用户点击某个主题后:

.onClick(() => {
  this.selectedTheme = theme.id;
  AppStore.applyTheme(theme.id);
})

页面本地 selectedTheme 负责立即更新选中态,AppStore.applyTheme() 更新跨页面颜色。列表通过主题数组展示预览色块,选中项有背景、边框和“使用中”文本,不只依赖色差。

这里也暴露两个后续点。第一,当前选择没有在本文复核的代码中写入 Preferences,重新启动时 AppStore.bootstrap() 仍调用 applyTheme('chinese_ink');第二,预览色块总是使用主题亮色字段,系统暗色下没有展示暗色预览。若产品要求跨启动记住主题,应把 ThemeId 持久化,并在 bootstrap 读取后再应用。

十二、ThemeService、ThemeManager 与 ThemeConfig 是潜在平行真源

ThemeService 内部还有:

private static currentThemeId: ThemeId = 'chinese_ink';

static setTheme(id: ThemeId): void {
  ThemeService.currentThemeId = id;
}

static getTheme(): AppTheme {
  return getThemeById(ThemeService.currentThemeId);
}

实际源码搜索没有发现其他模块调用 ThemeService。主题设置页直接调用 AppStore.applyTheme(),因此 ThemeService.currentThemeId 不会随 UI 选择变化。

同一目录中还存在 ThemeManagerThemeConfig:它们各自保存一份当前主题对象,并把字符串写入 currentThemeId。本轮聚焦扫描同样没有发现其他生产模块调用它们。它们现在更像未接入的历史方案或候选方案,而不是已运行的主题链路。文章不能把三个类都描述为协同工作;更稳的事实是,当前可见页面、Ability 和 Tab 壳主要围绕 AppStoreStateKeys 运转。

未使用代码本身不一定造成运行错误,但它代表潜在双真源。后续开发者若调用 ThemeService.getTheme(),可能得到国风主题,而页面正在显示樱花主题。建议二选一:

  • 将 ThemeService 改为无状态解析工具,只接收 ThemeId。
  • 让 AppStore 成为唯一主题状态所有者,移除 ThemeService 的当前 ID。

主题 ID 只能有一个权威来源。

十三、间距、圆角与字号也需要令牌

视觉一致性不只靠颜色。项目已经定义:

export class AppSpacing {
  static readonly xs: number = 4;
  static readonly sm: number = 8;
  static readonly md: number = 12;
  static readonly lg: number = 16;
  static readonly xl: number = 20;
  static readonly xxl: number = 24;
  static readonly xxxl: number = 32;

  static readonly pagePadding: number = 16;
  static readonly cardPadding: number = 16;
  static readonly sectionGap: number = 28;
}

export class AppRadius {
  static readonly sm: number = 8;
  static readonly md: number = 12;
  static readonly lg: number = 16;
  static readonly round: number = 9999;

  static readonly card: number = 16;
  static readonly button: number = 12;
  static readonly chip: number = 20;
}

除了按数值分级,还提供 cardbuttonchip 这样的角色名。角色名更稳定:设计调整卡片圆角时只改 card,不会误伤所有 16vp 圆角元素。

字号也集中在 AppFontSize,从 28vp 的 h1 到 10vp 的 footnote。发布前要特别关注小字号:手机、折叠屏、平板常规正文应尽量不低于 12vp;PC 常规正文应尽量不低于 14vp。footnote 只能用于非关键辅助信息,不能承载主要操作或错误原因。

十四、交互状态必须成为一等令牌

当前颜色表已经有 success、warning、danger,但对 pressed、disabled、focused、selected 的定义还不完整。页面经常通过条件表达式直接选择颜色:

.backgroundColor(
  this.selectedTheme === theme.id
    ? AppColors.bgSurfaceAlt
    : AppColors.bgSurface
)

这能表达选中态,但项目级设计系统还应统一:

状态必须定义
normal背景、文字、边框
pressed蒙层或亮度变化
selected背景、边框、图标与文字
disabled填充、文字、透明度与不可点击
focused焦点环颜色与宽度
error错误文字、边框、提示
success成功反馈,不只靠绿色

选中态不能只依赖颜色。ThemeSettingsView 同时显示边框与“使用中”文字,这是可复用模式。错误态也应配合文本或图标,避免色觉差异用户无法判断。

十五、硬编码颜色扫描能找到真实断点

工程中仍有多处十六进制色值:

  • 三种服务卡片固定 #FFFFFF 背景和深色文字。
  • CountdownCardShareCard 内部按类型返回固定颜色。
  • 首页心情背景按时段返回固定浅色。
  • 阴影与图片遮罩使用带 Alpha 的黑色。
  • 若干组件 Prop 提供固定色默认值。

不是所有硬编码都必须删除。图片遮罩、阴影和外部分享图可能确实需要稳定输出;但每一处都应分类:

必须随模式变化 -> 资源或 ThemeTokens
必须随用户主题变化 -> 动态 ThemeTokens
品牌固定色 -> 品牌资源并验证两种背景
图片遮罩/阴影 -> 专用效果 token
导出图片固定设计 -> 与应用 UI 令牌分开

服务卡片的 colorMode 声明为 auto,但页面仍固定白底,这是一处需要实机复核的高优先级风险。声明自动模式不等于组件内部颜色会自动适配。

十六、状态栏与导航栏也属于主题

AppStore.updateStatusBarStyle() 根据背景亮度决定系统栏内容颜色:

const isLight = AppStore.isLightColor(bgColor);
const contentColor = isLight ? '#000000' : '#FFFFFF';

AppStore.mainWindow.setWindowSystemBarProperties({
  statusBarContentColor: contentColor,
  navigationBarContentColor: contentColor,
});

亮度计算使用 RGB 加权:

return (
  r * 0.299 +
  g * 0.587 +
  b * 0.114
) > 128;

这比永远使用白色或黑色图标可靠,但仍是简化判断。半透明背景、图片背景和渐变区域不一定能由单个十六进制色值代表。主题测试必须目视检查状态栏实际背景,尤其是沉浸式页面和心情图片背景。

系统栏不是页面外的附属物。它与页面背景割裂、图标不可读,同样会成为审核问题。

十七、对比度是发布门槛,不是审美偏好

每组主题都应计算并实测主要组合:

textPrimary / bgPage
textPrimary / bgSurface
textSecondary / bgSurface
textOnPrimary / actionPrimary
danger / bgSurface
disabledText / disabledFill
divider / bgSurface

正文与背景的对比度应大于 4.5:1,关键图标、标题和按钮文字应大于 3:1。禁用态可以减弱,但不能弱到用户无法辨认控件存在。

五套主题乘以亮暗两种模式就是十套组合。如果状态色也随主题自由变化,组合数量会继续扩大。因此 success、warning、danger 更适合保持稳定语义,只在明暗模式下提供经过验证的成对值。

不要只验证色值表。透明度、图片背景、阴影和组件叠加后,最终像素对比度可能和设计稿不同。

十八、深色模式不能只看首页

建议按组件族验证:

组件族重点
页面根背景无亮色闪屏、无白边
卡片表面层级可辨,边框与阴影不过重
输入框文本、占位符、光标、错误边框
Tab选中、未选中、按下与底部安全区
列表主副文本、分割线、空状态
对话框/Sheet遮罩、标题、按钮和滚动区域
图片卡片遮罩文字在明暗图片上均可读
服务卡片桌面亮暗模式与三种尺寸
系统栏状态栏与导航栏图标清晰

项目的业务页面很多:日记、习惯、情侣空间、相册、愿望清单、组件设置、隐私政策。只看首页通过,不能证明整套令牌没有遗漏。

十九、主题切换的状态一致性测试

主题与系统模式有两个独立输入,测试顺序要覆盖交叉组合:

  1. 亮色模式选择樱花。
  2. 保持页面不退出,切到暗色模式。
  3. 进入详情、设置和日记页,检查所有表面。
  4. 返回主题页,选中态仍指向樱花。
  5. 切回亮色,检查状态栏与导航栏。
  6. 结束应用并重新启动,核对是否按产品预期恢复主题。

当前代码最后一步会回到 chinese_ink,因为 bootstrap 明确调用该主题,未展示 ThemeId 持久化。如果产品希望记住用户选择,应在 DataStore 中新增专用 Key,并在首帧阶段读取:

const savedTheme = DataStore.getInstance()
  .getJsonSync<ThemeId>(
    DataKeys.THEME_ID,
    'chinese_ink'
  );
AppStore.applyTheme(savedTheme);

这属于建议实现。落地时还要对白名单校验,防止旧版本或损坏字符串被直接断言成 ThemeId。

二十、常见问题与修复

现象根因修复
切主题只改页面背景动态主题与 AppColors 混用明确最终令牌消费路径
深色下服务卡片仍白底组件硬编码颜色使用模式资源或运行时令牌
重启后恢复默认主题ThemeId 未持久化存储并在 bootstrap 校验读取
某服务读到旧主题ThemeService 是第二真源合并到 AppStore 或改无状态
选中态看不出来只用相近背景色增加边框、图标或文字
状态栏图标不清晰背景是图片或透明层按真实最终背景设置系统栏
暗色分割线太亮直接复用亮色透明度dark 资源单独校准
禁用按钮文字不可读只降低整体 opacity定义 disabledFill/Text
页面间圆角不一致使用多个裸数值改用 card/button/chip 角色令牌

二十一、上线前核对

  • base 与 dark 颜色资源名称一一对应。
  • 用户主题和系统明暗模式由单一解析器合并。
  • 页面不同时消费两套相互冲突的背景或文字真源。
  • ThemeService、ThemeManager 与 ThemeConfig 不再维护彼此独立的旧主题状态。
  • “跟随系统”能够从强制模式恢复,且三态选择有明确选中反馈。
  • ThemeId 若需跨启动恢复,已经持久化并做白名单校验。
  • 所有业务页面、Dialog、Sheet 和服务卡片都完成亮暗测试。
  • 主正文对比度大于 4.5:1,关键图标与按钮大于 3:1。
  • selected、pressed、disabled、focused、error、success 状态完整。
  • 状态栏与导航栏在图片背景、主题背景上都可读。
  • 间距、圆角、字号使用语义令牌,避免页面各写一套。
  • 10 组“5 主题 × 2 模式”完成截图或自动化基线检查。

二十二、总结:主题系统的核心是所有权

时光清单 已经具备一套可用的视觉系统骨架:base/dark 同名资源提供系统模式适配,AppColors 把资源转成 ArkTS 语义入口,五套 AppTheme 提供用户风格,AppStore.applyTheme() 把主题与明暗模式合并并驱动页面,AppSpacingAppRadiusAppFontSize 则统一空间和层级。

真正需要继续解决的是所有权:资源色与动态主题谁负责最终背景,ThemeService 是否仍应保存状态,硬编码色哪些是视觉效果、哪些是遗漏,服务卡片是否真的跟随 colorMode,主题选择是否需要跨启动恢复。只要这些边界不清晰,再多色值也只是分散配置。

高质量亮暗色适配的结果不是“页面变黑了”,而是任何主题、任何模式、任何页面和任何交互状态都能保持同样的语义层级。把颜色、间距、圆角、字号和状态统一为可验证的视觉令牌,才能避免 HarmonyOS 应用在功能扩展后逐渐变成一组彼此割裂的页面。


本文基于 D:\huawei\one8Theme.etsThemeService.etsAppStore.etsThemeSettingsView.etsStateKeys.etsColors.etsSpacing.etsRadius.etsTypography.ets 以及 base/dark 颜色资源的真实源码复核。文中完整 ThemeTokens、主题持久化和交互状态令牌均明确作为演进建议,不声称已经落地。

AI 辅助声明:本文由 AI 辅助整理与润色,所有当前事实、历史证据和建议实现均按文中列出的真实项目文件重新核对;发布前仍应以当前构建产物、设备表现与 AppGallery Connect 实际页面为准。

Logo

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

更多推荐