HarmonyOS Next ArkTS 无障碍属性完全指南:从零到精通打造读屏友好应用

摘要:本文系统讲解 HarmonyOS (API 23+)中 ArkUI 无障碍属性的核心概念、使用方法和最佳实践。通过丰富的代码示例和对比分析,帮助开发者掌握 accessibilityTextaccessibilityDescriptionaccessibilityGroupaccessibilityRoleaccessibilityState 等关键属性的实战技巧,构建对视障用户真正友好的应用。


效果

一、为什么需要无障碍开发?

根据世界卫生组织数据,全球约有 2.85 亿 视障人群。在 HarmonyOS 生态中,屏幕朗读(TalkBack) 是视障用户使用手机的核心辅助工具。

然而,很多开发者对无障碍的认知停留在"加个 accessibilityText"的阶段。实际上,HarmonyOS 提供了一套完整的无障碍属性体系,正确使用可以大幅提升应用的可用性。

无障碍开发的核心目标:

  • 让屏幕朗读器能准确理解每个 UI 元素的语义
  • 让用户能高效导航整个应用界面
  • 让交互操作有清晰的反馈机制

二、无障碍属性全景图

在 ArkTS 中,无障碍属性分为以下几大类:

属性分类 核心属性 作用
内容描述 accessibilityText 读屏器朗读的核心文本
操作提示 accessibilityDescription 告知用户如何操作
角色类型 accessibilityRole 告诉读屏器这是什么类型的控件
状态信息 accessibilityState 描述控件的当前状态
节点分组 accessibilityGroup 将多个子节点合并为一个焦点单元
层级控制 accessibilityLevel 控制节点是否参与无障碍遍历

三、accessibilityText:读屏的第一道声音

3.1 基本用法

accessibilityText 是最基础也是最常用的无障碍属性,它为 UI 元素提供文本描述,屏幕朗读器会朗读这段文本。

// 基础文本描述
Image($r('app.media.avatar'))
  .accessibilityText('用户头像')

Button('提交订单')
  .accessibilityText('提交订单')

3.2 核心原则:不重复已有信息

最常见的错误是在 accessibilityText 中重复系统已知的信息(如角色、状态):

// ❌ 错误示范:重复了 Button 的角色信息
Button('提交')
  .accessibilityText('按钮 提交表单')
  // 读屏器实际朗读:"按钮 按钮 提交表单 按钮"

// ✅ 正确写法:只提供语义内容
Button('提交')
  .accessibilityText('提交表单')
  // 读屏器朗读:"提交表单 按钮"

3.3 图片的无障碍描述

图片是最容易遗漏无障碍描述的元素:

// 装饰性图片 - 不需要读屏
Image($r('app.media.divider_line'))
  .accessibilityLevel('no-hide-descendants')

// 信息性图片 - 需要详细描述
Image($r('app.media.qr_code'))
  .accessibilityText('商家收款二维码,可双击放大查看')
  .accessibilityRole(AccessibilityRoleType.IMAGE)

// 图标按钮 - 描述功能而非外观
Image($r('app.media.icon_search'))
  .accessibilityText('搜索')  // ✅ 描述功能
  // .accessibilityText('放大镜图标')  // ❌ 描述外观无意义

3.4 敏感信息的无障碍处理

对于手机号、银行卡号等敏感信息,应使用中文数字替代阿拉伯数字:

// 展示文本:13912345678
// 无障碍文本:一三九 一二三四 五六七八
Text('13912345678')
  .accessibilityText('一三九 一二三四 五六七八')

// 银行卡号后四位
Text('**** **** **** 6789')
  .accessibilityText('卡号后四位 六七八九')

四、accessibilityDescription:操作提示的艺术

accessibilityDescription 为可交互元素提供操作提示。当设置此属性时,它会替换系统默认的"单指双击即可执行"提示。

4.1 基本用法

Button('删除')
  .accessibilityText('删除笔记')
  .accessibilityDescription('双击删除当前笔记,此操作不可撤销')
  .accessibilityRole(AccessibilityRoleType.BUTTON)

4.2 复杂交互的提示

// 滑动操作
Row() {
  Text('消息列表项')
}
.accessibilityText('来自张三的消息')
.accessibilityDescription('双击打开消息,左滑删除,右滑标记已读')

// 长按操作
Column() {
  Text('图片')
}
.accessibilityText('拍摄于2025年12月的风景照片')
.accessibilityDescription('双击查看大图,长按可分享或保存')

最佳实践:操作提示应该简洁、准确,告知用户核心交互方式即可,避免冗长的说明。


五、accessibilityRole:让读屏器理解控件类型

5.1 角色类型概览

