亮暗色与视觉令牌封面

主题适配常被简化成“准备一套浅色和一套深色颜色”,但真正影响工程质量的,是页面有没有统一使用语义颜色、字号、间距、圆角和交互状态。只要一部分页面直接写十六进制颜色,另一部分引用常量;一个模块把正文灰定义为 #444444,另一个模块定义为 #3D4A45;资源目录虽然有 dark,应用启动却强制锁定浅色,那么“支持深色模式”就只是文件结构上的表象。

中国方言题库的真实 ArkTS 源码提供了一个很典型的阶段性方案:公共组件和业务页面大量引用 ColorsSizes,形成墨绿、米白、白色卡片的统一视觉语言;答题选项的默认、选中、正确、错误状态都有独立语义令牌;输入框还显式定义文字、占位符和背景色,避免系统颜色变化造成不可读。与此同时,EntryAbility 明确调用 COLOR_MODE_LIGHT,当前版本选择锁定浅色;basedark 资源文件内容相同,页面主令牌又是硬编码字符串,所以不能宣称现有版本已经完成亮暗色自动切换。

本文面向 HarmonyOS 5.0 及以上版本,围绕真实源码解释视觉令牌怎样减少页面割裂、当前浅色锁定为什么能保持一致、若未来跟随系统深色模式需要改造哪些层。本文唯一核验标记:视觉令牌统一语义,颜色模式决定令牌如何解析

一、先核对真正生效的主题文件

本文复核的主要文件是:

entry/src/main/ets/common/constants/ThemeConstants.ets
librarya/src/main/ets/constants/ThemeConstants.ets
entry/src/main/ets/entryability/EntryAbility.ets
entry/src/main/resources/base/element/color.json
entry/src/main/resources/dark/element/color.json
entry/src/main/ets/common/components/GreenButton.ets
entry/src/main/ets/common/components/BankCard.ets
entry/src/main/ets/pages/PracticePage.ets
entry/src/main/ets/pages/SettingsPage.ets
entry/src/main/ets/views/HomePage.ets
entry/src/main/ets/views/FavoritePage.ets

队列 brief 指向了 entry/src/main/ets/common/constants/ThemeConstants.ets,该文件确实存在;工程同时还有 librarya 的公共主题常量。多数页面通过:

import { Colors, Sizes } from 'librarya'

使用公共能力层令牌,少量与资源图标、等级印章绑定的映射仍位于 entry。分析主题时不能只看一个同名文件,否则会遗漏实际导入来源。

二、当前版本究竟支不支持深色模式

先给出结论:当前应用正常启动后锁定浅色模式,并没有跟随系统切换到深色模式。

证据位于 EntryAbility.onCreate()

this.context
  .getApplicationContext()
  .setColorMode(
    ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT
  )

调用被放在 try/catch 中,失败时记录日志。只要设置成功,应用就会使用浅色模式,不会因为系统切到深色而自动改变。

工程确实存在:

entry/src/main/resources/base/element/color.json
entry/src/main/resources/dark/element/color.json

但两个文件当前颜色值相同,例如背景、表面和文本都还是浅色值。这说明 dark 目录更像占位或防御性资源,并不是一套已完成的深色设计。

更关键的是,页面主要使用的 Colors.PRIMARYColors.BACKGROUND 等是 ArkTS 字符串常量:

static readonly BACKGROUND: string = '#F5F1E8'
static readonly SURFACE: string = '#FFFFFF'
static readonly TEXT_PRIMARY: string = '#1F2A26'

它们不会因为资源限定目录变化而自动切换。即便移除浅色锁定,仅修改 dark/element/color.json,这些硬编码令牌仍保持浅色。当前版本的正确描述是“视觉令牌集中管理,并有明确的浅色锁定策略”,而不是“已经支持亮暗色自适应”。

视觉令牌从定义到页面消费的流程

三、为什么锁定浅色也必须认真设计

锁定浅色并不等于可以忽略系统主题。用户可能在深色系统环境中启动应用,启动窗口、状态栏、输入控件、弹窗和第三方组件都有可能受系统默认样式影响。如果页面背景是米白色,而某个输入框文字沿用深色模式下的系统默认浅色,就可能出现白底白字。

libraryaColors 为输入组件增加了:

static readonly INPUT_TEXT: string = '#1F2A26'
static readonly INPUT_PLACEHOLDER: string = '#6F7873'
static readonly INPUT_BG: string = '#FFFFFF'

