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

一个 HarmonyOS 应用页面增加到十几个之后,视觉问题往往不是“某个颜色不好看”,而是同一种语义在不同页面被写成不同值:标题有时 18vp、有时 20vp;卡片圆角一会儿 12vp、一会儿 18vp;成功状态和主按钮都用绿色,却没有区分品牌色与语义色;暗色模式下某些页面自动变暗,另一些页面仍固定白底。这样的应用单页看似正常,连续使用时却会明显割裂。
本文基于句匠项目 D:\huawei\one18-11 的真实 ArkTS 源码,重点核对 entry/src/main/ets/common/constants/ThemeConstants.ets、EntryAbility.ets、resources/base/element/color.json、resources/dark/element/color.json,并抽查 TopBar.ets、GreenButton.ets、BankCard.ets、SectionHeader.ets、PracticePage.ets、SettingsPage.ets 等页面和组件。
本文唯一源码标识:com.jiaweikang.one18。
先说结论:句匠已经把大量颜色、字号、间距、圆角和状态色集中到 Colors 与 Sizes,共享组件也在复用这些令牌;但应用当前明确锁定浅色模式,dark 资源与 base 资源值相同,ArkTS 颜色常量又是固定十六进制字符串。因此,源码可以证明的是“浅色视觉体系已经部分令牌化”,不能证明“亮暗色自动适配已经完成”。
一、视觉令牌解决的不是美术问题,而是语义一致性
视觉令牌是把“为什么使用这个值”变成稳定名称。比如:
Colors.TEXT_PRIMARY
Colors.TEXT_HINT
Colors.SUCCESS
Sizes.BODY_FONT
Sizes.CARD_RADIUS
Sizes.PADDING_LARGE
页面不需要记住标题到底是 #1F2A26,只需要表达“这是主要文本”。以后调整配色时,修改令牌即可影响所有正确引用它的页面。
视觉令牌通常分成三层:
| 层级 | 示例 | 作用 |
|---|---|---|
| 原始值 | #2F6B5E、16vp | 最底层数值 |
| 语义令牌 | PRIMARY、TEXT_HINT | 描述用途 |
| 组件令牌 | 按钮背景、卡片圆角 | 描述具体组件状态 |
句匠已有前两层的一部分,也有答题选项这一类接近组件级的状态令牌。
二、句匠的品牌色如何被集中
ThemeConstants.ets 将英伦学院风定义为墨绿、米白与暗金:
export class Colors {
static readonly PRIMARY: string = '#2F6B5E'
static readonly PRIMARY_DARK: string = '#1F5246'
static readonly PRIMARY_LIGHT: string = '#E6EFEB'
static readonly ACCENT: string = '#8A6814'
static readonly INK: string = '#3A4A45'
}
这些值承担不同职责:
PRIMARY用于主按钮、进度、选中状态;PRIMARY_DARK用于按压感或渐变下端;PRIMARY_LIGHT用于标签底色和选中区域浅底;ACCENT用于金色点缀;INK作为墨色扩展。
把“深一点的绿色”命名为 PRIMARY_DARK,比页面里散落 #1F5246 更容易维护。设计调整时也能知道它与主色的关系。
三、背景与表面必须分开
项目定义了三种背景层级:
static readonly BACKGROUND = '#F5F1E8'
static readonly BACKGROUND_ALT = '#FBF8F0'
static readonly SURFACE = '#FFFFFF'
BACKGROUND 是页面底,SURFACE 是卡片与工具栏,BACKGROUND_ALT 是卡片内部的次级区域。即使都接近浅色,语义仍不同。
如果所有区域都直接写 Color.White,页面层级只能依赖阴影和边框;如果每个页面随意挑一种米白,又会出现轻微但持续的色差。令牌让页面背景、卡片表面与嵌套区域保持稳定关系。

