HarmonyOS 7 GridRow:折叠屏作品卡换列无障碍标注【鸿蒙心迹】
一、卡片换成两列之后,谁来定义“下一项”
数字博物馆里有一组很普通的作品卡:青铜器、山水卷、拓片、瓷瓶、书法和玉器。手机窄屏上,六张卡从上往下排,编号和阅读顺序自然一致。展开折叠屏,设计希望把卡片拆成两列,每列放三件作品,视觉上能更快扫到不同类别。这个愿望在视觉设计里很容易实现,放到键盘导航和屏幕朗读的视角,就多出一件不能凭直觉判断的事情:手指或焦点下一个到达的对象,是否仍是作者期待的第二件作品。
页面布局顺序、业务作品顺序、无障碍实际焦点顺序并不是同一个变量。开发者修改GridCol的order,首先改变的是栅格组件的展示顺序;设置accessibilityText,首先提供的是被访问时应读出的文字。这两件事都重要,但既不能靠改一张图就证明读屏路径正确,也不能用朗读标签冒充焦点顺序控制。如果把这两个层次混为一谈,验收图上显示“正确”,视障用户拿到设备时仍可能听到跳项、重复播报或找不到下一张卡片。
本篇在设计阶段先处理一个可复核的问题:同一份作品列表因为窄屏、宽屏重排而出现四个视觉顺序不一致项,如何让业务上的阅读序列始终有一个稳定参照,并为每张卡片提供完整的无障碍语义。Demo叫A11yOrderDesk,只给出业务序列模型、ArkUI布局示例及模拟诊断界面,尚未用真实读屏服务和键盘完成验证。