注释明确说明 INPUT_TEXT 用于显式覆盖系统深色默认色。这个处理与应用锁浅色的策略一致:既然产品决定保持浅色,就必须让容易受系统默认主题影响的组件也显式使用浅色令牌。

锁定策略还应覆盖启动窗口。当前 base 和 dark 的 start_window_background 都是 #FFFFFF,避免系统深色时出现黑色启动窗口、进入应用后突然切到米白背景的闪变。它不是完整深色适配,却是保持浅色视觉连续性的必要措施。

四、Colors 的职责不是保存颜色名,而是表达语义

公共主题类没有按“绿色一、绿色二、灰色一”命名,而是分为主色、背景、文本、状态和答题选项:

class Colors {
  static readonly PRIMARY = '#2F6B5E'
  static readonly BACKGROUND = '#F5F1E8'
  static readonly SURFACE = '#FFFFFF'
  static readonly TEXT_PRIMARY = '#1F2A26'
  static readonly TEXT_SECONDARY = '#3D4A45'
  static readonly TEXT_HINT = '#5F6763'
  static readonly SUCCESS = '#267A55'
  static readonly ERROR = '#B3261E'
}

页面关心的是“这是页面背景”“这是主标题”“这是错误状态”,而不是某个具体色值。未来品牌色调整时,只需修改令牌定义,使用 Colors.PRIMARY 的按钮、标签、进度和选中态会一起变化。

语义化还能减少误用。例如“清除所有学习数据”使用 Colors.ERROR,正确答案使用 Colors.SUCCESS,普通辅助文案使用 Colors.TEXT_HINT。如果所有页面只拿一个 REDGRAY,开发者很难判断它该用于错误、危险操作、装饰还是禁用状态。

五、背景层级如何避免页面像拼起来的

中国方言题库使用三个主要背景层:

BACKGROUND
  -> 页面底色,米白 #F5F1E8

SURFACE
  -> 卡片、按钮底、底部操作区,白色 #FFFFFF

BACKGROUND_ALT
  -> 卡片内部说明区,浅米白 #FBF8F0

题库详情页根容器使用 Colors.BACKGROUND,学习摘要、简介、重点和章节卡片使用 Colors.SURFACE,简介卡片内部的文化提示使用 Colors.BACKGROUND_ALT。三层关系稳定后,页面之间不会出现一个用纯灰底、一个用米白底、一个又用未经约束的透明底。

设置页也遵循相同模式:页面根背景是 BACKGROUND,分组卡片是 SURFACE,选择器未选项是 BACKGROUND_ALT。即使组件结构不同,用户仍能识别“页面、卡片、内嵌选项”的视觉层级。

六、Sizes 如何统一信息密度

颜色只是视觉令牌的一部分。Sizes 集中定义:

static readonly H1_FONT: number = 22
static readonly H2_FONT: number = 18
static readonly TITLE_FONT: number = 16
static readonly BODY_FONT: number = 14
static readonly CAPTION_FONT: number = 12
static readonly SMALL_FONT: number = 10

static readonly CARD_RADIUS: number = 16
static readonly CARD_RADIUS_SM: number = 10
static readonly BTN_RADIUS: number = 24

static readonly PADDING_SMALL: number = 8
static readonly PADDING_MEDIUM: number = 12
static readonly PADDING_LARGE: number = 16
static readonly PADDING_XL: number = 20

因此首页标题、页面二级标题、卡片标题、正文和辅助文字有清晰层级;常规卡片圆角和小组件圆角也能保持一致。开发页面时不需要反复决定“这里到底是 15 还是 16”“这个卡片圆角是 14 还是 18”。

但源码中仍有一些局部硬编码,例如首页品牌标题使用 25,考试页标题使用 24,部分卡片使用圆角 18 或 22。这些值可能出于视觉强调,并不自动构成错误;真正的问题是它们没有语义名称,后续统一调整时难以检索意图。建议把稳定复用的特殊规格补成 PAGE_TITLE_FONTHERO_RADIUS 等令牌,单次特例则保留在组件内部。

七、交互状态为什么需要成组定义

答题选项不是只有“选中”和“没选中”。公共令牌为它定义了完整状态:

OPTION_BG
OPTION_BORDER

OPTION_SELECTED_BG
OPTION_SELECTED_BORDER

OPTION_CORRECT_BG
OPTION_CORRECT_BORDER