四、文本颜色要按信息层级定义
真实源码中有:
static readonly TEXT_PRIMARY = '#1F2A26'
static readonly TEXT_SECONDARY = '#444444'
static readonly TEXT_HINT = '#5F6763'
它们分别服务标题、正文、辅助信息。TEXT_HINT 的注释明确写了“符合 4.5:1”,说明项目在调整辅助文字时考虑了正文可读性,而不是只追求浅灰效果。
页面使用时应按语义选择:
Text('题库')
.fontColor(Colors.TEXT_PRIMARY)
Text('按地区与题型挑选练习内容')
.fontColor(Colors.TEXT_HINT)
同一页面内,如果标题、正文、占位符、禁用文本都用同一种灰色,信息层级会消失;如果辅助文字过浅,又会成为 AppGallery 色彩对比风险。
五、状态色不能由品牌色代替
句匠单独定义:
static readonly SUCCESS = '#267A55'
static readonly ERROR = '#B3261E'
static readonly WARNING = '#8A5A00'
虽然品牌主色也是绿色,但成功色仍有独立语义。这样未来品牌色改成蓝色时,“答题正确”不需要跟着变成蓝色。
错误、警告、成功还应同时使用文字、图标或形状表达,不能只依赖色相。对于色觉差异用户,仅靠红绿判断选项正误并不可靠。当前项目已有正确/错误背景与边框令牌,后续可以结合图标或明确文案继续增强。
六、答题选项已经形成组件状态矩阵
Colors 对答题选项定义了完整状态:
OPTION_BG
OPTION_BORDER
OPTION_SELECTED_BG
OPTION_SELECTED_BORDER
OPTION_CORRECT_BG
OPTION_CORRECT_BORDER
OPTION_WRONG_BG
OPTION_WRONG_BORDER
这比页面里写一串三元表达式更稳。一个答题选项至少包含默认、选中、正确、错误四种状态,每种状态又有背景与边框两个维度。
状态矩阵可以写成:
| 状态 | 背景 | 边框 | 文本/图标 |
|---|---|---|---|
| 默认 | OPTION_BG | OPTION_BORDER | 主要文本 |
| 选中 | OPTION_SELECTED_BG | OPTION_SELECTED_BORDER | 主色强调 |
| 正确 | OPTION_CORRECT_BG | OPTION_CORRECT_BORDER | 成功语义 |
| 错误 | OPTION_WRONG_BG | OPTION_WRONG_BORDER | 错误语义 |
这类组件级令牌最能防止 PracticePage 多种题型之间出现交互状态不一致。
七、字号、间距和圆角同样是令牌
Sizes 并不只有颜色:
static readonly H1_FONT = 22
static readonly H2_FONT = 18
static readonly TITLE_FONT = 16
static readonly BODY_FONT = 14
static readonly CAPTION_FONT = 12
static readonly SMALL_FONT = 10
同时还集中:
CARD_RADIUS
CARD_RADIUS_SM
BTN_RADIUS
PADDING_SMALL
PADDING_MEDIUM
PADDING_LARGE
PADDING_XL
TAB_BAR_HEIGHT
BOTTOM_NAV_MIN_PADDING
这让视觉一致性从颜色扩展到排版、空间与形状。TopBar、SectionHeader、BankCard 和 GreenButton 都在引用这些值。
需要注意,SMALL_FONT = 10 对手机辅助信息尚可,但不能无差别用于正文;在 PC/2in1 场景,10vp 也可能偏小。令牌化不代表数值永远正确,它只是让后续统一调整变得可能。
八、共享组件是令牌真正落地的位置
仅定义 Colors 和 Sizes 不会自动获得一致性,页面必须通过共享组件消费它们。
TopBar.ets 使用:
.colorBlend(Colors.PRIMARY)
.backgroundColor(Colors.SURFACE)
.border({ width: 1, color: Colors.DIVIDER })
.fontSize(Sizes.H2_FONT)
.fontColor(Colors.TEXT_PRIMARY)
GreenButton.ets 使用主色与深主色构建渐变:
.linearGradient({
angle: 135,
colors: [
[Colors.PRIMARY, 0],
[Colors.PRIMARY_DARK, 1]
]
})
BankCard.ets 则统一卡片背景、标题、标签、辅助文字和圆角。共享组件覆盖得越多,页面越不容易自行发明另一套样式。
九、alpha 工具如何减少透明色散落
项目提供:
export function alpha(
color: string,
alphaHex: string
): string {
if (!color || color.length < 7) return color
return '#' + alphaHex + color.substring(1, 7)
}
ArkUI 十六进制颜色采用 #AARRGGBB,这个工具把基础颜色和透明度组合。比如:
alpha(Colors.SUCCESS, '15')
比直接写 #15267A55 更能看出语义关系。
真实边界也要写清楚:该函数只取 color.substring(1, 7),适用于项目当前的 #RRGGBB 字符串;如果传入资源颜色、短格式、已有 Alpha 或非十六进制表示,就不一定符合预期。它不是通用颜色解析器。
十、当前应用明确锁定浅色模式
EntryAbility.onCreate() 中存在:
this.context
.getApplicationContext()
.setColorMode(
ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT
)
这意味着应用启动时主动选择浅色,而不是跟随系统暗色模式。调用放在 try/catch 中,失败会记录日志,但正常情况下用户切换系统暗色不会让应用进入真正暗色主题。
因此,标题里的“亮暗色”应理解为对现状与迁移方法的实战分析,不能解释为项目当前已提供亮暗色开关。
十一、dark 资源存在,但内容与 base 完全相同
项目同时存在:
resources/base/element/color.json
resources/dark/element/color.json
但两个文件中的 start_window_background、primary、background、surface、text_primary 等值完全相同,例如背景都为 #F5F5F5,表面都为 #FFFFFF。
这说明资源目录结构已经准备好,但暗色值并没有设计完成。即使移除强制浅色,如果页面引用这些资源,暗色限定目录也不会产生视觉变化。
更关键的是,主要页面实际大量引用 ThemeConstants.ets 中的固定字符串,而不是 $r('app.color.background')。资源限定机制无法自动替换这些 ArkTS 常量。
十二、当前存在两套并不一致的颜色来源
ThemeConstants.ets 的主色是 #2F6B5E,资源 color.json 的 primary 却是 #4CAF50;前者背景是米白 #F5F1E8,后者是灰白 #F5F5F5。
这构成双重真源:
ArkTS Colors.PRIMARY → #2F6B5E
resource app.color.primary → #4CAF50
如果启动页、原生配置或少数页面使用资源,而大部分页面使用 ArkTS 常量,就可能在启动过渡、对话框或系统组件上看到配色跳变。
真正稳定的主题体系需要指定唯一真源。对于需要系统亮暗色限定的颜色,优先使用同名资源;ArkTS 中可以保留尺寸、业务映射和辅助函数,但不应再复制一套固定颜色值。
十三、硬编码颜色仍然大量存在
源码抽查显示,多个页面除了 Colors.* 之外,还直接使用:
Color.White
'#E6FFFFFF'
'#33000000'
'#29000000'
'#00000000'
'#B3000000'
部分硬编码是合理的,例如深色渐变上的白字与半透明遮罩;但它们仍需进入主题审计。暗色模式下,固定白字可能落在浅色背景上,固定黑色遮罩可能使内容过暗。
迁移时可以将常见叠加层集中:
HERO_TEXT
HERO_TEXT_SECONDARY
SCRIM_LIGHT
SCRIM_STRONG
PRESSED_OVERLAY
这段命名是建议,不是当前源码已有字段。
十四、真正双主题应怎样迁移
建议按风险从低到高分四步:
- 先确定是否跟随系统、提供应用内切换,还是继续锁定浅色;
- 统一颜色真源,把页面级固定颜色迁移到语义资源;
- 为 dark 限定目录填写真实暗色值;
- 移除强制浅色并做全页面回归。
资源可以保持同名:
{
"color": [
{ "name": "background", "value": "#F5F1E8" },
{ "name": "surface", "value": "#FFFFFF" },
{ "name": "text_primary", "value": "#1F2A26" }
]
}
dark 目录使用同样名称,但填写经过对比度验证的深色值。页面引用:
.backgroundColor($r('app.color.background'))
.fontColor($r('app.color.text_primary'))
这样系统资源解析才会根据限定目录切换。
十五、不能机械反转浅色调色板
暗色主题不是把白变黑、黑变白。句匠的墨绿和暗金在深色背景上可能失去对比度,浅绿色标签底也不能直接沿用。
暗色设计至少要重新确定:
| 语义 | 浅色关注点 | 暗色关注点 |
|---|---|---|
| 页面背景 | 米白氛围 | 避免纯黑造成强烈反差 |
| 卡片表面 | 与背景分层 | 比背景略亮并保持边界 |
| 主文本 | 深色高对比 | 近白但避免刺眼 |
| 辅助文本 | 满足 4.5:1 | 不使用过暗灰 |
| 主色 | 品牌识别 | 提升亮度或降低饱和 |
| 分割线 | 可见但克制 | 避免完全消失 |
成功、错误、警告状态还要分别检查文字、背景和边框组合,不能只验证单个色值。
十六、组件状态要覆盖按压、禁用与聚焦
当前 GreenButton 能表达主按钮与部分视觉变化,答题选项也有选中、正确、错误状态。但完整的 PC/2in1 和多输入体验还需要:
- 普通;
- 按压;
- 禁用;
- 键盘聚焦;
- 鼠标悬停;
- 加载中;
- 操作成功或失败。
视觉令牌可以继续扩展:
BUTTON_PRIMARY_BG
BUTTON_PRIMARY_PRESSED_BG
BUTTON_DISABLED_BG
BUTTON_DISABLED_TEXT
FOCUS_RING
HOVER_OVERLAY
是否需要全部字段取决于组件,不应一次性制造庞大体系。但关键工作流至少要有可识别的禁用与反馈状态。
十七、主题切换必须连同系统栏和启动页验证
主题不仅存在于 ArkUI 页面。句匠还涉及:
start_window_background;- 状态栏图标明暗;
- 底部导航指示区;
- 对话框与弹窗;
- 图片、图标和渐变;
- 空状态与错误状态;
- TextArea 占位符;
- Canvas 或自绘内容。
如果启动页仍是白色,而首屏变为深色,会出现明显闪白。若状态栏背景变深但图标仍使用深色模式,图标会不可见。移除 COLOR_MODE_LIGHT 前必须把这些系统表面一起纳入验收。

