HarmonyOS无障碍配色检查连载02:前景背景对比与问题分级【共创稿事节】
单个颜色的相对亮度并不能说明文字是否清楚,文字总是和具体背景一起出现。深灰文字放在白底上可能接近阈值,换到另一块背景后结论又会改变。沿着上一章的颜色解析,本篇把前景与背景组成稳定色对,计算完整精度的对比度,再将普通文本的检查结果分成三个明确等级。

将检查对象固定为相邻色对
样本保留深蓝正文与白底,增加次级文本和提示文本。每个样本都有业务身份、显示名称、前景和背景四个字段。身份用于后续主题映射与问题定位,名称服务于界面阅读;不要用颜色值作为唯一身份,因为不同用途可能恰好采用同一种颜色。
| 用途 | 业务身份 | 前景 | 背景 | 对比度约值 |
|---|---|---|---|---|
| 正文 | text-primary | #1E3A5F | #FFFFFF | 11.503 |
| 次级文本 | text-secondary | #767676 | #FFFFFF | 4.542 |
| 提示文本 | text-hint | #999999 | #FFFFFF | 2.849 |
上述数值来自颜色公式计算,页面显示三位小数,规则判断仍读取完整数值。当前协议继续接收不透明的标准红绿蓝颜色。如果设计采用透明前景、渐变背景或背景图片,应先明确文字相邻位置的有效颜色;不能直接把带透明度的编码删去一段后交给当前函数。
对比度由较亮相对亮度加常数,再除以较暗相对亮度加同一常数。前景背景交换后结果相同,但业务含义仍不同:检查记录应保留哪一个是文字、哪一个是背景,不能因为比值对称就丢失字段角色。
规则比较保留完整精度
本项目采用 WCAG 2.2 的普通文本色对阈值:基础对比度为四点五,增强对比度为七。项目将结果显示为增强通过、基础通过和需调整,描述当前普通文本色对满足哪一级阈值。
规则把字号与字重条件明确分开,当前统一走普通文本分支。大文本有不同条件,不能根据界面里一个字体相对单位的数值就自动选择更低阈值。控件边界、图标等非文本元素也应采用对应规则,而不是复用普通文本等级名称。
// model/ContrastRule.ets
export function contrastRatio(first: number, second: number): number {
if (!Number.isFinite(first) || !Number.isFinite(second) ||
first < 0 || first > 1 || second < 0 || second > 1) {
throw new Error('相对亮度无效');
}
return (Math.max(first, second) + 0.05) / (Math.min(first, second) + 0.05);
}
export function gradeRatio(ratio: number): string {
if (!Number.isFinite(ratio) || ratio < 1 || ratio > 21) {
throw new Error('对比度无效');
}
if (ratio >= 7) { return '增强通过'; }
return ratio >= 4.5 ? '基础通过' : '需调整';
}
边界比较使用大于等于,不在函数内提前取位。灰色 #777777 与白底的比值约为四点四七八,虽然取一位小数看起来接近四点五,仍应归入需调整。界面显示和阈值判断分别维护,才能避免“显示值达到阈值但原值没有达到”的错误通过。
亮度函数已经约束到零与一之间,正常色对的对比度落在一到二十一范围内。公共函数仍检查非有限数值和越界输入,防止上游损坏的数据进入等级判断。不能把异常值默认归入增强通过,也不能把计算失败与低对比度混成同一种问题。
构建独立的色对结果
新增 model/PairCheck.ets,沿用上一章的标准颜色对象与亮度函数。结果中保留标准前景、标准背景、完整比值和等级;页面不用重新解析颜色或重复写阈值。检查集合时先拒绝重复身份,再建立整组候选,某个样本解析失败会中止当前候选,不提交半份结果。
// model/PairCheck.ets
import { ColorValue, parseColor } from './ColorValue';
import { relativeLuminance } from './Luminance';
import { contrastRatio, gradeRatio } from './ContrastRule';
export interface PairSpec {
id: string; name: string; foreground: string; background: string;
}
export interface PairResult {
id: string; name: string; foreground: ColorValue; background: ColorValue;
ratio: number; level: string;
}
export const PAIRS: PairSpec[] = [
{ id: 'text-primary', name: '正文', foreground: '#1E3A5F', background: '#FFFFFF' },
{ id: 'text-secondary', name: '次级文本', foreground: '#767676', background: '#FFFFFF' },
{ id: 'text-hint', name: '提示文本', foreground: '#999999', background: '#FFFFFF' }
];
export function analyzePair(spec: PairSpec): PairResult {
const foreground = parseColor(spec.foreground);
const background = parseColor(spec.background);
const ratio = contrastRatio(relativeLuminance(foreground), relativeLuminance(background));
return { id: spec.id, name: spec.name, foreground: foreground,
background: background, ratio: ratio, level: gradeRatio(ratio) };
}
export function analyzePairs(specs: PairSpec[]): PairResult[] {
const ids: Set<string> = new Set();
specs.forEach((spec: PairSpec) => {
if (spec.id.trim().length === 0 || ids.has(spec.id)) {
throw new Error('色对身份无效或重复');
}
ids.add(spec.id);
});
return specs.map((spec: PairSpec): PairResult => analyzePair(spec));
}
这里的业务身份描述文字用途,后续浅色与深色主题可以沿用相同身份,再分别提供颜色数据。不同主题下结论可能不同,因此结果还需要与主题上下文对应,不能只把一个用途曾经通过的结论永久保存。当前所有样本都明确采用白色背景,先把单主题链路固定下来。
华为颜色对比度检查规则给出了文本与背景至少四点五比一的可读性要求。这里的应用层检查可以消费运行时样本,工程中的静态资源检查继续保留,两条路径共同帮助定位问题。示例采用 API 20 及以上开发套件,纯计算与本地预览无需额外权限。