数据集为museum_cards_06,任务ORD-1010-23,六个稳定ID固定为W01到W06。页面名GalleryPage,诊断页OrderAuditPage,序列模型ReadingOrderPlan。窄窗口宽390vp,展开后宽820vp;记为两个布局快照,layoutEpoch=2。初始的演示重排列为W01 W04 W02 W05 W03 W06,对比规范序列出现四处位置不一致;校正后的可视序列是W01 W02 W03 W04 W05 W06,模型差异计数为0,业务状态ORDER_PLAN_READY。注意最后这个状态只表示规划结果一致,屏幕朗读真实测量仍是NOT_RUN。
二、先问展陈在讲什么,再谈卡片怎么排列
展品卡不是六个可随意交换位置的矩形。馆方可能希望先介绍器物,再介绍书画,让观众从材料与用途走向审美与文字。如果布局组件中的数组就是唯一真相,产品为了适应横屏把数组重排,很可能顺手改变业务推荐顺序;下一次又要根据新的数组算“上一件”“下一件”,最终连深链分享和语音导览都跟着漂移。真实展馆页面里,这种漂移往往不是布局Bug,而是数据模型从一开始就没有区分“内容本身”和“显示安排”。
因此我的第一条判断是:给每张卡一个不依赖数组下标的稳定ID,并为策展顺序建立显式rank。业务上从第1项走到第2项,永远按rank推进;布局可以使用另一个纯展示映射,但不能覆盖策展排序。展示顺序仅是一份投影,阅读序列是另一份长期有效的内容合同。即使作品标题改名、图片换封面,ID也不应改变;否则语音导览与焦点恢复可能找不到原本的对象。
本例的六张卡分别是W01 青铜器、W02 山水卷、W03 拓片、W04 瓷瓶、W05 书法、W06 玉器。这些名称只用于演示,不涉及真实馆藏出处或作品鉴定。模型同时存储唯一ID、展示名称和rank,避免“排序靠名称、身份靠索引”的常见捷径。以后接入真正的数据仓库,还需要解决新增、下架、内容审核、版本快照等问题,但不应动摇稳定身份的原则。
第一段代码解决的是数据源可能被意外打乱的问题。它将来源顺序按rank稳定排序,并对重复ID和重复rank给出显式错误。这样一份错误的内容清单不会被某种屏宽恰好掩盖。代码属于应用自建数据模型,不依赖特定设备形态;rank从一开始就是人为定义的业务语义,不是GridRow偷偷推断出来的值。
export interface MuseumCard {
id: string;
title: string;
rank: number;
}
export function canonicalCards(input: MuseumCard[]): MuseumCard[] {
const idSet = new Set<string>();
const rankSet = new Set<number>();
input.forEach((card: MuseumCard) => {
if (!card.id || !Number.isSafeInteger(card.rank) || card.rank < 1 ||
idSet.has(card.id) || rankSet.has(card.rank)) {
throw new Error(`CARD_ID_OR_RANK_INVALID:${card.id}`);
}
idSet.add(card.id);
rankSet.add(card.rank);
});
return input.slice().sort((a: MuseumCard, b: MuseumCard) => a.rank - b.rank);
}
这里特意用slice生成新的排序数组,而不直接在共享入参上调用sort。假如左侧列表、详情页、导览栏都在引用同一份数组,原地修改会让多个组件同时出现难以追踪的状态改变。模型异常也应该保持“失败不改数据”的性质:发现重复身份后抛出错误,页面展示上一次可信版本并提示编辑人员,而不是带着半份乱序清单继续演示。
三、GridRow的order解决呈现,却不替代语义校验
华为官方2026年9月的GridRow/GridCol指南明确了栅格断点、列数、span和order的含义。GridCol的order用于决定子组件的展示先后;没有设置或order相同的项目,会根据组件在代码中的顺序参与排列。对我们的任务而言,最危险的写法是某些卡设置了order、另外几张没有设置,再凭肉眼推断顺序会保持不变。文档对这类混合设置有专门说明:显式配置的子项可能排在未配置项之后,而不是自然插回原位置。
我的做法是保证六张卡全部拥有明确的rank,然后在界面上把rank用于order。窄态四栅格列时每件作品占满四列,展开到八栅格列时每件占四列,呈现从单列变成双列。这里的sm与md是栅格断点类型,默认sm覆盖320至600vp、md覆盖600至840vp;390vp和820vp只是两个用于说明的宽度样例,并非所有折叠屏的固定规格。用户调大字体、使用分屏、多窗口改变可用容器宽度时,布局可能再次发生变化。
第二段代码展示页面上的关键区域。ForEach使用id而非数组下标作为组件键,卡片容器显式标记一个无障碍组,并用accessibilityText提供包含序号和标题的简洁文本。它没有擅自调用无障碍焦点控制接口,也不声称order自动修改系统读屏聚焦顺序;后者需要在设备上用屏幕朗读服务单独验收。为了让示例保持短小,封面图片用资源路径占位,实际项目需要在资源目录中提供文件。
@Entry
@Component
struct GalleryPage {
@State cards: MuseumCard[] = [
{ id: 'W01', title: '青铜器', rank: 1 },
{ id: 'W02', title: '山水卷', rank: 2 },
{ id: 'W03', title: '拓片', rank: 3 },
{ id: 'W04', title: '瓷瓶', rank: 4 },
{ id: 'W05', title: '书法', rank: 5 },
{ id: 'W06', title: '玉器', rank: 6 }
];
build() {
Scroll() {
GridRow({ columns: { sm: 4, md: 8 } }) {
ForEach(this.cards, (card: MuseumCard) => {
GridCol({ span: { sm: 4, md: 4 }, order: card.rank }) {
Column() {
Text(card.title).fontSize(18)
Text(`编号 ${card.id}`).fontSize(12)
}
.accessibilityGroup(true)
.accessibilityText(`第${card.rank}项,${card.title}`)
}
}, (card: MuseumCard) => card.id)
}
}
}
}
要注意这不是一段可以替代项目所有配置的完整源码。MuseumCard接口、资源文件、路由与数据注入需要按工程目录组织;这里聚焦的是稳定ID、统一order、整卡语义。accessibilityGroup(true)让子内容作为一组被感知,也要求开发者认真写完整提示,不能只留下“图片一”这种没有意义的播报。若卡片含真正可点击的子按钮,还要考虑是否应该分组,避免将互动入口隐藏在不可逐个操作的组中。

