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

【时光清单|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 实际页面为准。

相关推荐
贾伟康30 分钟前
【时光清单|15】HarmonyOS ArkTS 本地状态持久化实战:让保存、删除和页面返回后的数据即时一致
harmonyos·arkts·状态管理·preferences·arkdata
HarmonyOS_SDK8 小时前
焕新鸿蒙应用权限管理方案,应用授权体验再升级
harmonyos
黑臂麒麟2 天前
HarmonyOS鸿蒙实战应用7:随手账本——数据导出与分享
华为·app·arkts·鸿蒙
贾伟康2 天前
【天体运行模拟|11】HarmonyOS ArkTS 收藏与笔记实战:同步知识页和个人学习记录
harmonyos·arkts·preferences·多设备适配·arkdata
2501_919749032 天前
华为鸿蒙录音可视化APP—小羊声觉
华为·harmonyos·鸿蒙
大雷神2 天前
HarmonyOS AR Engine深度估计实战——把毫米距离画成实时热力图
华为·ar·harmonyos
GKxx2 天前
在 HarmonyOS 上给 QEMU 搭一个最小 aarch64 Linux guest(内核 + busybox initramfs)
linux·华为·qemu·harmonyos·鸿蒙·鸿蒙pc
黑臂麒麟2 天前
屏幕信息全解析:HarmonyOS的分辨率、刷新率、折叠状态一个API搞定
华为·arkts·鸿蒙
黑臂麒麟2 天前
HarmonyOS 网络连接诊断实战:检测网络状态、WiFi 切换、弱网监测一网打尽
网络·华为·arkts·鸿蒙