主题适配常被简化成“准备一套浅色和一套深色颜色”,但真正影响工程质量的,是页面有没有统一使用语义颜色、字号、间距、圆角和交互状态。只要一部分页面直接写十六进制颜色,另一部分引用常量;一个模块把正文灰定义为#444444,另一个模块定义为#3D4A45;资源目录虽然有dark,应用启动却强制锁定浅色,那么“支持深色模式”就只是文件结构上的表象。
中国方言题库的真实 ArkTS 源码提供了一个很典型的阶段性方案:公共组件和业务页面大量引用Colors、Sizes,形成墨绿、米白、白色卡片的统一视觉语言;答题选项的默认、选中、正确、错误状态都有独立语义令牌;输入框还显式定义文字、占位符和背景色,避免系统颜色变化造成不可读。与此同时,EntryAbility明确调用COLOR_MODE_LIGHT,当前版本选择锁定浅色;base与dark资源文件内容相同,页面主令牌又是硬编码字符串,所以不能宣称现有版本已经完成亮暗色自动切换。
本文面向 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.PRIMARY、Colors.BACKGROUND等是 ArkTS 字符串常量:
static readonly BACKGROUND: string = '#F5F1E8' static readonly SURFACE: string = '#FFFFFF' static readonly TEXT_PRIMARY: string = '#1F2A26'它们不会因为资源限定目录变化而自动切换。即便移除浅色锁定,仅修改dark/element/color.json,这些硬编码令牌仍保持浅色。当前版本的正确描述是“视觉令牌集中管理,并有明确的浅色锁定策略”,而不是“已经支持亮暗色自适应”。
三、为什么锁定浅色也必须认真设计
锁定浅色并不等于可以忽略系统主题。用户可能在深色系统环境中启动应用,启动窗口、状态栏、输入控件、弹窗和第三方组件都有可能受系统默认样式影响。如果页面背景是米白色,而某个输入框文字沿用深色模式下的系统默认浅色,就可能出现白底白字。
librarya的Colors为输入组件增加了:
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。如果所有页面只拿一个RED或GRAY,开发者很难判断它该用于错误、危险操作、装饰还是禁用状态。
五、背景层级如何避免页面像拼起来的
中国方言题库使用三个主要背景层:
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_FONT、HERO_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 都定义了Colors、Sizes和部分题型/等级映射。它们看起来接近,但并不完全相同:
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 资源。但重复定义同名Colors和Sizes会带来漂移风险。某个页面如果误从 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而忽略接口类型。
应优先迁移根背景、表面、主要文本、次要文本、分隔线和输入控件,因为它们决定全页可读性;品牌色和状态色可在确认两种模式对比度后再迁移。
十五、第三步:处理图片、阴影和覆盖层
颜色资源切换后,仍有几类视觉元素不会自动安全:
- 题库封面图片可能在深色背景中过亮。
- 深色卡片上的
CARD_SHADOW可能几乎不可见。 #66000000蒙层在深色背景上层次不足。- 白色文字与渐变主色需要重新检查对比度。
- 启动窗口、状态栏和导航栏图标颜色必须与背景一致。
这些元素应在真机或模拟器中逐页验证,不能只靠资源 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 与资源文件复核。