ArkUI + HarmonyOS 知识卡片:无障碍与多端兼容收尾(连载06/06)【共创稿事节】
最后一次评审,我让同一张“页面导航”知识卡片在三种操作条件下被打开:手指触控、外接键盘以及屏幕朗读。最容易被忽略的差异是,用户目光看到的“蓝色选中框”并不等于朗读服务能够说清它代表什么;键盘焦点也不等于业务选中状态。前五篇已经拥有卡片 id/title/category/summary、selectedId、focusedId、NavPathStack 和宽窄窗口适配,本篇不再创建新导航系统,而是审视这些能力怎样为不同阅读方式协同服务。

图1:知识卡片的多种访问方式。
图1:知识卡片的多种访问方式。
一、先讲清“看到、听到、操作到”是不是同一件事
手机单栏中,用户点击卡片后进入详情;宽窗口的列表与详情可以同时出现,且 selectedId 负责说明当前阅读哪张卡片。纯视觉用户依赖边框颜色判断高亮,使用屏幕朗读的用户需要听见标题、分类、是否选中以及操作结果;使用键盘的用户需要清楚的焦点指示。把所有信息写在图片里,会使朗读结果缺失;用颜色作为唯一状态标记,同样让部分用户难以辨认。
设计时先给每张卡片一个简单的朗读规则:“页面导航,Stage 分类,已选中;打开详情”。未选中时改成“未选中”;真正的摘要内容放进详情页,避免首页每个控件都读出长段文字。组合成一个可感知的卡片组件,减少装饰图与标题被反复读出的情况。B04 的 focusedId 仍只代表键盘当前位置,不能在 onFocus 中把它直接写成 selectedId,否则用户移动 Tab 就会意外打开内容或改写历史。

图2:触控、键盘与朗读通道共享业务身份。
图2:触控、键盘与朗读通道共享业务身份。
在 B05 的 CardTile 上做增量修改即可。需要补上 summary 的展示入口,但不改变 KnowledgeCard 原先的四个字段;cardId 继续作为组件稳定身份,onActivate 仍交给 B04 的 openCard(id)。下面使用 ArkUI V1 的 @Component、@Prop,沿用前文项目,不混入 V2 的 @Local 语法。
// components/CardTile.ets:替换 B05 的单卡组件
@Component
export struct CardTile {
@Prop cardId: string = '';
@Prop title: string = '';
@Prop category: string = '';
@Prop highlighted: boolean = false;
@Prop focused: boolean = false;
onActivate: (id: string) => void = (_id: string): void => {};
onFocused: (id: string) => void = (_id: string): void => {};
build() {
Column({ space: 8 }) {
Text(this.category).fontSize(14).fontColor('#334155')
Text(this.title).fontSize(19).fontWeight(FontWeight.Bold)
.fontColor('#102A43')
Text(this.highlighted ? '当前阅读' : '打开阅读')
.fontSize(13).fontColor('#215F8B')
}
.alignItems(HorizontalAlign.Start)
.padding(16).width('100%')
.backgroundColor(this.highlighted ? '#E3F0FF' : '#FFFFFF')
.border({ width: this.focused ? 3 : 1,
color: this.focused ? '#B45309' : '#9CB8D2' })
.borderRadius(12)
.id(`card_${this.cardId}`)
.focusable(true)
.accessibilityGroup(true)
.accessibilityLevel('yes')
.accessibilityText(`${this.title},${this.category} 分类,` +
`${this.highlighted ? '已选中' : '未选中'}`)
.accessibilityDescription('打开知识卡片详情')
.onFocus((): void => this.onFocused(this.cardId))
.onClick((): void => this.onActivate(this.cardId))
}
}
这里的 accessibilityText 是给朗读系统的可见信息替代文本,accessibilityDescription 用于解释操作意图,accessibilityGroup 把卡片内部子组件组织为一个可访问单元。装饰性图片应避免重复朗读,可以在不承载信息的图片上按官方规则设置 accessibilityLevel('no')。实际朗读顺序和系统播报的控件类型要在设备开启屏幕朗读后核对,不能仅凭字符串拼接推断完整播报行为。如果实际使用图片承载了内容,必须另有文字等价信息。
二、别让窗口切换制造第二份阅读历史
B01 的 600vp、840vp 是工作台自己定义的布局断点,B02 的 NavigationMode.Auto 交由导航容器根据窗口条件选择模式。无障碍适配不应该重写这两个规则。窗口宽度、卡片列数和导航呈现可以变化,业务选中身份仍旧是 selectedId。当用户在双栏中选中 k03 后缩小窗口,列表可能暂时让位给详情,但回到列表时应仍能定位 card_k03。若项目启用了 B04 的 restoreTarget,请求焦点之前应确认目标卡片还存在且列表可见;恢复失败时保留正常的键盘导航路径。
动态字体是另一个需要专门检查的变量。开发者容易在固定卡片高度、固定图片宽度和一行截断之间做出看似整齐的布局,系统字体放大后却出现按钮被挤出屏幕的情况。原则是让文字有换行空间,长标题允许增加行高;布局宽度不足时减少并排操作,不要把字号缩小到用户难以阅读。针对窄窗口,列表优先单列;宽窗口可以将两栏保持独立滚动,但焦点路径应从导航区依次进入列表再进入详情,而不是在视觉上跨列乱跳。