OPTION_WRONG_BG
OPTION_WRONG_BORDER

这组设计把填充和边框成对组织。例如正确选项使用浅绿色背景配成功绿色边框,错误选项使用浅红背景配错误红边框。页面不必临时拼接颜色,也不容易出现“背景表达正确,边框却仍是选中主色”的冲突。

交互状态还需要文字和图标配合。只改变背景色对色觉差异用户不够友好,题目页面还应通过正确/错误文案、图标或结果说明传达状态。当前令牌层解决的是颜色一致性,不等于已经替代完整的无障碍反馈。

八、GreenButton 如何复用主操作语言

GreenButton 支持实心和描边两种形态。实心按钮使用:

.fontColor(Color.White)
.linearGradient({
  angle: 180,
  colors: [
    [Colors.PRIMARY, 0],
    [Colors.PRIMARY_DARK, 1]
  ]
})

描边按钮使用:

.fontColor(Colors.PRIMARY)
.borderColor(Colors.PRIMARY)
.backgroundColor(Colors.SURFACE)

两个按钮共享 Sizes.BODY_FONT,圆角由高度的一半计算,形成胶囊形操作按钮。题库详情页的“随机练习”和“模拟考试”、结果页的“错题解析”和“再考一次”都可使用同一组件,主次关系保持一致。

当前组件没有显式的 pressed、disabled、loading 参数。PRIMARY_DARK 注释提到可用于按钮按压或渐变下端,但现有 GreenButton 只把它用于渐变。若后续增加提交、保存或耗时操作,应扩展状态接口,而不是在各页面外层临时改透明度。

九、选中态如何避免只靠一处变化

设置页底部选择器的已选项同时改变多项属性:

.fontColor(selected ? Colors.PRIMARY : Colors.TEXT_PRIMARY)
.fontWeight(selected ? FontWeight.Bold : FontWeight.Normal)
.backgroundColor(
  selected ? Colors.PRIMARY_LIGHT : Colors.BACKGROUND_ALT
)

并额外显示“已选”标签:

Text('已选')
  .fontColor(Color.White)
  .backgroundColor(Colors.PRIMARY)

这比只改变文字颜色更可靠。用户可以从字重、背景和标签三个维度识别状态。收藏页 Tab 也会在选中时使用主色背景和白色文字,未选中时使用表面色与次要文字。

不过,点击、按压和键盘焦点状态在源码中没有形成统一令牌。对于手机触控,选中态比较完整;若面向 2in1 鼠标键盘,还需要补充 hover、focus 和 pressed 的视觉反馈。

视觉令牌系统的职责结构

十、alpha() 解决的是透明度复用,不是主题切换

两个主题文件都提供:

export function alpha(color: string, alphaHex: string): string {
  if (!color || color.length < 7) return color
  return '#' + alphaHex + color.substring(1, 7)
}

ArkUI 使用 #AARRGGBB,该函数把透明度放在 RGB 前面。例如:

alpha('#2F6B5E', '15')

得到 #152F6B5E。它适合生成主色的低透明背景或阴影,使同一品牌色在不同层级复用。

函数假设传入的是至少七位的 #RRGGBB 字符串,没有验证 alphaHex 是否为两位合法十六进制,也不会正确处理资源对象或完整八位色。当前调用范围简单时足够;如果主题系统扩展,最好把输入约束写成更明确的接口,避免把动态主题资源强行当字符串处理。

十一、两个 ThemeConstants 为什么是潜在漂移点

entry 与 librarya 都定义了 ColorsSizes 和部分题型/等级映射。它们看起来接近,但并不完全相同:

entry TEXT_SECONDARY = #444444
librarya TEXT_SECONDARY = #3D4A45

librarya 额外包含 INPUT_TEXT / INPUT_PLACEHOLDER / INPUT_BG
librarya 额外包含 TOUCH_TARGET

entry 的 RankInfo 包含 stamp Resource
librarya 的 RankInfo 只有 label 和 color

这说明两份文件已经承担不同职责:librarya 是公共视觉令牌,entry 还绑定了 app 资源。但重复定义同名 ColorsSizes 会带来漂移风险。某个页面如果误从 entry 导入,正文色就可能与从 librarya 导入的组件不同。

更稳的边界是:

librarya
  -> Colors、Sizes、纯数据映射

entry
  -> 资源图标、印章 Resource、应用专属映射
  -> 直接复用 librarya 的 Colors

