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

封面

夜间驾驶面板最怕两类问题:一类是“看起来都是深色”,文字、边框与按钮却没有层级;另一类是灯光状态只靠颜色区分,近光、远光、双闪和故障提示混成同一种亮色。这样的页面即使截图好看,状态一多也会迅速失去可读性。

The_kemusan 没有在 Index.ets 里散落十六进制色值,而是先用资源名描述界面角色,再由 getLightColor() 把灯光动作映射到状态色。这个做法值得拆开看,但也要先说明一个源码事实:当前 base/element/color.jsondark/element/color.json 的 15 个条目同名、同值,因此它现在是一套“默认即暗色”的面板资源,并不是已经拥有两套可见差异的亮暗主题。

先把颜色问题拆成两层契约

页面资源和业务状态不是一回事。app_bgsurface_bgtext_primary 描述“这个颜色用在哪里”;successwarningdangerfog 描述“这个状态意味着什么”。把两层混在一起,会出现背景换了,状态提示也跟着失真的情况。

流程图

可以把实际链路读成四步:资源目录提供语义名,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_bgsurface_bgpanel_bgtext_primaryaccentsuccessdanger 等 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" }
  ]
}

成对目录的价值在于“资源键保持稳定,限定词目录只覆盖差异”。当前两个文件完全一致,切换系统深浅模式时自然不会看到颜色变化。这不是框架失效,而是资源数据没有提供差异。若产品决定长期只做暗色驾驶台,这种结果可以接受;若产品承诺支持亮色主题,就必须补出真正不同的 basedark 数值,并同时检查前景、背景、边框和状态色。

$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_backgroundbasedark 中都存在,当前值同为 #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 引用和状态映射,但两套颜色值完全相同。把这个事实说清楚,比笼统宣称“已完成深浅主题适配”更有工程价值;未来要扩展亮色方案时,也只需在资源层建立真实差异,再用完整状态矩阵验证即可。

Logo

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

更多推荐