AccessibilityRoleType 枚举提供了丰富的控件类型:

// 交互控件
AccessibilityRoleType.BUTTON         // 按钮
AccessibilityRoleType.SWITCH         // 开关
AccessibilityRoleType.CHECK_BOX      // 复选框
AccessibilityRoleType.RADIO_BUTTON   // 单选按钮
AccessibilityRoleType.SLIDER         // 滑块
AccessibilityRoleType.TEXT_FIELD     // 文本输入框

// 信息展示
AccessibilityRoleType.IMAGE          // 图片
AccessibilityRoleType.TEXT           // 文本
AccessibilityRoleType.HEADING        // 标题(导航地标)
AccessibilityRoleType.LIST           // 列表容器
AccessibilityRoleType.LIST_ITEM      // 列表项

5.2 正确使用角色

// 标题元素 - 读屏器支持快速跳转到标题
Text('个人信息')
  .fontSize(24)
  .fontWeight(FontWeight.Bold)
  .accessibilityRole(AccessibilityRoleType.HEADING)

// 开关控件
Toggle({ type: ToggleType.Switch, isOn: this.isDarkMode })
  .accessibilityText('深色模式')
  .accessibilityRole(AccessibilityRoleType.SWITCH)
  .accessibilityState({ isOn: this.isDarkMode })

// 自定义按钮
Column() {
  Image($r('app.media.icon'))
  Text('拍照')
}
.accessibilityRole(AccessibilityRoleType.BUTTON)
.accessibilityText('拍照')

5.3 为什么角色很重要?

角色类型不仅影响朗读内容,还影响读屏器的交互模式

  • 设为 BUTTON:读屏器会提示"双击执行"
  • 设为 SWITCH:读屏器会提示"双击切换"
  • 设为 HEADING:用户可以通过"标题导航"快速跳转

六、accessibilityState:状态信息的精确传达

6.1 checked vs selected 的区别

这是最容易混淆的两个状态属性:

属性 适用场景 状态切换朗读
checked 复选框(多选) 每次切换都朗读
selected 单选按钮(单选) 仅首次聚焦朗读
// 复选框 - 使用 checked
Checkbox()
  .accessibilityText('记住密码')
  .accessibilityRole(AccessibilityRoleType.CHECK_BOX)
  .accessibilityState({
    checked: this.rememberPassword,
    checkedChange: (checked: boolean) => {
      this.rememberPassword = checked
    }
  })
// 勾选时朗读:"已选中"
// 取消时朗读:"未选中"

// 单选按钮 - 使用 selected
Radio({ value: 'male', group: 'gender' })
  .accessibilityText('男')
  .accessibilityRole(AccessibilityRoleType.RADIO_BUTTON)
  .accessibilityState({
    selected: this.gender === 'male',
    selectedChange: (selected: boolean) => {
      if (selected) this.gender = 'male'
    }
  })

6.2 开关与滑块状态

// Switch 开关
Toggle({ type: ToggleType.Switch, isOn: this.wifiEnabled })
  .accessibilityText('Wi-Fi')
  .accessibilityRole(AccessibilityRoleType.SWITCH)
  .accessibilityState({
    isOn: this.wifiEnabled,
    selectedChange: (selected: boolean) => {
      this.wifiEnabled = selected
    }
  })

// Slider 滑块
Slider({ value: this.volume, min: 0, max: 100 })
  .accessibilityText('媒体音量')
  .accessibilityRole(AccessibilityRoleType.SLIDER)
  .accessibilityState({
    currentValue: this.volume,
    minValue: 0,
    maxValue: 100
  })

七、accessibilityGroup:焦点合并的智慧

7.1 问题:碎片化焦点

没有分组的复合 UI 元素,每个子元素都会单独获得焦点,导致读屏体验碎片化:

// ❌ 没有分组:每个元素单独聚焦,读屏器朗读4次
Row() {
  Image($r('app.media.avatar'))           // 焦点1:朗读"头像"
  Text('李华')                             // 焦点2:朗读"李华"
  Text('产品经理')                          // 焦点3:朗读"产品经理"
  Text('4.8分')                            // 焦点4:朗读"4.8分"
}

7.2 解决方案:使用 accessibilityGroup

// ✅ 使用分组:合并为一个焦点,读屏器朗读1次
Row() {
  Image($r('app.media.avatar'))
  Text('李华').fontSize(18).fontWeight(FontWeight.Bold)
  Text('产品经理').fontColor('#666666')
  Text('4.8 ★').fontColor('#FFD740')
}
.accessibilityGroup(true)
.accessibilityText('李华,产品经理,评分4.8星')
.accessibilityRole(AccessibilityRoleType.LIST_ITEM)