用文本说明等级,避免只依赖颜色
页面将三组结果按既定顺序显示,色对预览使用各自前景和背景,等级文字放在预览之外。提示文本的灰色较浅,问题说明本身仍应保持正常可读,不能把需调整的颜色继续用于整个问题卡片。按钮触发重新检查时,全部候选成功才替换列表,失败提示单独更新。
// pages/Index.ets
import { PairResult, PAIRS, analyzePairs } from '../model/PairCheck';
@Entry
@Component
struct Index {
@State results: PairResult[] = analyzePairs(PAIRS);
@State status: string = '已计算';
check(): void {
try {
const next = analyzePairs(PAIRS);
this.results = next;
this.status = '已计算';
} catch { this.status = '检查失败,请核对色对数据'; }
}
build() {
Scroll() {
Column({ space: 20 }) {
Text('普通文本配色检查').fontSize('26fp')
Text('普通文本 AA≥4.5 · AAA≥7').fontSize('16fp')
ForEach(this.results, (item: PairResult) => {
Column({ space: 12 }) {
Text(item.name).fontSize('20fp')
Text(item.foreground.hex + ' / ' + item.background.hex).fontSize('16fp')
Text('普通文本样例').fontSize('18fp').width('100%').padding(12)
.fontColor(item.foreground.hex).backgroundColor(item.background.hex)
Text(item.ratio.toFixed(3) + ':1').fontSize('22fp')
Text(item.level).fontSize('18fp')
}.width('100%').alignItems(HorizontalAlign.Start)
}, (item: PairResult) => item.id + ':' + item.foreground.hex + ':' +
item.background.hex + ':' + item.ratio + ':' + item.level)
Text(this.status).fontSize('18fp')
Button('重新检查').onClick(() => { this.check(); })
}.width('100%').padding(24).alignItems(HorizontalAlign.Start)
}.width('100%').height('100%')
}
}
结果键包含样本身份与当前颜色和结论,保证样本更新后重建对应卡片。进入多设备布局时,卡片可以从纵向排列改为多列,但不能让布局分支重新定义阈值,也不能根据列宽改变样本颜色。计算数据与界面组织分离,同一组色对才能在不同区域保持一致结论。

检查阈值附近与数据变化路径
数学检查应覆盖黑白色对、相同颜色、前景背景交换,以及阈值前后极近的比值。恰好四点五属于基础通过,恰好七属于增强通过,略低于阈值的数值应保持原等级。三位小数的文字只用于展示,不能作为下一次判断的输入。
结果分级需要保留明确的规则身份。当前普通文本的两档阈值是固定项目策略,今后增加图标、焦点边框或大文本时,应建立新的规则入口,或让用途显式选择规则。不能在一个共享函数里依据名称猜测用途,也不能因为某个颜色原来用于正文,就把它在其他控件状态中的结论直接复用。
问题列表的稳定身份同样重要。正文、次级文本和提示文本可以采用相同前景颜色,但它们在设计系统里承担不同职责。后续修改次级文本令牌时,只应影响对应样本;将颜色值当作身份会让这些用途被合并,难以追踪问题来自哪一条资源配置。业务身份应与显示颜色分开保存。
阈值附近的结果应优先解释原始计算值。开发者看到四点四七八时,可以直接定位到前景与白底的组合,再讨论加深文字或调整背景;不宜只显示一个红色图标,让用户猜测是输入格式、计算异常还是对比度不足。不同原因应保留不同提示路径,等级文字则明确当前色对的阅读风险。
页面的错误处理也要对应数据快照。当前集合为固定样本,因此失败后保留旧结果仍能用于查看;增加颜色编辑或主题切换以后,旧结果必须与旧输入身份一起保存,或者立即标记失效。候选成功后整体替换可以减少半张列表已经更新、另一半仍是旧主题的情况。
对于背景会变化的组件,还需要把交互状态作为检查维度。普通态、选中态和按压态可能使用不同底色,仅检查静态页面的一组颜色会漏掉这些关系。当前三组白底样本提供基础链路,下一步应由主题令牌和状态样本明确列出需要相邻比较的色对,而不是遍历所有颜色任意两两配对。
数据检查还应覆盖重复身份、缺少颜色、透明度写法和非法通道字符。失败时保留旧结果并显示失败状态,后续若增加编辑能力,应在输入改变时让旧结果退出当前有效状态。不可把空结果数组直接解释成所有样本都通过,也不可把旧样本结论套到新主题上。
前景背景形成明确色对以后,问题等级就能回到具体用途和具体颜色。下一篇将在相同身份与计算模型上加入浅深主题令牌,继续检查状态色与资源切换后的对应关系。
更多推荐

所有评论(0)