系列文章《鸿蒙开发从入门到专家》第 59 篇 · 高级
无障碍开发是技术文章里最少见的主题之一。不是因为不重要,而是因为没踩过坑的人觉得"不就是加个 accessibilityText 吗"。实际项目里,无障碍适配的坑比你想的多:语义树怎么组织、焦点怎么管理、动态内容怎么通知屏幕阅读器。
鸿蒙从 API 10 开始提供无障碍属性,API 12 之后支持卡片和元服务。这篇讲怎么在 ArkUI 里做无障碍适配。
一、为什么需要无障碍
先看一组数据。中国有超过 1700 万视障人群,他们使用屏幕阅读器(鸿蒙上叫"随选朗读")操作手机。如果应用不做无障碍适配,视障用户面对的是一堆"未知控件"、“未知按钮”,根本无法使用。
政府项目和部分行业的应用上架要求必须通过无障碍检测。不做适配,上架都过不了。
二、核心 API
鸿蒙的无障碍 API 是组件的通用属性,通过链式调用设置:
2.1 accessibilityText
为没有文字内容的组件提供语音标签。最典型的场景是图标按钮:
// 没有无障碍文本:屏幕阅读器播报"未知"
Image($r('app.media.ic_back'))
.width(24)
.height(24)
.onClick(() => this.router.back())
// 有无障碍文本:屏幕阅读器播报"返回"
Image($r('app.media.ic_back'))
.width(24)
.height(24)
.accessibilityText('返回')
.onClick(() => this.router.back())
注意:如果组件本身有文本(比如 Text('返回')),屏幕阅读器会自动播报文本内容,不需要额外设置 accessibilityText。如果同时设了 accessibilityText,只播报 accessibilityText,组件文本会被忽略。
2.2 accessibilityDescription
用于补充说明,在 accessibilityText 之后播报。常见用途:告知操作后果。
Button('删除')
.accessibilityText('删除')
.accessibilityDescription('删除后不可恢复,请确认')
.onClick(() => this.delete())
屏幕阅读器播报:“删除,删除后不可恢复,请确认”。
2.3 accessibilityLevel
控制组件是否可以被屏幕阅读器聚焦。三个值:
"auto":由组件类型决定(按钮、文本可聚焦,纯装饰图片不可聚焦)。"yes":强制可聚焦。"no":强制不可聚焦。
// 装饰性图片:屏幕阅读器忽略
Image($r('app.media.bg_decoration'))
.accessibilityLevel('no')
// 强制聚焦的自定义组件
Column() {
Image($r('app.media.ic_avatar'))
Text('用户名')
}
.accessibilityLevel('yes')
2.4 accessibilityGroup
把多个子组件组合为一个焦点单元。用户滑动一次手指聚焦整个组,而不是逐个子组件聚焦。
// 不分组:用户要滑动 3 次才能听完一条联系人
ListItem() {
Row() {
Text('张三') // 聚焦 1
Text('13800001111') // 聚焦 2
Text('zhangsan@example.com') // 聚焦 3
}
}
// 分组:滑动 1 次听完整条联系人
ListItem() {
Row() {
Text('张三')
Text('13800001111')
Text('zhangsan@example.com')
}
.accessibilityGroup(true)
// 分组后无 accessibilityText 时,系统自动拼接子组件文本(深度优先)
// 播报:"张三,13800001111,zhangsan@example.com"
}
分组时也可以设 accessibilityText 覆盖拼接结果:
Row() {
Text('张三')
Text('13800001111')
}
.accessibilityGroup(true)
.accessibilityText('联系人:张三,电话 13800001111')
三、语义树管理
屏幕阅读器不是看 UI 像素,而是遍历"语义树"。语义树是 UI 组件树的无障碍视图,每个节点包含 accessibilityText、description、level、group 等信息。
3.1 树结构影响播报顺序
// 播报顺序:标题 → 内容 → 按钮
Column() {
Text('设置')
Text('管理你的账户偏好')
Button('进入设置')
}
// 播报顺序会变:按钮 → 标题 → 内容(因为 z-index 不同)
Stack() {
Text('设置').zIndex(1)
Button('进入设置').zIndex(2)
Text('管理你的账户偏好').zIndex(1)
}
语义树的遍历顺序受布局层级和 z-index 影响。如果你发现播报顺序乱了,检查组件树结构。
3.2 动态内容的语义更新
列表项新增或删除时,屏幕阅读器不会自动感知。需要主动发送无障碍事件通知:
import { accessibility } from '@kit.AccessibilityKit';
@Component
struct DynamicListComponent {
@State items: string[] = ['A', 'B', 'C'];
addItem(): void {
this.items.push('D');
// 通知屏幕阅读器内容变化
accessibility.sendAccessibilityEvent({
type: 'contentChange',
text: ['列表已更新'],
bundleName: 'com.example.app'
});
}
}
3.3 焦点管理
某些场景需要手动控制焦点:
// 对话框打开时把焦点移到标题上
@Component
struct DialogComponent {
private titleRef: Text = ...;
showDialog(): void {
this.visible = true;
// 延迟一帧确保组件已渲染
setTimeout(() => {
accessibility.requestFocus(this.titleRef);
}, 100);
}
}
四、实战:完整适配示例
一个购物应用的列表页:
@Entry
@Component
struct ShoppingListPage {
@State products: Product[] = [];
build() {
Column() {
// 顶部导航栏
Row() {
Image($r('app.media.ic_back'))
.width(24)
.height(24)
.accessibilityText('返回上一页')
.accessibilityLevel('yes')
.onClick(() => this.back())
Text('购物车')
.fontSize(18)
Image($r('app.media.ic_search'))
.width(24)
.height(24)
.accessibilityText('搜索商品')
.accessibilityDescription('点击进入搜索页面')
.onClick(() => this.search())
}
.width('100%')
.height(56)
// 商品列表
List() {
LazyForEach(this.dataSource, (product: Product) => {
ListItem() {
Row() {
Image(product.imageUrl)
.width(80)
.height(80)
.accessibilityText(`商品图片:${product.name}`)
Column() {
Text(product.name)
.fontSize(16)
Text(`¥${product.price}`)
.fontSize(14)
.fontColor('#FF0000')
Text(`已售${product.soldCount}件`)
.fontSize(12)
.fontColor('#999999')
}
.layoutWeight(1)
Button('加入购物车')
.fontSize(12)
.accessibilityDescription(`将${product.name}加入购物车`)
}
.padding(12)
.accessibilityGroup(true)
.accessibilityText(
`${product.name},价格${product.price}元,已售${product.soldCount}件`
)
}
}, (product: Product) => product.id)
}
.cachedCount(5)
}
}
}
每个商品卡片分组为一个焦点,播报完整的商品信息。用户不需要逐个子组件滑动。
五、无障碍适配清单
5.1 颜色不传递信息的规则
// 错误:只靠红色表示错误
TextInput({ text: this.value })
.borderColor('#FF0000')
// 正确:颜色+文本双重传达
TextInput({ text: this.value })
.borderColor('#FF0000')
.accessibilityDescription('当前输入有误,请检查格式')
六、测试方法
6.1 开启随选朗读
设置 → 辅助功能 → 随选朗读 → 开启。
开启后,在应用上滑动,屏幕阅读器会朗读聚焦到的组件。检查:
每个可交互元素是否有有意义的播报。
播报顺序是否合理。
是否有"未知控件"播报。
6.2 无障碍检查工具
DevEco Studio 的 Layout Inspector 可以查看语义树。切换到 Accessibility 视图,检查每个节点的无障碍属性。
6.3 自动化检查
在测试中用 Hypium 框架写无障碍断言:
import { describe, it, expect } from '@ohos/hypium';
export default function accessibilityTest() {
describe('accessibilityTest', () => {
it('submit_button_has_accessibility_text', 0, () => {
const button = findByKey('submit_button');
expect(button).not.null();
expect(button.accessibilityText).assertEqual('提交订单');
});
it('list_items_are_grouped', 0, () => {
const items = findAllByKey('product_item');
items.forEach(item => {
expect(item.accessibilityGroup).assertTrue();
});
});
});
}
小结
无障碍适配不是"加几个 accessibilityText"就完事。核心是管理语义树:哪些元素可聚焦、哪些要分组、播报顺序对不对、动态内容有没有通知。适配清单覆盖十个场景,做完基本能通过无障碍检测。对于政府项目和上架审核,这是硬性要求。
下篇是高级实战阶段的回顾篇。前 60 篇的知识图谱和后 40 篇的展望。
参考阅读:
第 44 篇:鸿蒙设计规范(人因工程部分)
第 46 篇:鸿蒙 UI 自动化测试(Hypium 框架)
官方文档:无障碍属性、AccessibilityKit