7.3 使用原则

场景 是否分组 说明
联系人卡片 ✅ 是 多个信息构成一个整体
列表项 ✅ 是 图标+标题+描述是一体的
表单按钮组 ❌ 否 每个按钮需要独立操作
导航栏图标 ❌ 否 每个图标是独立功能入口

八、accessibilityLevel:控制无障碍遍历层级

8.1 四种层级模式

// 'auto' - 系统自动决定(默认值)
Text('普通文本')
  .accessibilityLevel('auto')

// 'yes' - 强制参与遍历
Text('重要公告')
  .accessibilityLevel('yes')

// 'no' - 自身不参与,但子元素仍参与
Column() {
  Text('装饰性标题框')    // 不会被朗读
    .accessibilityLevel('no')
  Text('核心内容')        // 仍然会被朗读
    .accessibilityLevel('yes')
}

// 'no-hide-descendants' - 自身和所有子元素都不参与
Column() {
  Image($r('app.media.decoration'))
  Text('v2.0.1')
}
.accessibilityLevel('no-hide-descendants')  // 整个区域对读屏器不可见

8.2 实际应用:装饰性元素

// 背景装饰图 - 不需要读屏
Image($r('app.media.bg_pattern'))
  .accessibilityLevel('no-hide-descendants')

// 版本号 - 对普通用户有意义,对读屏用户干扰
Text('Version 3.2.1')
  .accessibilityLevel('no')

// 分隔线 - 纯装饰
Divider()
  .accessibilityLevel('no-hide-descendants')

九、主动播报:sendAccessibilityEvent

9.1 场景说明

有些信息变化不会触发焦点变化(如 Toast 提示、加载状态),此时需要主动播报让用户知晓:

import { accessibility } from '@kit.AccessibilityKit'

// 操作成功提示
submitOrder(): void {
  // 提交订单逻辑...
  accessibility.sendAccessibilityEvent(
    this.getContext(),
    'announceForAccessibility',
    { text: '订单提交成功,预计3个工作日内发货' }
  )
}

// 加载进度通知
loadData(): void {
  accessibility.sendAccessibilityEvent(
    this.getContext(),
    'announceForAccessibility',
    { text: '正在加载数据,请稍候' }
  )
}

9.2 请求焦点转移

// 表单验证失败时,将焦点移到出错的输入框
validateForm(): void {
  if (!this.isValidEmail) {
    accessibility.sendAccessibilityEvent(
      this.getContext(),
      'announceForAccessibility',
      { text: '邮箱格式不正确,请重新输入' }
    )
    accessibility.sendAccessibilityEvent(
      this.getContext(),
      'requestFocusForAccessibility',
      { targetId: 'emailInput' }
    )
  }
}

十、完整示例:无障碍表单页面

下面是一个完整的无障碍友好型表单页面示例:

@ComponentV2
struct AccessibleFormPage {
  @Local username: string = ''
  @Local password: string = ''
  @Local rememberMe: boolean = false
  @Local showPassword: boolean = false

  build() {
    Column() {
      // 标题区域
      Text('用户登录')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .accessibilityText('用户登录')
        .accessibilityRole(AccessibilityRoleType.HEADING)
        .margin({ top: 40, bottom: 30 })

      // 用户名输入框
      TextInput({ placeholder: '请输入用户名', text: this.username })
        .id('usernameInput')
        .accessibilityText('用户名输入框')
        .accessibilityDescription('输入您的用户名用于登录')
        .accessibilityRole(AccessibilityRoleType.TEXT_FIELD)
        .onChange((value: string) => { this.username = value })

      // 密码输入框
      TextInput({
        placeholder: '请输入密码',
        text: this.password
      })
        .id('passwordInput')
        .type(this.showPassword ? InputType.Normal : InputType.Password)
        .accessibilityText('密码输入框')
        .accessibilityDescription('输入您的登录密码')
        .accessibilityRole(AccessibilityRoleType.TEXT_FIELD)
        .onChange((value: string) => { this.password = value })

      // 显示密码开关
      Row() {
        Toggle({ type: ToggleType.Switch, isOn: this.showPassword })
          .accessibilityText('显示密码')
          .accessibilityRole(AccessibilityRoleType.SWITCH)
          .accessibilityState({
            isOn: this.showPassword,
            selectedChange: (selected: boolean) => {
              this.showPassword = selected
            }
          })
        Text('显示密码')
          .margin({ left: 8 })
      }
      .accessibilityGroup(true)

      // 记住密码复选框
      Row() {
        Checkbox()
          .accessibilityText('记住密码')
          .accessibilityRole(AccessibilityRoleType.CHECK_BOX)
          .accessibilityState({
            checked: this.rememberMe,
            checkedChange: (checked: boolean) => {
              this.rememberMe = checked
            }
          })
        Text('记住密码')
          .margin({ left: 8 })
      }
      .accessibilityGroup(true)

      // 登录按钮
      Button('登录')
        .accessibilityText('登录')
        .accessibilityDescription('双击提交登录信息')
        .accessibilityRole(AccessibilityRoleType.BUTTON)
        .onClick(() => {
          // 登录逻辑
        })
    }
    .padding(24)
    .width('100%')
  }
}