图3:窗口尺寸与辅助功能的交叉检查项。
图3:窗口尺寸与辅助功能的交叉检查项。
下面的轻量兼容检查器适合放在 model/CardLayoutPolicy.ets,它只返回业务布局策略,不调用设备型号接口,更不决定系统导航实际采用堆叠还是双栏。其边界与 B01 保持一致,明确在恰好达到阈值时进入下一档。为避免把示例代码变成无效调用,templateFor() 返回可直接给 Grid.columnsTemplate() 使用的列模板字符串。
// model/CardLayoutPolicy.ets
export type CardLayoutMode = 'compact' | 'medium' | 'expanded';
export function modeForWidth(vp: number): CardLayoutMode {
if (!Number.isFinite(vp) || vp < 0) { return 'compact'; }
if (vp < 600) { return 'compact'; }
if (vp < 840) { return 'medium'; }
return 'expanded';
}
export function templateFor(mode: CardLayoutMode): string {
if (mode === 'expanded') { return '1fr 1fr 1fr'; }
if (mode === 'medium') { return '1fr 1fr'; }
return '1fr';
}
export function validSelection(ids: string[], selectedId: string): boolean {
return selectedId.length === 0 || ids.includes(selectedId);
}
validSelection() 在内容更新或恢复后可作为兜底:如果原卡片被删除,不要继续向不存在的 NavDestination 传参。若组件已经打开了该详情,应展示“内容已不可用”并让用户返回,而不是自动选中另一个同位置卡片。无障碍检查时,这个错误分支也必须有可读取的标题和返回按钮;不能只显示红色警告图标。
三、用一张矩阵检查所有真正影响体验的交叉条件
兼容性不能只按手机、平板、电脑划分。相同设备也可能使用放大字体、不同输入方式和不同窗口宽度。建立一个二维矩阵:横向为窄窗、中窗、宽窗,纵向为触控、键盘、朗读、字体放大;每个交叉点检查卡片可达性、朗读内容、选中与焦点分离、返回是否可预测、图片失败时是否还有文字。最后再将系统版本、SDK版本和测试设备型号记录在相应案例旁边。没有执行的设备组合应保持待测,不能因为一种布局通过就替另一种标记通过。
具体操作顺序可以统一为:从首页找到“页面导航”卡片并打开,返回列表;再切换到“状态管理”,拉宽窗口观察详情是否只更新到当前选中项;启用朗读检查标题和动作;接入键盘使用 Tab 与确认键执行同一流程;放大系统字体后检查内容是否被遮挡。A 系列验证业务写入,而 B 系列验证内容能否被顺利感知和到达,两者的验收性质不同。测试记录应保留失败路径、屏幕条件和实际语音反馈文字,方便设计与研发复核。
还有两类容易遗漏的错误。第一类是重复播报:父卡片有标签,内部的装饰性图标也获得焦点,用户就必须多次滑动才能跳出一个卡片。第二类是焦点丢失:详情关闭时 card_k03 已不存在,强行恢复会失败;此时应回退到列表的合理起点,不能把焦点锁在不可见节点。对于只有鼠标点击才正常、键盘无法激活的自定义布局,应审查 focusable 和点击目标的语义;盲目添加更多动画并不能修复输入通道缺失。
四、六篇工作台的设计结论
从 B01 的断点与信息密度,到 B02 的列表详情、B03 的窗口状态、B04 的焦点返回栈、B05 的组件与图片资源,再到本篇的无障碍适配,贯穿始终的核心不是某个设备,而是同一张知识卡片在不同条件下仍然具有稳定身份。id 负责身份,selectedId 代表阅读目标,focusedId 代表当前输入位置,NavPathStack 保留导航动作;这些值不应互相替代。若一项优化只在某个宽度减少了组件更新,却导致放大字体下无法阅读,不能视为完成设计评审。
最终交付建议将失败案例和兼容矩阵同代码一起管理:每次新增卡片类型,检查其是否有文字替代;每次调整断点,检查聚焦路径;每次改造图片资源,检查朗读标签和占位文本。长期看,比固定三种设备截图更可靠的是一套可以反复操作的多模式验收步骤。知识卡片工作台至此形成从布局、导航、状态、焦点、性能到可访问性的完整工程链。
参考资料:华为开发者—屏幕朗读适配、Navigation 子页面、ArkTS状态管理、Accessibility Kit。
更多推荐


所有评论(0)