HarmonyOS 7 + ArkUI-Hvigor:动态字体与色彩对比上架预检【鸿蒙心迹】

一、审核问题往往不是代码报错

AccessAudit 是我们给购物结算页做的一套上架前检查工具。页面在开发机上看起来没有明显问题:主按钮醒目,优惠提示使用品牌橙色,小字说明也能读清。真正把系统字体调到 1.8 倍、切换深色模式,再开启屏幕朗读后,三个问题同时暴露出来:金额说明被截断、优惠标签与背景对比不足、右上角图标没有可读名称。

这类问题不会让 ArkTS 编译失败,也不会在普通单元测试里抛异常。它们通常等到上架检测、审核反馈或真实用户投诉时才出现。项目排期一紧,团队最容易做的事是临时改几个颜色、缩短一段文案,然后重新提审;页面多了以后,同一种问题还会在另一个模块出现。

这次我没有继续靠人工巡检,而是把页面可访问性检查拆成可重复的构建任务。扫描批次固定为 AUDIT-20260930-2057-12,目标页面是 CheckoutConfirmPage,共检查 46 个组件。第一次报告发现 5 项:低对比文本 2 项、字体缩放风险 2 项、缺失无障碍文本 1 项;修复后阻断项为 0,状态由 SCANNING → REVIEW_REQUIRED → FIXED → PASS。

需要说明的是,这套工具是项目内预检器,不等同于官方审核系统。它的价值是把容易反复出现的问题提前到开发和 CI 阶段,最终要求仍应以当前 HarmonyOS 设计规范、API 文档和上架检测结果为准。

二、把视觉规范变成可计算的规则

颜色检查最容易陷入"看起来还行"。同一个浅灰文字在高亮屏幕上能看清,换到低亮度、户外反光或视觉能力不同的用户,就可能失去可读性。我们为普通正文设置项目阈值 4.5:1,为大字号文本设置 3:1;这是团队采用的较严格策略,不冒充平台唯一阈值。官方设计资料要求文本与背景保持足够对比,项目可以在此基础上建立更严格的门槛。

第一处失败发生在优惠标签:前景色 #E86F51,背景色 #FFF1EC,计算结果为 2.71:1。设计稿里两种颜色都很温和,叠在一起却不适合承载关键金额。修正后前景改为 #9D321D,背景保持不变,对比度变成 4.83:1。

这段代码解决什么问题。 它把十六进制颜色转换为相对亮度,再计算前景与背景的对比度,供预检脚本统一判定。

ts 复制代码
interface RGBColor {
  r: number
  g: number
  b: number
}

export class ContrastInspector {
  static ratio(foreground: string, background: string): number {
    const lightA = this.luminance(this.parse(foreground))
    const lightB = this.luminance(this.parse(background))
    const lighter = Math.max(lightA, lightB)
    const darker = Math.min(lightA, lightB)
    return (lighter + 0.05) / (darker + 0.05)
  }

  static validate(name: string, foreground: string, background: string,
    largeText: boolean): AuditIssue | undefined {
    const ratio = this.ratio(foreground, background)
    const threshold = largeText ? 3.0 : 4.5
    if (ratio >= threshold) return undefined
    return {
      code: 'COLOR_CONTRAST_LOW',
      component: name,
      actual: ratio.toFixed(2),
      expected: `>=${threshold.toFixed(1)}`
    }
  }

  private static luminance(color: RGBColor): number {
    const channels = [color.r, color.g, color.b].map(value => {
      const s = value / 255
      return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4)
    })
    return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2]
  }

  private static parse(hex: string): RGBColor {
    const value = hex.replace('#', '')
    return {
      r: parseInt(value.slice(0, 2), 16),
      g: parseInt(value.slice(2, 4), 16),
      b: parseInt(value.slice(4, 6), 16)
    }
  }
}

计算本身并不复杂,难点在于"背景色是谁"。文本可能覆盖纯色、渐变、图片或半透明蒙层。预检器只能准确处理静态资源和明确的主题色;遇到图片背景时会标记 MANUAL_REVIEW,要求人工在亮色与暗色区域分别检查,不能拿一次平均色计算冒充结论。

实际项目还要处理深浅色主题。fontPrimary 在不同主题下对应不同颜色,扫描器必须分别生成 LIGHT 与 DARK 两份组合。只检查亮色模式,会漏掉深色背景上的次级文字问题。

三、字体放大后,截断只是表面现象

动态字体测试不只是把 fontSize 乘以 1.8。文字变高以后,固定高度容器、横向按钮、尾部图标和金额布局都会重新竞争空间。第一版 CheckoutConfirmPage 把说明区域固定为 44vp,字体放大后第二行被裁掉;确认按钮仍能点击,但用户已经看不到完整费用说明。