四、一个看似很小的重排,为什么能出现四处差异
在当前固定数据中,窄态语义顺序是W01 W02 W03 W04 W05 W06。设计将宽态做成“左列前三件,右列后三件”的版式,如果把列中元素直接扁平化成两行优先的视觉阅读序列,就会得到W01 W04 W02 W05 W03 W06。逐位置比较后,第1位W01、第6位W06仍正确;中间四个位置各与规范序列不同,所以差异计数是4。这里的差异只是我们定义的视觉映射审核,不等于读屏服务报告了四次焦点错误。
解决它不是往每张图片旁边再加一个编号,而是让生产布局映射的地方使用相同的rank。当按rank创建并排序所有GridCol后,当前示意中的两列按行主序排成W01/W02、W03/W04、W05/W06。视觉上仍有双列的宽态效率,但每行从左到右、下一行从上到下更符合我们规定的阅读叙事。即使将来因为美术需求再次交换某个展品的位置,也会清楚地留下“修改了业务阅读序列”或“只修改了视觉投影”的变更记录。
A11yOrderDesk的首页采用宽态820vp作为示意,展示六件作品及完整序号。与之对应,OrderAuditPage可以同时列出规范、调整前和调整后序列。首页需要让审核者快速看到六张卡与布局形态,诊断页则需要回答到底哪些位置曾经错、改了以后怎么证明应用侧比较通过。让两张图片承担不同证据角色,比在一张UI里塞进所有统计信息更容易发现不一致。

