灯光模拟HarmonyOS应用实战-17-暗色驾驶面板的资源化配色与状态色映射
灯光模拟HarmonyOS应用实战-17-暗色驾驶面板的资源化配色与状态色映射

夜间驾驶面板最怕两类问题:一类是“看起来都是深色”,文字、边框与按钮却没有层级;另一类是灯光状态只靠颜色区分,近光、远光、双闪和故障提示混成同一种亮色。这样的页面即使截图好看,状态一多也会迅速失去可读性。
The_kemusan 没有在 Index.ets 里散落十六进制色值,而是先用资源名描述界面角色,再由 getLightColor() 把灯光动作映射到状态色。这个做法值得拆开看,但也要先说明一个源码事实:当前 base/element/color.json 与 dark/element/color.json 的 15 个条目同名、同值,因此它现在是一套“默认即暗色”的面板资源,并不是已经拥有两套可见差异的亮暗主题。
先把颜色问题拆成两层契约
页面资源和业务状态不是一回事。app_bg、surface_bg、text_primary 描述“这个颜色用在哪里”;success、warning、danger、fog 描述“这个状态意味着什么”。把两层混在一起,会出现背景换了,状态提示也跟着失真的情况。

可以把实际链路读成四步:资源目录提供语义名,ArkUI 通过 $r 取资源,状态函数选择语义色,Builder 决定激活与未激活的呈现。任何一步写死色值,后续改主题时都可能出现漏网区域。
base/dark color.json
↓ 同一资源名
$r(...)
↓ 状态语义映射
isLightStatusActive + getLightColor
↓
状态圆标、反馈文字、倒计时与启动窗口
这条链路是本文的审计边界。源码存在只能证明配置和引用关系,不能替代构建、模拟器或真机上的视觉验收。
当前资源现状:base 与 dark 是成对覆盖,但数值完全相同
两个文件分别位于:
entry/src/main/resources/base/element/color.json
entry/src/main/resources/dark/element/color.json
它们都定义了 app_bg、surface_bg、panel_bg、text_primary、accent、success、danger 等 15 个资源。下面是其中一组关键条目:
{
"color": [
{ "name": "app_bg", "value": "#0A0F1A" },
{ "name": "surface_bg", "value": "#121826" },
{ "name": "panel_bg", "value": "#0B0E14" },
{ "name": "text_primary", "value": "#F1F5F9" },
{ "name": "text_secondary", "value": "#94A3B8" },
{ "name": "accent", "value": "#3B82F6" },
{ "name": "success", "value": "#4ADE80" },
{ "name": "danger", "value": "#F87171" }
]
}
成对目录的价值在于“资源键保持稳定,限定词目录只覆盖差异”。当前两个文件完全一致,切换系统深浅模式时自然不会看到颜色变化。这不是框架失效,而是资源数据没有提供差异。若产品决定长期只做暗色驾驶台,这种结果可以接受;若产品承诺支持亮色主题,就必须补出真正不同的 base 或 dark 数值,并同时检查前景、背景、边框和状态色。
$r 让页面只依赖语义名
Index.ets 的根容器、标题、卡片和边框都通过资源引用取色:
build() {
Column() {
this.Header()
// 依据 currentPage 分发不同页面区域
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.app_bg'))
}
@Builder
Header() {
Row() {
Text(this.pageTitle)
.fontColor($r('app.color.text_primary'))
Text(this.getHeaderSubtitle())
.fontColor($r('app.color.text_secondary'))
}
.width('100%')
}
这里没有把 #0A0F1A 写进页面。页面只知道“应用背景”和“主文本”,至于它们在某个主题下是什么色值,由资源目录决定。这样改视觉体系时,不必在 1757 行的页面文件里搜索并替换色号,也不会误伤业务语义相近但用途不同的颜色。
语义命名还让评审更直接:看到 $r('app.color.text_secondary'),就知道它不该承担主操作按钮;看到 $r('app.color.danger'),就应该追问这里是否真的是失败或紧急状态。
状态色不是装饰,而是动作到语义的映射
灯光状态由 getLightColor() 统一分组。它不是按按钮位置取色,而是按动作含义返回资源:
private getLightColor(stateName: string): ResourceColor {
if (stateName === LIGHT_LOW || stateName === LIGHT_LEFT_SIGNAL ||
stateName === LIGHT_RIGHT_SIGNAL || stateName === LIGHT_LOW_MARKER) {
return $r('app.color.success');
}
if (stateName === LIGHT_HIGH || stateName === ACTION_ALT) {
return $r('app.color.accent');
}
if (stateName === LIGHT_PARKING) {
return $r('app.color.warning');
}
if (stateName === LIGHT_FOG) {
return $r('app.color.fog');
}
return $r('app.color.text_secondary');
}
源码中的映射可以整理为:
| 状态组 | 动作常量 | 资源色 | 页面含义 |
|---|---|---|---|
| 常规通行 | 近光、左右转向、近光加示廓 | success |
当前常规灯光已生效 |
| 强提示 | 远光、远近交替 | accent |
需要更强视觉关注 |
| 告警 | 示廓加双闪 | warning |
临停或故障警示 |
| 特殊天气 | 雾灯加双闪 | fog |
与普通告警区分 |
| 未激活/关闭 | 其他状态 | text_secondary |
保持低视觉权重 |
以后新增动作时,先决定它属于哪个语义组,再补常量和映射。若直接在新按钮上写一个新颜色,状态行、历史卡片和反馈信息就很难保持一致。

激活判定与颜色选择必须分开
isLightStatusActive() 回答“这个状态现在是否点亮”,getLightColor() 回答“点亮后使用哪种语义色”。两者分开,才能处理远近光交替这种动态状态:
private isLightStatusActive(stateName: string): boolean {
if (this.altFlashActive && stateName === ACTION_ALT) {
return true;
}
if (this.altFlashActive &&
(stateName === LIGHT_LOW || stateName === LIGHT_HIGH)) {
return this.altFlashHighVisible
? stateName === LIGHT_HIGH
: stateName === LIGHT_LOW;
}
if (stateName === LIGHT_LOW && this.lightState === LIGHT_LOW_MARKER) {
return true;
}
return this.lightState === stateName;
}
Builder 只消费判定结果和颜色结果:
@Builder
LightStatus(label: string, icon: string, stateName: string) {
Column({ space: 6 }) {
Text(icon)
.fontColor(this.isLightStatusActive(stateName)
? $r('app.color.app_bg')
: $r('app.color.text_secondary'))
.backgroundColor(this.isLightStatusActive(stateName)
? this.getLightColor(stateName)
: $r('app.color.panel_bg'))
.borderRadius(19)
Text(label)
.fontColor($r('app.color.text_secondary'))
}
}
这比传入一个预先算好的 selected: boolean 更稳:Builder 内部直接读取页面状态,可避免原始值参数在后续状态变化时无法及时反映的问题。对读者而言,stateName 也是比“蓝色/绿色”更可迁移的接口。
倒计时危险色不能污染灯光语义
倒计时最后两秒使用 danger,但它没有被塞进 getLightColor()。这是一个重要边界:灯光状态色描述当前动作,倒计时颜色描述时间风险。
Column() {
Row()
.height(8)
.width(this.getTimerWidth())
.borderRadius(4)
.backgroundColor(this.timeLeft <= 2
? $r('app.color.danger')
: $r('app.color.accent'))
}
.width('100%')
.height(8)
.backgroundColor($r('app.color.track_bg'))
如果把所有“红、黄、蓝”都集中成一个按颜色名称调用的工具函数,业务含义反而会丢失。更合适的做法是共用资源键,但保持各自的状态决策函数。这样将来把时间风险改成脉冲动画,也不会改变灯光动作映射。
启动窗口也属于主题资源链
主题一致性从应用启动画面开始。module.json5 没有写死背景色,而是把启动窗口指向同一套资源:
{
"abilities": [
{
"name": "EntryAbility",
"startWindowIcon": "$media:layered_image",
"startWindowBackground": "$color:start_window_background"
}
]
}
start_window_background 在 base 与 dark 中都存在,当前值同为 #0A0F1A。因此源码能证明“引用闭环存在”,不能据此宣称系统模式切换后启动页已经呈现两套效果。启动阶段还要在模拟器或真机上观察冷启动画面,确认图标前景、背景和首页第一帧没有突兀跳色。
真正扩展成亮暗双主题时,先改资源而不是页面
如果要让 base 承担亮色、dark 承担暗色,可以保持键名不变,只替换差异值。下面是迁移思路,不是当前工程已有内容:
// base/element/color.json(示意)
{ "name": "app_bg", "value": "#F5F7FA" },
{ "name": "surface_bg", "value": "#FFFFFF" },
{ "name": "text_primary", "value": "#18212F" }
// dark/element/color.json(保留当前暗色方向)
{ "name": "app_bg", "value": "#0A0F1A" },
{ "name": "surface_bg", "value": "#121826" },
{ "name": "text_primary", "value": "#F1F5F9" }
迁移时要遵守三个约束:同一资源键在两套目录中语义一致;base 提供完整回退,dark 只承担差异;状态色在两种背景上都要有足够辨识度。当前 EntryAbility.onCreate() 调用了 setColorMode(COLOR_MODE_NOT_SET);静态源码只能确认这个调用存在,系统如何选择限定词资源仍应结合对应系统版本文档,并在目标设备上确认。
用状态矩阵验收,而不是只截一张首页图
暗色面板至少需要覆盖以下组合:
| 场景 | 操作 | 应观察的资源/状态 | 证据层级 |
|---|---|---|---|
| 默认首页 | 冷启动 | 启动背景与 app_bg 连续 |
模拟器/真机 |
| 近光 | 点“近光灯” | 近光激活,使用 success |
页面操作 |
| 远近交替 | 点“远近交替” | ACTION_ALT 激活,近远光按节拍切换 |
页面操作/录屏 |
| 临停 | 点“示廓+双闪” | 使用 warning,不与近光混淆 |
页面操作 |
| 雾天 | 点“雾灯+双闪” | 使用 fog |
页面操作 |
| 时间风险 | 考试剩余 2 秒 | 计时条由 accent 转为 danger |
页面操作 |
| 模式切换 | 切换系统深浅模式 | 当前工程预期无明显差异,因为两套值相同 | 设备对照 |
静态审计只能确认键名、目录和调用关系;构建成功只能确认资源可编译;真正的可读性还要看模拟器和真机上的亮度、字体缩放与屏幕对比。不要把其中任意一层替代成全部结论。
出现“颜色不对”时按引用链排查
| 现象 | 优先检查 | 根因方向 |
|---|---|---|
| 切换模式没有变化 | 对比 base/dark 同名条目的值 | 当前两套资源是否本来就相同 |
| 某个圆标永远不亮 | isLightStatusActive() |
动作常量或交替状态判定遗漏 |
| 圆标亮了但颜色不合适 | getLightColor() |
状态语义分组不正确 |
| 首页文字清楚,卡片文字发灰 | 前景与背景是否成对覆盖 | 只改背景,漏改文本/边框 |
| 冷启动闪白 | startWindowBackground 与资源值 |
启动窗没有进入同一主题链 |
| 倒计时颜色影响其他状态 | 风险决策是否混进灯光映射 | 两类业务语义没有分层 |
排查顺序应从资源是否存在开始,再看限定词匹配,最后看状态函数。直接在 Builder 上临时补色,往往只会让下一次主题调整更难收口。
结语:主题能力的核心是稳定语义
这套暗色驾驶台真正可复用的不是某一组十六进制值,而是三条约束:页面只引用资源语义名,灯光动作集中映射为状态语义色,激活判定与颜色选择各自负责一件事。
当前源码已经具备 base/dark 成对目录、$r 引用和状态映射,但两套颜色值完全相同。把这个事实说清楚,比笼统宣称“已完成深浅主题适配”更有工程价值;未来要扩展亮色方案时,也只需在资源层建立真实差异,再用完整状态矩阵验证即可。
更多推荐



所有评论(0)