Administrator
发布于 2026-09-03 / 8,657 阅读
0
0

鸿蒙无障碍开发:Accessibility 服务、语义树与辅助功能适配

系列文章《鸿蒙开发从入门到专家》第 59 篇 · 高级

无障碍开发是技术文章里最少见的主题之一。不是因为不重要,而是因为没踩过坑的人觉得"不就是加个 accessibilityText 吗"。实际项目里,无障碍适配的坑比你想的多:语义树怎么组织、焦点怎么管理、动态内容怎么通知屏幕阅读器。

鸿蒙从 API 10 开始提供无障碍属性,API 12 之后支持卡片和元服务。这篇讲怎么在 ArkUI 里做无障碍适配。

一、为什么需要无障碍

先看一组数据。中国有超过 1700 万视障人群,他们使用屏幕阅读器(鸿蒙上叫"随选朗读")操作手机。如果应用不做无障碍适配,视障用户面对的是一堆"未知控件"、“未知按钮”,根本无法使用。

政府项目和部分行业的应用上架要求必须通过无障碍检测。不做适配,上架都过不了。

二、核心 API

鸿蒙的无障碍 API 是组件的通用属性,通过链式调用设置:

属性

作用

默认值

accessibilityText(value: string)

屏幕阅读器朗读的文本

空(使用组件文本)

accessibilityDescription(value: string)

额外描述

空

accessibilityLevel(value: string)

是否可被聚焦

“auto”

accessibilityGroup(value: boolean)

是否组合为一个焦点

false

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)
    }
  }
}

每个商品卡片分组为一个焦点,播报完整的商品信息。用户不需要逐个子组件滑动。

五、无障碍适配清单

场景

适配方式

纯图标按钮

加 accessibilityText

图片内容

加 accessibilityText 描述图片内容

装饰性元素

accessibilityLevel(‘no’)

多元素卡片

accessibilityGroup(true) + 汇总文本

操作有后果

accessibilityDescription 提示后果

动态列表更新

sendAccessibilityEvent 通知

对话框

打开时手动设置焦点

表单输入

accessibilityText 描述输入要求

加载中/空状态

accessibilityText 描述当前状态

颜色传达信息

补充文本(不只靠颜色区分)

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


评论