最终状态ORDER_PLAN_READY代表业务模型和我们设定的视觉映射相符;screenReader=NOT_RUN代表无障碍服务尚未做实际验证。没有读屏录屏、键盘巡检日志和焦点事件轨迹,就不能说“用户现在一定按W01到W06听到内容”。实际的辅助服务还可能按无障碍树层级、组件分组与可聚焦属性做自己的遍历,布局视觉顺序只是其中的一个参考。这个边界越早写清楚,工程交付越不会在验收时出现一句话承诺过大。
五、审核器不碰焦点,但它能先排除明显的业务漂移
为了让布局变化可被测试,我采用纯函数来对照业务顺序和某次视觉快照。输入是期望ID数组和显示ID数组,输出是位置不相同的数量。如果长度不等、出现未知ID、丢失卡片,则不返回一个好看的0,而是直接判作结构错误。如此一来,删除W03又在另一处重复W05的页面,不会因为某些位置巧合匹配而被认为“只是两个位置错误”。
第三段代码把“缺失/重复”“位置错误”分开。它在应用侧运行,和华为GridRow实现内部无关,能在独立的ArkTS测试环境中验证。测试输入来自布局方案模拟器,不是现场无障碍树;若将来接入真实导航事件,也应该保留这条纯模型检查作为低成本的前置防线。
export function countPositionDrift(expected: string[], actual: string[]): number {
if (expected.length !== actual.length) throw new Error('CARD_COUNT_CHANGED');
const unique = new Set<string>(actual);
if (unique.size !== actual.length) throw new Error('DUPLICATE_VISIBLE_ID');
for (const id of actual) {
if (!expected.includes(id)) throw new Error(`UNKNOWN_CARD:${id}`);
}
let drift = 0;
for (let i = 0; i < expected.length; i++) {
if (expected[i] !== actual[i]) drift += 1;
}
return drift;
}
const canonical = ['W01', 'W02', 'W03', 'W04', 'W05', 'W06'];
const before = ['W01', 'W04', 'W02', 'W05', 'W03', 'W06'];
const after = ['W01', 'W02', 'W03', 'W04', 'W05', 'W06'];
if (countPositionDrift(canonical, before) !== 4) throw new Error('fixture changed');
if (countPositionDrift(canonical, after) !== 0) throw new Error('order not repaired');
把业务审核器放在布局端口之外,还有一个维护好处:屏幕形态只是输入参数,而不是安全和语义判断的来源。未来展馆增加平板、桌面窗口、分屏或平行视界,设计可以为不同宽度采用新的列数,但countPositionDrift仍只关心这六张卡的内容合同。新增第七张作品时,应该先调整稳定序列,再生成每种宽度的展示方案;不能仅凭首页能把第七张卡放进去,就认为详情页的“下一件”已自然跟随。
六、产品审阅最容易忽略的四个场景
首先是字体放大。标准字体大小下双列宽度足够,不代表系统无障碍大字号时仍适合双列。文字可能换行、按钮可能挤出可点击区域。布局使用容器宽度断点只是基础,实际要把字体倍率、文案长度和辅助功能设置一起带入验收矩阵。必要时让内容重新回到单列,也比勉强维持两列让说明文字被截断更可靠。
其次是互动卡片。这里每张卡只有标题和序号,分组较简单。若将来卡片含收藏、音频导览、查看详情三个按钮,直接把整个容器设置为一个可访问项,可能使子操作不可单独访问。应该按使用目标决定哪些对象需要独立焦点,并给按钮补上动词与对象,例如“播放青铜器导览”,而不是只写“播放”。朗读文本是功能描述,不是将页面上所有字逐字重复一次。
第三是页面恢复。用户在W04详情中折叠屏幕,返回后窗口由820vp变为390vp,布局必然发生变化,但当前作品仍是W04。焦点归还应该依赖稳定ID,不应按“返回时列表第4个组件的地址”重新定位,因为组件可能重建。本文没有实现主动焦点恢复API,只规定恢复目标始终来自稳定ID;真正需要聚焦时,应参考Accessibility Kit提供的主动聚焦规则,且在目标组件已经渲染并可访问之后再尝试触发。
第四是装饰元素。栅格卡片旁可能添加“热度”、“推荐”、“同类作品”图标。若每个装饰都被读作独立对象,原本六项的阅读流程会被许多无用停顿打断。应明确哪些图形只承担视觉装饰,哪些图标包含真实信息,并通过无障碍属性控制。给一个纯装饰边框配accessibilityText反而会降低体验。这些取舍与界面是否漂亮没有必然关系,却会直接影响用户完成一次浏览操作的次数。
七、诊断页要暴露未知,而不是把未测试写成通过
诊断页给出三段序列:规范W01 W02 W03 W04 W05 W06,错误的视觉方案W01 W04 W02 W05 W03 W06,修正方案回到规范顺序。时间轴在16:32:10建立窄态390vp快照,16:32:11切到展开态820vp,16:32:12比较出四个位置差异,16:32:13应用有序布局得到零差异,16:32:14把状态标记为ORDER_PLAN_READY。这些时间为设计样例,不是可证明的设备事件,也没有单位毫秒的响应时长指标。