我们不在业务页面读取系统倍率后手算字号,而是尽量使用可跟随系统的字体单位与弹性布局。预检器做两件事:扫描固定高度承载文本的组合;在测试构建中注入 1.0、1.3、1.6、1.8 四档环境,记录组件是否溢出或重叠。

这段代码解决什么问题。 它给页面提供一套可测试的文字布局策略,把固定高度改为最小高度,并允许说明文案自然换行。

ts 复制代码
@Component
struct PriceNotice {
  @Prop title: string
  @Prop detail: string
  @Prop accentColor: ResourceColor

  build() {
    Row({ space: 12 }) {
      Column({ space: 4 }) {
        Text(this.title)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .maxLines(2)
        Text(this.detail)
          .fontSize(14)
          .fontColor($r('app.color.font_secondary'))
          .maxLines(4)
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)

      Text('查看明细')
        .fontSize(14)
        .fontColor(this.accentColor)
        .accessibilityText('查看费用明细')
    }
    .width('100%')
    .constraintSize({ minHeight: 44 })
    .padding({ top: 10, bottom: 10, left: 16, right: 16 })
  }
}

constraintSize({ minHeight: 44 }) 保留了常规字号下的设计基线,同时允许内容继续增高。左侧列使用 layoutWeight(1),右侧操作不会把说明文案压缩成极窄的一列。maxLines 仍然存在,但它表达的是产品允许的内容边界,不是依赖固定高度粗暴裁剪。

易错点是把所有文本都无限换行。按钮、金额和状态标签需要明确的降级策略:空间不足时可以改成上下排列,或把次要信息移到下一行。预检报告只负责指出溢出,最终布局取舍仍由产品和设计共同确认。

DevEco Studio 图里,左侧工程目录包含 ContrastInspector.ets、FontScaleRunner.ets 和 CheckoutConfirmPage.ets;中间代码正在检查 discountLabel;右侧模拟器使用 1.8 倍字体;底部 HiLog 打印 46 个组件、5 个问题和 2.71:1 的失败值。红圈只标记对比度与固定高度风险。

四、图标能点,不代表屏幕朗读能理解

右上角问号图标在视觉上很明确,但屏幕朗读只读出"按钮",用户不知道点击后会发生什么。扫描器无法从图标文件推断语义,因此对没有文本子节点的可交互组件强制要求 accessibilityText;装饰性图标则要明确从可访问树中排除,避免朗读产生噪音。

这段代码解决什么问题。 它为预检报告建立统一问题结构,并把缺失名称、过小触控区和固定尺寸风险汇总为可阻断项。

ts 复制代码
interface ComponentAuditMeta {
  id: string
  role: 'TEXT' | 'BUTTON' | 'IMAGE' | 'CONTAINER'
  clickable: boolean
  accessibilityText?: string
  widthVp?: number
  heightVp?: number
  containsText: boolean
  fixedHeight: boolean
}

export function inspectComponent(meta: ComponentAuditMeta): AuditIssue[] {
  const issues: AuditIssue[] = []
  if (meta.clickable && !meta.accessibilityText) {
    issues.push({ code: 'ACCESSIBLE_NAME_MISSING', component: meta.id })
  }
  if (meta.clickable && (meta.widthVp ?? 0) < 40 && (meta.heightVp ?? 0) < 40) {
    issues.push({ code: 'TOUCH_TARGET_SMALL', component: meta.id })
  }
  if (meta.containsText && meta.fixedHeight) {
    issues.push({ code: 'FONT_SCALE_CLIP_RISK', component: meta.id })
  }
  return issues
}

这里的 40vp 是项目基线,用于提前发现风险,不替代当前设计规范。扫描器只看静态元数据时会产生误报,例如父容器可能扩大了图标的真实点击区域;所以报告会同时记录组件路径与布局截图,开发者可以确认后标记豁免。豁免必须写明原因、负责人和到期版本,不能用一条全局忽略规则把问题藏掉。

五、把预检接进 Hvigor,而不是留在某个人电脑上

工具如果只能由作者手动运行,很快就会失去约束力。我们把它包装成构建任务:解析审计清单、启动四档字体测试、合并颜色与无障碍问题,最后输出 JSON 和 Markdown 报告。CI 对 BLOCKER 失败,对 MANUAL_REVIEW 提示人工确认。

这段代码解决什么问题。 它把扫描流程接入构建命令,并确保报告批次、版本和页面清单可追踪。

