深色主题中的浅色文字并不能直接沿用浅色主题的检查结论。文字用途相同,背景和状态底色却已经改变;如果页面只替换色块,问题等级仍停留在切换之前,检查工具就会把旧结论贴在新颜色旁边。沿着颜色解析与色对分级,本篇把主题、交互状态和业务身份一起纳入检查上下文。

主题令牌与状态色

保留用途身份,显式列出主题和状态

工程继续使用 ContrastDesk 的三个用途:正文、次级文本与提示文本,对应身份保持为 text-primary、text-secondary、text-hint。新增文件 model/ThemePairs.ets 负责颜色映射,model/ThemeCheck.ets 负责上下文结果,页面负责选择。原有解析、亮度和分级函数继续使用,主题切换不改动数学公式。

这里的主题选择用于检查设计数据,并不修改应用或系统的颜色模式。浅色和深色按钮是明确的样本选择器,界面外壳继续保持正常可读。资源接入时应把项目实际解析到的颜色交给同一检查入口,不能把按钮名称当作已经完成资源切换的依据。当前样本全部是不透明颜色,避免在主题计算中遗漏合成背景。

用途浅色前景深色前景
正文#1E3A5F#E8EEF7
次级文本#767676#B8C4D6
提示文本#999999#737F91

普通态背景分别为白色和 #16202E,选中态分别为 #D9E8FA 和 #28384F。选中态在这里表达选中项上的普通文本,仍采用普通文本规则;焦点边框、图标和停用控件需要各自的检查条件,不因采用相似底色就共享文字等级。

// model/ThemePairs.ets
import { PairSpec } from './PairCheck';
export interface TokenRow {
  id: string; name: string; light: string; dark: string;
}
export const TOKENS: TokenRow[] = [
  { id: 'text-primary', name: '正文', light: '#1E3A5F', dark: '#E8EEF7' },
  { id: 'text-secondary', name: '次级文本', light: '#767676', dark: '#B8C4D6' },
  { id: 'text-hint', name: '提示文本', light: '#999999', dark: '#737F91' }
];
export function themeBackground(theme: string, state: string): string {
  if (theme === 'light') { return state === 'normal' ? '#FFFFFF' : '#D9E8FA'; }
  return state === 'normal' ? '#16202E' : '#28384F';
}
export function resolvePairs(theme: string, state: string): PairSpec[] {
  if ((theme !== 'light' && theme !== 'dark') ||
    (state !== 'normal' && state !== 'selected')) {
    throw new Error('主题或状态无效');
  }
  return TOKENS.map((row: TokenRow): PairSpec => ({
    id: row.id, name: row.name,
    foreground: theme === 'light' ? row.light : row.dark,
    background: themeBackground(theme, state)
  }));
}

令牌表保留标准颜色文本,不在此处缓存亮度与等级。颜色修改后,解析和计算沿同一入口重新发生,避免同时维护颜色表和等级表。主题、状态名称先经过白名单检查,再进入映射函数;界面传入未知名称时,应给出输入提示,不能静默落到某个默认主题并继续展示结论。

让结果携带产生它的上下文

主题与状态是结果身份的一部分,但不是用途身份的一部分。把它们拼进原始用途名称,会让后续问题导航难以关联同一用途;只保留用途,又容易覆盖不同状态的结果。这里采用外层上下文加内层用途列表,既保留稳定身份,也能明确当前结论属于哪个组合。

// model/ThemeCheck.ets
import { PairResult, analyzePairs } from './PairCheck';
import { resolvePairs } from './ThemePairs';
export interface ThemeReport {
  theme: string; state: string; results: PairResult[];
}
export function checkTheme(theme: string, state: string): ThemeReport {
  const specs = resolvePairs(theme, state);
  const results = analyzePairs(specs);
  return { theme: theme, state: state, results: results };
}

检查成功返回新的上下文对象和结果数组。页面先建立候选,再一次替换报告,主题标签直接读取报告字段,避免按钮先显示新主题、列表却仍保留旧颜色。计算失败时保留旧报告和旧上下文,同时更新独立提示;用户仍然能判断当前可见结果属于之前的哪次选择。

深色普通态的三个比值约为十四点零六四、九点三和四点零四一,提示文本仍需调整。深色并不会自动改善所有文字的阅读条件,关键是相邻前景和背景之间的关系。选中态底色进一步变亮后,同一个浅色前景的比值也会变化,必须重新计算,而不是复用普通态记录。

主题检查开发预览

页面只提交完整的检查结果

下面的页面替换上一章入口,保留原有模型文件。结果预览区域使用待检查颜色,标签、比值与错误提示保持界面默认文字颜色。普通态和选中态由两个按钮选择,页面不通过点击动作的视觉反馈去猜测业务状态,数据中的状态字段是唯一依据。