如果把“阅读顺序”只做成一个绿色检查项,测试同学容易以为已经开屏幕朗读滑过六张卡。这里特意把FIXTURE_ONLY和NOT_RUN放在醒目位置,意味着系统辅助服务真实焦点与语音播报仍未验证。实际验收应记录至少两种窗口形态、默认与放大字体、触摸浏览与键盘导航、打开详情再返回、切后台后恢复等路径。对每个路径采集真实顺序,才能把本文的业务预检与设备验收衔接起来。
八、结论留在可重复的层次
本例完成的是“六件作品有确定排序”“两种宽度共用一份语义ID合同”“应用模型能检测四处视觉投影位置偏差”“调整后模型差异计数归零”。还没有完成“读屏真实从W01读到W06”“系统焦点不会重复或跳过”“所有设备文字放大后仍可访问”这些验收。因此不能仅凭图片里六张卡排得整齐,就把无障碍适配写成已完成。后续若需要通过无障碍审核,必须留出真机、系统辅助服务及交互任务的观察记录。
与单纯追求折叠屏双列布局相比,我更在意一份被所有界面尊重的阅读序列合同。视觉设计可以为不同屏幕改变列数、留白与图片比例,但业务身份不能随手变;GridCol.order能帮助稳定视觉顺序,accessibilityText能补充内容语义,真正的屏幕阅读路径仍由设备验收来回答。把能够自动验证的部分提前完成,把尚未测量的部分坦诚留下,才是这类多形态界面可以持续维护的基础。
官方依据:华为2026-09-08《Responsive Grid Layout (GridRow/GridCol)》明确栅格断点、span与order;华为《Screen Reading Adaptation Guide》介绍accessibilityText与整组可读语义,且要求对网格或列表的每个项目提供完整信息。本文中的序列审核器、样例宽度、六件馆藏卡及差异计数均为应用自建业务模型,不是华为平台返回的无障碍审计结论。
参考:https://developer.huawei.com/consumer/en/doc/harmonyos-guides/arkts-layout-development-grid-layout;https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V14/screen-reading-adapt-guide-V14。
九、把这份计划交给真正的验收人员
应用侧模型完成后,真正的验收人员应拿到的不只是一个测试版安装包,而是一份清楚的观察表。表里需要列出目标设备和系统版本、应用target API、窗口宽度、系统文字倍率、辅助服务是否开启、测试者采用触摸探索还是线性滑动,以及六张卡在每次操作中的实际到达顺序。某次测试如果因为设备设置不同而得到不同结果,记录完整上下文,比简单写“偶现失败”更能帮助后续改动。
对窄态390vp,先检查六张作品都能按规范顺序逐项触达,再打开W03详情、返回列表,看焦点能否落到合理的相邻项目;对展开态820vp,重点检查每行左右两项的聚焦顺序与阅读说明。试一次从屏幕边缘折叠、试一次打开应用后直接展开,再试一次在另一个窗口中恢复。系统很可能在这些阶段重建布局树,所以只验证打开首页的一条路径远远不够。若发现焦点没有按预期移动,应该进一步区分是布局树顺序、组件分组、焦点策略还是主动恢复时机导致的问题,不能把所有现象一概归咎于GridRow。
验收记录还要包含用户操作的结果,而非只数朗读节点。听到“第4项,瓷瓶”之后,打开详情是否真的是W04?返回后是否仍指向W04?用户收藏W05会不会因为屏宽变换错误操作W03?这些看起来超出“阅读顺序”的问题,实际上都与稳定ID和视觉重排有关。一个只能把语音报得整齐、却不能保证动作落到正确内容上的页面,也不能算完成了无障碍适配。
从调试角度,后续可以补充三种不同日志。第一种是业务顺序快照,包括六个ID和各自rank;第二种是布局决策,记录当时宽度、断点和每个GridCol的显式order;第三种才是真正的辅助服务焦点事件,记录可访问节点的目标业务ID和时间。只有把这三条日志在同一个窗口快照下对齐,才有可能解释“业务顺序正确、视觉顺序正确、读屏却跳过一项”的故障。本文没有第三种日志,所以不编造事件,也不伪造读屏已通过的统计。
最后要留下回退方案。如果一个新布局在放大字体时暴露无法解释的焦点问题,产品应允许先回退到稳定单列,而不是为了保持双列效果让辅助技术使用者承担风险。回退应基于可观测条件、保留当前作品ID和浏览位置,不能清空用户正在阅读的内容。需要恢复双列时,再用相同的六项顺序合同复核;这比给某个设备型号写死一段例外逻辑更容易长期维护。
本轮验收边界因此被明确写成两栏:可自动断言的,是ID唯一、rank唯一、六项齐全以及位置漂移从4变0;必须在设备完成的,是辅助服务真实阅读、键盘焦点、文字放大和页面恢复。前者可以纳入持续集成,后者需要设备矩阵与人工观察。没有第二栏的证据,任何绿色ORDER_PLAN_READY都只属于模型,不属于真正的用户体验承诺。
更多推荐




所有评论(0)