ts 复制代码
// hvigorfile.ts 中的项目任务示意
const auditTask = {
  name: 'accessAudit',
  run: async () => {
    const batchId = 'AUDIT-20260930-2057-12'
    const report = await AccessAuditRunner.scan({
      batchId,
      pages: ['CheckoutConfirmPage'],
      fontScales: [1.0, 1.3, 1.6, 1.8],
      colorModes: ['LIGHT', 'DARK']
    })
    await ReportWriter.write('build/reports/access-audit.json', report)
    if (report.blockers > 0) {
      throw new Error(`ACCESS_AUDIT_FAILED:${report.blockers}`)
    }
  }
}

示例表达的是工程组织方式,实际 Hvigor 扩展应按项目当前版本和插件能力注册任务。我们没有让脚本自动修改颜色或布局,因为这些改动涉及设计语义;自动工具只提供证据、组件路径和建议范围,最终修改仍需开发者审核。

报告第一次运行状态为 REVIEW_REQUIRED:46 个组件中 5 个问题。修复提交后,同一页面重新扫描:最低对比度 4.83:1、字体 1.8 倍无裁剪、可交互图标均有名称,阻断项 0,状态 PASS。

六、手机验收页要能解释修复结果

普通业务页只展示用户界面,不适合塞满技术指标。测试构建增加了 AccessAuditResultPage,把扫描批次、页面、主题、字体倍率、问题数量和修复前后数据放在一起。这样测试人员不需要翻 CI 日志,也能确认当前安装包对应哪份报告。

运行页显示批次 AUDIT-20260930-2057-12,页面 CheckoutConfirmPage,字体倍率 1.8x,LIGHT 与 DARK 均通过;低对比项从 2 降到 0,字体风险从 2 降到 0,缺失名称从 1 降到 0。红色箭头指向 2.71:1 → 4.83:1,说明修复发生在哪里。

诊断页则展示五个原始问题的组件路径和处置结果:discountLabel 的颜色已调整,priceNotice 与 agreementText 改为弹性高度,helpIcon 补充可访问名称。它和运行页不是同一张漂亮 UI,而是一份能够复核的工程证据。

七、预检工具的边界

静态扫描不能替代真实辅助功能测试。焦点顺序、弹窗打断、动态内容播报、手势操作和自绘组件的无障碍树,都需要在设备上验证。特别是 XComponent 或三方渲染内容,必须按照对应接入指南提供节点与事件,普通 ArkUI 组件扫描器看不到内部语义。

颜色规则也不能覆盖所有视觉场景。渐变、视频、图片蒙层与半透明叠加要做运行时采样或人工检查;品牌色不能因为"必须保留"就跳过可读性要求,可以改用描边、底色或文字层级保持品牌感。

最终我们把预检定义为三层:脚本发现可计算问题,设备页复核布局与朗读,官方上架检测作为交付门槛。三层各自负责不同证据,避免把一份自研 PASS 报告误当成必然审核通过。

八、这次治理留下的结果

这次改动最有价值的不是修掉 5 个问题,而是把"看起来没问题"改成了可重复验证。CheckoutConfirmPage 在 1.8 倍字体下不再裁剪,最低文本对比度从 2.71:1 提升到 4.83:1,所有可交互图标都有明确名称,构建报告与手机验收页使用同一个批次 ID。

以后新增页面时,开发者不需要记住全部历史反馈,只要运行同一任务,就能提前看到固定高度、主题颜色与可访问名称风险。真正进入上架流程时,团队面对的是少量复杂问题,而不是一批本可以在编码阶段发现的基础缺陷。

参考资料:

相关推荐
李游Leo2 小时前
HarmonyOS 7 ArkTS + ohpm:三方 JS 库副作用隔离与 API 26 升级回归【鸿蒙心迹】
javascript·回归·harmonyos
垆边人似月.5 小时前
华为机考题:明明的随机数
数据结构·算法·华为
AI备忘录5 小时前
(二十三)华为华三锐捷迈普思科 MLAG 跨设备配置命令(双活网关五厂商对照)
服务器·网络·华为
500845 小时前
React Native for OpenHarmony 实战:三方库 react-native-crypto-js 的鸿蒙化适配指南
javascript·react native·性能优化·electron·harmonyos
特立独行的猫A6 小时前
Godot 游戏编辑器移植鸿蒙 PC:难度与可行性分析
后端·harmonyos
熊猫钓鱼>_>6 小时前
Harmony Intelligence AI 开放能力深度解读 | 图像超分 + 文搜图:端侧视觉的“放大镜“与“搜索引擎“
人工智能·搜索引擎·harmonyos·鸿蒙·npu·图像超分·文搜图
三掌柜6666 小时前
ArkWeb 手记 10|把 ArkWeb 收成业务容器
harmonyos
500847 小时前
React Native for OpenHarmony 实战:三方库 react-native-device-name 的鸿蒙化适配指南
深度学习·react native·react.js·机器学习·harmonyos
三掌柜6667 小时前
ArkWeb 手记 07|缓存和离线页怎么做
缓存·harmonyos