十一、无障碍测试清单

开发完成后,请按以下清单逐项检查:

基础检查

  • 所有可交互元素都有 accessibilityText
  • accessibilityText 没有重复角色/状态信息
  • 装饰性图片已设置 accessibilityLevel('no-hide-descendants')
  • 所有图片类型的功能按钮有功能描述

交互检查

  • 所有按钮设置了正确的 accessibilityRole
  • 复选框使用 checked 状态
  • 单选按钮使用 selected 状态
  • 开关控件使用 isOn 状态

结构检查

  • 复合卡片使用了 accessibilityGroup(true)
  • 列表容器标记了 AccessibilityRoleType.LIST
  • 标题元素使用 accessibilityText 提供清晰的区域描述

反馈检查

  • 操作结果通过 sendAccessibilityEvent 主动播报
  • 表单验证错误信息能被读屏器获取
  • 加载状态有语音提示

十二、实战踩坑总结

在实际项目开发中,以下问题经常遇到,请务必注意:

12.1 accessibilityState 的组件兼容性

重要提醒accessibilityState 属性并非所有组件都支持

组件类型 是否支持 accessibilityState 说明
Checkbox / Toggle ✅ 支持 内置状态管理
Radio ✅ 支持 内置选中状态
Slider ✅ 支持 内置范围状态
Text ❌ 不支持 编译报错
Column / Row ❌ 不支持 编译报错
Image ❌ 不支持 编译报错

替代方案:对于不支持 accessibilityState 的组件,将状态信息合并到 accessibilityText 中:

// ❌ Text 不支持 accessibilityState
Text(this.isFavorite ? '★' : '☆')
  .accessibilityState({ checked: this.isFavorite })  // 编译报错!

// ✅ 将状态信息融入 accessibilityText
Text(this.isFavorite ? '★' : '☆')
  .accessibilityText(`收藏按钮,${this.isFavorite ? '已收藏' : '未收藏'},双击切换`)
  .accessibilityRole(AccessibilityRoleType.BUTTON)

12.2 AccessibilityRoleType 枚举值的实际可用性

官方文档列出了大量枚举值,但并非所有值在当前 API 版本中都可用

枚举值 实际可用性 替代方案
BUTTON ✅ 可用
TEXT ✅ 可用
TEXT_FIELD ✅ 可用
LIST / LIST_ITEM ✅ 可用
IMAGE ✅ 可用
CHECK_BOX ✅ 可用
SWITCH ✅ 可用
HEADING ❌ 不存在 使用 TEXT + 字体样式区分
SECTION ❌ 不存在 使用 TEXT + accessibilityGroup
SUMMARY ❌ 不存在 使用 TEXT + accessibilityGroup

建议:只使用确认存在的枚举值,用 accessibilityText 补充语义信息。

12.3 @ComponentV2getContext() 不可用

@ComponentV2 组件没有 getContext() 方法,应使用 getUIContext()

// ❌ @ComponentV2 中不存在
@ComponentV2
struct MyPage {
  build() {
    Button('测试')
      .onClick(() => {
        let ctx = this.getContext()  // 编译报错!
      })
  }
}

// ✅ 使用 getUIContext()
@ComponentV2
struct MyPage {
  build() {
    Button('测试')
      .onClick(() => {
        let ctx = this.getUIContext()  // 正确
      })
  }
}

十三、总结

无障碍开发不是"锦上添花",而是每个开发者的责任。HarmonyOS ArkTS 提供的无障碍属性体系功能完善、使用简便:

  1. accessibilityText —— 提供核心语义,不重复系统已知信息
  2. accessibilityDescription —— 补充操作提示,引导用户交互
  3. accessibilityRole —— 标识控件类型,影响朗读和交互模式
  4. accessibilityState —— 精确传达状态,区分 checked/selected/isOn
  5. accessibilityGroup —— 合并焦点单元,提升导航效率
  6. accessibilityLevel —— 控制遍历层级,排除装饰性干扰
  7. sendAccessibilityEvent —— 主动播报动态变化

掌握这些属性并养成使用习惯,同时记住实战中的注意事项,你的应用将惠及更多用户。

参考文档

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