
系列社会责任篇·第32篇。前几篇讲了性能和数据今天聊点不一样的。有视障开发者留言“鸿蒙系统本身的TalkBack很好用但你们的电商Demo读屏时只念‘按钮’我根本不知道是‘加入购物车’还是‘收藏’。能不能讲讲如何让应用更无障碍” 这让我意识到技术不仅有商业价值更有社会价值。今天我们将电商Demo进行无障碍Accessibility改造遵循WCAG 2.1 AA标准让视障用户能通过听觉和触觉顺利购物。我们将解决组件语义缺失、焦点逻辑混乱、动态内容播报三大无障碍痛点。全程基于API23含官方文档未提及的“无障碍最佳实践清单”。一、前言为什么无障碍不是“可选项”在很多开发者眼里无障碍Accessibility是“锦上添花”甚至是负担。但在鸿蒙生态中这是必选项法规要求《信息技术 互联网内容无障碍可访问性技术要求》等法规明确要求App应具备无障碍功能。用户基数中国有1700多万视障人士他们是潜在的巨大用户群。系统评分华为应用市场对无障碍的支持程度有明确的检测和评分直接影响搜索排名和上架审核。鸿蒙优势鸿蒙的无障碍套件Accessibility Kit提供了比Android更统一的API且系统级的屏幕阅读器TalkBack体验极佳。核心原则无障碍不是给盲人专门开发一个版本而是让同一套UI通过不同的感官听觉、触觉被感知。我们要做的是给UI组件“贴标签”和“指路”。二、核心概念辨析无障碍三要素要让视障用户顺畅使用必须理解以下三个核心概念语义Semantics组件是什么是按钮、图片还是文本框焦点Focus用户当前在哪里焦点框在哪里如何移动播报Announcement组件当前的状态是什么已选中、已展开、加载中错误做法正确做法用Image组件画按钮不设置无障碍标签用Button组件或给Image设置accessibilityLabel焦点顺序混乱从上到下跳到左边焦点顺序符合视觉流左-右上-下动态加载商品后不通知用户使用accessibilityAnnouncer主动播报“加载了10件商品”三、代码实现电商Demo的无障碍改造3.1 基础语义标注给组件“上户口”视障用户依赖读屏软件如TalkBack了解界面。如果组件没有语义读屏软件就不知道该说什么。修改GoodsItem.etsComponent struct GoodsItem { Prop goods: GoodsBean Prop isFavorite: boolean false build() { Row() { // 商品图片必须设置无障碍标签和角色 Image(this.goods.image) .width(100) .height(100) .borderRadius(8) .accessibilityGroup(true) // 标记为无障碍节点 .accessibilityLabel(商品图片${this.goods.name}) // 关键描述图片内容 .accessibilityRole(AccessibilityRole.IMAGE) // 声明角色为图片 Column() { // 商品名称默认Text组件会被朗读但需确保不被截断 Text(this.goods.name) .fontSize(16) .maxLines(2) .accessibilityGroup(true) .accessibilityLiveRegion(AccessibilityLiveRegion.POLITE) // 内容变化时礼貌播报 // 商品价格 Text(¥${this.goods.price}) .fontSize(18) .fontColor(#FF0000) .accessibilityLabel(价格${this.goods.price}元) // 收藏按钮状态很重要 Button() .width(40) .height(40) .backgroundColor(this.isFavorite ? #FFD700 : #EEEEEE) .onClick(() this.toggleFavorite()) .accessibilityGroup(true) .accessibilityLabel(this.isFavorite ? 取消收藏 : 收藏) // 根据状态变化 .accessibilityRole(AccessibilityRole.BUTTON) .accessibilityState({ selected: this.isFavorite }) // 标记选中状态 } .layoutWeight(1) .margin({ left: 12 }) // 加入购物车按钮 Button(加购) .height(40) .backgroundColor(#0A59F7) .onClick(() this.addToCart()) .accessibilityGroup(true) .accessibilityLabel(加入购物车) .accessibilityRole(AccessibilityRole.BUTTON) .accessibilityHint(双击将商品加入购物车) // 操作提示 } .padding(16) .borderRadius(12) .backgroundColor(#FFFFFF) // 给整个商品卡片设置一个大的无障碍标签 .accessibilityGroup(true) .accessibilityLabel(商品${this.goods.name}价格${this.goods.price}元。双击查看详情) .accessibilityRole(AccessibilityRole.BUTTON) // 让整个卡片可点击 } toggleFavorite(): void { /* ... */ } addToCart(): void { /* ... */ } }3.2 焦点导航优化给手指“指路”默认的焦点导航是按组件树顺序走的但有时我们需要自定义顺序或者处理一些不可聚焦但重要的信息如“包邮”标签。修改ProductDetailPage.etsEntry Component struct ProductDetailPage { State description: string 这是一款... private scroller: Scroller new Scroller() build() { Column() { // 顶部导航栏焦点顺序1 Row() { Button(返回) .accessibilityLabel(返回上一页) .accessibilityOrder(1) // 显式指定焦点顺序 Text(商品详情) .fontSize(20) .layoutWeight(1) .textAlign(TextAlign.Center) .accessibilityOrder(2) } .padding(16) Scroll(this.scroller) { Column() { // 商品主图焦点顺序3 Image($r(app.media.big_image)) .width(100%) .height(300) .accessibilityLabel(商品主图) .accessibilityOrder(3) // 价格区域焦点顺序4 Row() { Text(¥5999) .fontSize(24) .fontColor(#FF0000) Text(包邮) .fontSize(12) .backgroundColor(#FFF3E0) .padding(4) .borderRadius(4) .margin({ left: 8 }) // 关键让“包邮”这个视觉信息被读屏软件捕获 // 虽然它没有背景交互但信息很重要 .accessibilityGroup(true) .accessibilityLabel(包邮) .accessibilityRole(AccessibilityRole.TEXT) } .margin(16) .accessibilityOrder(4) // 商品描述焦点顺序5 Text(this.description) .fontSize(14) .margin(16) .accessibilityGroup(true) .accessibilityLabel(商品描述${this.description}) .accessibilityOrder(5) // 长文本支持允许读屏软件分页朗读 .accessibilityLongClickEnabled(true) // 底部操作栏焦点顺序6-8 Row() { Button(客服) .accessibilityOrder(6) .accessibilityHint(联系在线客服) Button(收藏) .accessibilityOrder(7) .accessibilityHint(收藏此商品) Button(立即购买) .backgroundColor(#0A59F7) .accessibilityOrder(8) .accessibilityHint(立即购买此商品) } .padding(16) .backgroundColor(#FFFFFF) } } } .width(100%) .height(100%) // 开启无障碍滚动支持当焦点移动到屏幕外时自动滚动 .accessibilityScrollable(true, this.scroller) } }3.3 动态内容播报给耳朵“报信”当用户点击“加入购物车”后商品数量变化了或者网络加载开始了视障用户需要知道这些动态变化。我们不能指望他们去盯着屏幕看。创建entry/src/main/ets/common/AccessibilityHelper.etsimport { accessibility } from kit.AccessibilityKit /** * 无障碍辅助工具类 */ export class AccessibilityHelper { /** * 主动播报消息类似Toast但专为无障碍设计 * param text 要播报的文本 */ static announce(text: string): void { try { // 使用无障碍公告接口 accessibility.announce({ text: text, queueMode: accessibility.QueueMode.QUEUE_MODE_FLUSH // 打断当前播报立即播报 }) } catch (err) { console.error(无障碍播报失败:, err) } } /** * 模拟点击无障碍节点用于自动化测试或引导 * param elementId 组件的ID */ static performClick(elementId: string): void { const controller accessibility.getAccessibilityController() const node controller.getNodeById(elementId) if (node) { node.performAction(accessibility.Action.CLICK) } } } // 在ViewModel或UI中使用 // 当用户点击加购时 async function addToCart(): Promisevoid { await CartAPI.add() // 关键主动播报结果 AccessibilityHelper.announce(已加入购物车当前共3件商品) // 更新UI... }四、踩坑记录官方文档没写的无障碍细节accessibilityGroup(true)的滥用如果一个Column设置了accessibilityGroup(true)它内部的子组件就不再单独接受焦点而是作为一个整体。这在商品列表中很有用整行作为一个焦点但在表单中会造成灾难用户无法单独操作每个输入框。ImageView的alt文本网络图片加载失败时读屏软件会读文件名如0x123.png毫无意义。必须设置accessibilityLabel且在加载失败时更新标签为“图片加载失败”。自定义组件的焦点自定义组件如Component struct MyButton默认是不可聚焦的。必须在根容器上设置accessibilityGroup(true)和accessibilityRole。accessibilityHint的必要性Label告诉用户“是什么”Hint告诉用户“干什么”。例如一个箭头图标Label是“返回箭头”Hint应该是“双击返回上一页”。测试工具不要只用眼看要闭上眼睛用TalkBack操作一遍自己的App。你会发现很多逻辑问题比如焦点被困在某个区域出不来或者关键信息无法访问。