十八、用可执行矩阵检查对比度
发布前不应只凭肉眼说“看起来清楚”。建议至少检查:
正文文字 / 页面背景 > 4.5:1
标题与关键图标 / 背景 > 3:1
按钮文字 / 按钮背景 > 3:1
辅助文字 / 卡片表面 > 4.5:1
禁用态仍能识别,但不与可用态混淆
矩阵需要覆盖浅色与暗色、普通与按压、选中与未选中、正确与错误、占位符、分割线和弹窗。句匠当前注释提到了 TEXT_HINT 对比度,但源码中没有完整自动化对比度测试记录,因此不能把整个应用描述为已经全部测量通过。
十九、如何减少页面割裂
从真实源码出发,最有效的整理顺序是:
- 保留
Colors、Sizes已形成的语义命名; - 让
TopBar、GreenButton、BankCard、SectionHeader等共享组件成为页面默认入口; - 统计页面中的硬编码颜色与尺寸,按出现频率迁移;
- 合并 ArkTS 常量与资源颜色双重真源;
- 再设计 dark 限定资源和系统栏策略;
- 最后移除强制浅色并做真机/模拟器回归。
这样可以先获得一致性收益,再承担主题切换风险。直接删除 setColorMode(),只会把未完成的 dark 资源和固定十六进制颜色同时暴露出来。
二十、结语:先把现状说清楚,再谈暗色适配
句匠真实源码已经完成了一项有价值的基础工作:墨绿、米白、暗金、文本层级、状态色、答题选项状态、字号、间距、圆角和安全区尺寸,大量集中在 ThemeConstants.ets,共享组件也在持续消费这些令牌。这能显著降低页面各写一套样式的风险。
但“存在 dark 目录”不等于支持暗色,“定义 Colors 类”也不等于具备动态主题。当前应用锁定浅色,dark 与 base 资源相同,主要 ArkTS 颜色是固定字符串,资源文件与主题常量还存在不同配色。这些都是必须如实记录的工程边界。
一个可靠的 HarmonyOS 5.0 及以上主题体系,应以语义令牌为中心,以同名亮暗资源为唯一颜色真源,让共享组件统一消费,并对系统栏、启动页、图片和完整交互状态做对比度回归。做到这一步,亮暗色适配才不是“换一张色表”,而是整个应用视觉语义的一致切换。
本文部分内容由 AI 辅助整理,所有能力判断均基于句匠真实源码;未把浅色锁定状态描述成已完成的系统暗色适配,也未虚构审核或发布结果。
更多推荐



所有评论(0)