// pages/Index.ets
import { PairResult } from '../model/PairCheck';
import { ThemeReport, checkTheme } from '../model/ThemeCheck';
@Entry
@Component
struct Index {
  @State report: ThemeReport = checkTheme('dark', 'normal');
  @State message: string = '普通文本 AA≥4.5 · AAA≥7';
  choose(theme: string, state: string): void {
    try {
      const next = checkTheme(theme, state);
      this.report = next;
      this.message = '普通文本 AA≥4.5 · AAA≥7';
    } catch { this.message = '选择失败,请核对主题与状态'; }
  }
  build() {
    Scroll() {
      Column({ space: 16 }) {
        Text('主题色对检查').fontSize('26fp')
        Row({ space: 12 }) {
          Button('浅色').onClick(() => this.choose('light', this.report.state))
          Button('深色').onClick(() => this.choose('dark', this.report.state))
        }
        Row({ space: 12 }) {
          Button('普通态').onClick(() => this.choose(this.report.theme, 'normal'))
          Button('选中态').onClick(() => this.choose(this.report.theme, 'selected'))
        }
        Text((this.report.theme === 'light' ? '浅色' : '深色') + ' · ' +
          (this.report.state === 'normal' ? '普通态' : '选中态'))
        ForEach(this.report.results, (item: PairResult) => {
          Column({ space: 10 }) {
            Text(item.name).fontSize('20fp')
            Text(item.foreground.hex + ' / ' + item.background.hex)
            Text('普通文本样例').padding(12).width('100%')
              .fontColor(item.foreground.hex).backgroundColor(item.background.hex)
            Text(item.ratio.toFixed(3) + ':1 ' + item.level).fontSize('20fp')
          }.width('100%').alignItems(HorizontalAlign.Start)
        }, (item: PairResult) => this.report.theme + ':' + this.report.state + ':' + item.id)
        Text(this.message)
      }.width('100%').padding(24).alignItems(HorizontalAlign.Start)
    }.width('100%').height('100%')
  }
}

示例沿用 API 20 及以上声明式界面工程,本地颜色计算和状态选择无需新增权限。应用层的 checkTheme 是项目函数,职责是组装数据并调用既有纯函数。原生组件负责显示与交互,系统主题配置与这里的样本选择应分别管理,避免把检查工具的状态传播到业务页面。

华为栅格页面适配示例展示了不同宽度下背景和布局的组织方式。背景如果随布局变化,色对也需要带上对应布局条件;本篇先固定主题和状态,下一章再把窗口内容宽度纳入展示路径,保持计算模型独立。

深色普通态检查结果

从用途、状态和数据提交顺序定位问题

调试时可以按四个组合逐一检查:浅色普通态、浅色选中态、深色普通态、深色选中态。每次切换后同时核对上下文标签、前景文本、背景文本和等级,而不是只观察色块。若颜色正确但等级滞后,应沿候选生成和报告替换路径排查;若只有某个用途异常,应从令牌行和用途身份查起。

主题令牌表应保持职责单一。新增用途时必须给出两套前景值,新增状态时必须给出对应背景,不能通过缺省字段继承另一个场景的结论。项目可以选择让缺失数据立即失败,或者建立明确的资源回退策略,但回退来源应可定位,不能把缺失输入包装成一个正常通过的结果。

当前表格是必要的业务数据,随正文完整提供;实际工程将它迁移到配置或资源层时,应保留相同协议。资源值若带透明度,需要先完成前景背景合成,再进入亮度计算。对比度判断继续遵循 W3C 普通文本阈值,页面取位不能改变分级。

结果对象不应由多个页面共同修改。需要跨页面使用时,应传递主题、状态和颜色输入的快照,再由统一入口生成结果;如果加入异步资源读取,还要核对返回值对应的选择是否仍然有效。主题标签和结果整体提交这条约束,可以自然延伸到后续的异步场景。

资源映射中还要区分语义令牌和具体色值。正文令牌表达用途,浅深主题表达环境,普通态与选中态表达交互条件。三者分开以后,同一用途可以拥有多组合法颜色,问题记录也能准确指出应修改哪一份配置。若把十六进制值直接写入用途名称,颜色调整会连带改变身份,历史问题便难以关联。

主题按钮切换并不需要提前清空列表,因为报告对象完整保留了当前上下文;失败提示也明确表示新选择没有形成新的报告。后续如果产品要求按钮立即高亮目标主题,就应增加独立的待选择字段,并同时标记结果仍属于旧上下文。不要让同一个状态字段既表达用户意图,又表达已经计算完成的结果。

状态背景变化时,应该对真实相邻关系建模。例如选中条目里面的说明文字和正文文字可以共享同一个选中背景,但悬浮面板里的文字未必如此。不能只按颜色名称建立组合,更不能把主题中所有前景与所有背景任意配对后,将产生的大量低比值全部归为页面缺陷。样本必须对应实际阅读区域。

当前模型返回新的对象,便于后续保留一份只读比较快照。比较两次主题调整时,可按用途身份对应前后记录,再同时展示两组标准颜色和原始比值。排序应服从业务用途,而不是每次根据比值重新洗牌;否则用户在切换时难以找到刚才正在观察的那一行。

浅深主题检查的重点是把每个结论放回具体的上下文。用途保持连续,状态明确列出,颜色通过同一入口解析与计算,页面就能在切换过程中维持可理解的一致结果。

Logo

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

更多推荐