这样颜色和尺寸只有一个真源,entry 不再复制数值。

十二、资源颜色与 ArkTS 字符串令牌目前没有打通

color.json 中定义了:

{
  "name": "text_primary",
  "value": "#212121"
}

Colors.TEXT_PRIMARY 是:

static readonly TEXT_PRIMARY: string = '#1F2A26'

两者色值不同,页面又主要使用后者。也就是说,资源文件与 ArkTS 令牌是两套并行系统,修改资源文件不会自动影响这些页面。

这不是文章推测,而是由引用方式直接决定的:只有 $r('app.color.text_primary') 才会按资源限定目录解析;普通字符串 '#1F2A26' 不会访问资源系统。

如果产品继续锁浅色,保留字符串令牌也能工作,但应减少无效或过时的资源定义,避免维护者误以为修改 color.json 已改变全局主题。如果计划支持跟随系统,则应让语义令牌最终指向资源。

十三、迁移到真正亮暗色适配的第一步

未来若要跟随系统,第一步不是删除 COLOR_MODE_LIGHT,而是先建立资源语义一致性。

可在 base 中定义:

{
  "color": [
    { "name": "app_background", "value": "#F5F1E8" },
    { "name": "app_surface", "value": "#FFFFFF" },
    { "name": "text_primary", "value": "#1F2A26" },
    { "name": "text_secondary", "value": "#3D4A45" },
    { "name": "text_hint", "value": "#5F6763" }
  ]
}

dark 中使用同名资源:

{
  "color": [
    { "name": "app_background", "value": "#111614" },
    { "name": "app_surface", "value": "#1B211F" },
    { "name": "text_primary", "value": "#F2F5F3" },
    { "name": "text_secondary", "value": "#CDD5D1" },
    { "name": "text_hint", "value": "#AEB8B3" }
  ]
}

重点不是示例色值本身,而是两套资源名称必须一致。页面只引用语义名,系统根据当前颜色模式解析对应值。

十四、第二步:让组件消费资源,而不是静态十六进制字符串

页面可逐步从:

.fontColor(Colors.TEXT_PRIMARY)
.backgroundColor(Colors.BACKGROUND)

迁移为:

.fontColor($r('app.color.text_primary'))
.backgroundColor($r('app.color.app_background'))

也可以建立一个能返回 ResourceColor 的主题令牌层,统一封装 $r 引用。这里要注意类型设计:现有 Colors 字段是 string,而资源引用属于 Resource/ResourceColor 使用场景。迁移不能只把字符串替换成 $r 而忽略接口类型。

应优先迁移根背景、表面、主要文本、次要文本、分隔线和输入控件,因为它们决定全页可读性;品牌色和状态色可在确认两种模式对比度后再迁移。

十五、第三步:处理图片、阴影和覆盖层

颜色资源切换后,仍有几类视觉元素不会自动安全:

  1. 题库封面图片可能在深色背景中过亮。
  2. 深色卡片上的 CARD_SHADOW 可能几乎不可见。
  3. #66000000 蒙层在深色背景上层次不足。
  4. 白色文字与渐变主色需要重新检查对比度。
  5. 启动窗口、状态栏和导航栏图标颜色必须与背景一致。

这些元素应在真机或模拟器中逐页验证,不能只靠资源 JSON 静态检查。

十六、对比度不是“看着清楚”就算通过

当前令牌注释中,TEXT_HINT 标记为符合 4.5:1,正文色也做了对比度增强。工程审查应按实际前景/背景组合计算,而不是单独评价某个颜色。

需要覆盖:

TEXT_PRIMARY / BACKGROUND
TEXT_SECONDARY / SURFACE
TEXT_HINT / BACKGROUND
PRIMARY / PRIMARY_LIGHT
White / PRIMARY
ERROR / SURFACE
SUCCESS / OPTION_CORRECT_BG
INPUT_PLACEHOLDER / INPUT_BG
禁用态与半透明态

正文通常应达到 4.5:1,关键图标、按钮文字等至少应保持清晰可辨。深色模式不能简单把背景换黑、文字换白,因为状态色在深色底上的亮度关系也会变化。

十七、视觉令牌怎样减少跨页面割裂

以首页、题库详情、收藏页和设置页为例,它们的业务完全不同,但共同使用:

BACKGROUND 作为根背景
SURFACE 作为卡片背景
TEXT_PRIMARY 作为标题
TEXT_HINT 作为辅助信息
PRIMARY 作为主要动作或选中状态
DIVIDER 作为列表分隔
CARD_RADIUS 作为常规卡片圆角
PADDING_LARGE 作为页面水平间距

因此用户从首页进入详情,再进入设置,视觉层级仍是连续的。令牌系统的价值不只是“改色方便”,而是把设计决定变成代码契约:同一语义在不同页面必须有相同表现。

反过来,页面里仍存在 Color.White#E6FFFFFF#66000000 等直接值。白色用于主色渐变上的文字具有明确局部语义,半透明蒙层也可能是组件特例,并非所有硬编码都要消灭。应优先清理重复、跨页面使用且可能随主题变化的值。

十八、主题状态不应和业务状态混在一起

如果未来增加“跟随系统 / 浅色 / 深色”设置,不建议把主题切换逻辑散落在页面。设置页只负责选择,服务层负责保存偏好,应用或主题控制器负责调用颜色模式 API。

建议链路是:

设置页选择主题
  -> ThemePreferenceService 保存模式
  -> ThemeController 应用模式
  -> 资源系统重新解析同名颜色
  -> 页面自动刷新

主题偏好是轻量设置,可使用 Preferences;页面不应在每次 build 时重复调用 setColorMode。颜色模式 API 与 Ability 生命周期相关,应由应用级边界统一管理。

十九、当前版本适合执行的验证矩阵

由于当前策略是锁定浅色,测试重点不是“深色页面是否漂亮”,而是“系统深色环境下浅色应用是否仍稳定”:

系统浅色 + 应用启动
系统深色 + 应用启动
运行中切换系统颜色模式
冷启动窗口到首页的背景连续性
输入框文字与占位符
状态栏和导航栏图标
设置页底部选择器
题目正确、错误、选中状态
清除数据危险操作
弹层蒙版和取消动作

系统切换后,应用应继续保持浅色,不出现某些系统控件突然变暗、文字反色或启动页闪黑。若 setColorMode 调用失败,日志会记录错误,此时也需要观察回退表现。

二十、未来跟随系统时的验收矩阵

移除浅色锁定并完成资源迁移后,再扩展为:

浅色:所有主页面与二级页
深色:所有主页面与二级页
运行中浅转深
运行中深转浅
冷启动恢复用户偏好
跟随系统模式
输入、弹窗、选择器、列表、卡片
正确、错误、警告、选中、禁用、按压
状态栏、导航栏、启动窗口
图片、图标、阴影、蒙层

特别要检查设置页、练习页和考试结果页,因为它们包含状态色、底部操作区和多层表面,最容易出现局部遗漏。

二十一、最小改造顺序

建议按以下顺序推进,避免一次性改完后难以定位问题:

1. 确认产品策略:继续锁浅色,还是跟随系统
2. 合并 entry 与 librarya 的重复 Colors / Sizes
3. 清理 base 与 dark 资源的同名同值占位
4. 将背景、表面、文本、分隔线迁移为资源令牌
5. 迁移输入框和弹层
6. 迁移状态色与答题选项
7. 增加主题设置与持久化
8. 验证状态栏、导航栏和启动窗口
9. 执行全页面对比度与截图回归

如果产品决定继续锁浅色,那么第 2、3 步仍然值得做:统一令牌真源,明确 dark 资源只是浅色锁定下的防御性镜像,并在项目文档中记录策略。

二十二、结语

中国方言题库当前最真实的主题能力,是一套覆盖颜色、字号、间距、圆角、输入和答题状态的视觉令牌,以及明确的浅色锁定策略。它已经有效减少首页、题库、练习、收藏和设置之间的视觉割裂,但还不是完整的亮暗色自动适配:EntryAbility 锁定浅色,dark 资源与 base 相同,主要页面又使用静态字符串色值。

工程上最重要的判断是:视觉令牌统一语义,颜色模式决定令牌如何解析。先把同一语义收敛到唯一真源,再决定锁浅色还是跟随系统;如果选择深色适配,就让令牌真正连接资源系统,并验证输入、状态、系统栏和启动窗口。这样主题能力才不是一组散落色值,而是一条可维护、可切换、可复核的视觉契约。

AI 辅助声明:本文在结构整理和语言润色环节使用了 AI 辅助;关于浅色锁定、资源文件、颜色值、令牌重复和组件状态的描述均依据中国方言题库真实 ArkTS 与资源文件复核。

Logo

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

更多推荐