在移动应用开发中,无障碍功能常常被忽视,但它却是连接不同用户群体的重要桥梁。想象一下,视障用户通过屏幕朗读功能使用我们的应用时,能否清晰理解界面元素?今天咱们就来聊聊 HarmonyOS 中那些让应用更具包容性的无障碍属性,从基础设置到高级技巧,带大家一步步打造友好的无障碍交互体验。

一、无障碍分组:让组件成为一个整体

有时候我们需要让一组组件在无障碍服务中被当作一个整体处理,比如一个卡片内的多个元素,这时候就需要用到accessibilityGroup属性。

@Entry
@Component
struct AccessibilityGroupExample {
  build() {
    Column {
      Image($r("app.media.icon"))
        .width(80)
        .height(80)
      Text('天气卡片')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
      Text('25℃ | 多云')
        .fontSize(16)
    }
    .width(200)
    .height(150)
    .padding(15)
    .backgroundColor(Color.White)
    .borderRadius(10)
    .shadow({ radius: 5, color: '#33000000' })
    .accessibilityGroup(true) // 启用无障碍分组
    .accessibilityText('天气信息卡片') // 设置分组的无障碍文本
  }
}

重点总结:

  1. 分组逻辑accessibilityGroup(true)会将组件及其子组件视为一个整体,无障碍服务不再关注子组件内容。默认不启用分组,子组件会被单独识别。
  2. 文本拼接规则:如果分组组件没有无障碍文本且不含文本属性,会自动拼接子组件的通用文本(深度优先)。比如上面的天气卡片,若不设置accessibilityText,会拼接 "天气卡片" 和 "25℃ | 多云" 作为朗读内容。
  3. API 版本注意:从 API version 10 开始支持,API version 14 后新增accessibilityOptions参数,可设置accessibilityPreferred: true来优先拼接子组件的无障碍文本。

二、无障碍文本:给组件加上 "语音标签"

对于没有文本属性的组件,比如图片、图标,我们需要通过accessibilityText告诉屏幕朗读该播报什么内容。

@Entry
@Component
struct AccessibilityTextExample {
  @State isPlaying: boolean = false;

  build() {
    Button(isPlaying ? '暂停播放' : '开始播放')
      .width(120)
      .height(48)
      .backgroundColor(isPlaying ? Color.Gray : Color.Blue)
      .borderRadius(24)
      .accessibilityLevel('yes') // 确保组件可被无障碍服务识别
      .accessibilityText(isPlaying ? '暂停音频播放按钮' : '开始音频播放按钮') // 动态更新无障碍文本
      .onClick(() => {
        this.isPlaying = !this.isPlaying;
      })
  }
}

重点总结:

  1. 核心作用:当组件没有文本属性时,accessibilityText是屏幕朗读的关键信息源。如果组件同时有文本和无障碍文本,优先播报无障碍文本。
  2. 动态更新:如示例所示,无障碍文本可以根据组件状态动态变化,确保朗读内容始终与界面状态一致。
  3. 资源引用:从 API version 12 开始支持accessibilityText(Resource),可以引用字符串资源文件,方便多语言适配。

三、无障碍说明:补充组件的详细解释

有时候光有文本还不够,比如一个危险操作按钮,需要额外说明后果,这时候accessibilityDescription就派上用场了。

@Entry
@Component
struct AccessibilityDescriptionExample {
  build() {
    Button('删除账户')
      .width(160)
      .height(50)
      .backgroundColor(Color.Red)
      .fontColor(Color.White)
      .borderRadius(25)
      .accessibilityLevel('yes')
      .accessibilityText('删除账户按钮')
      .accessibilityDescription('点击后将永久删除您的账户及所有数据,此操作不可恢复') // 详细说明操作后果
  }
}

重点总结:

  1. 信息层级:当组件同时有文本、无障碍文本和说明时,播报顺序是:文本 → 无障碍文本 → 说明。示例中会先播报 "删除账户按钮",再播报说明内容。
  2. 场景应用:主要用于解释操作的潜在后果,尤其是那些无法从组件外观直接判断的风险操作。
  3. 资源引用支持:同accessibilityText,从 API version 12 开始支持引用资源文件。

四、无障碍重要性:控制组件的可识别性

有些组件不需要被无障碍服务识别,比如装饰性元素,这时候可以用accessibilityLevel来控制。

@Entry
@Component
struct AccessibilityLevelExample {
  build() {
    Column {
      // 重要按钮,必须可识别
      Button('提交订单')
        .accessibilityLevel('yes')
        .accessibilityText('提交订单按钮,确认购买')
      
      // 装饰性图标,无需识别
      Image($r("app.media.decorative_icon"))
        .width(30)
        .height(30)
        .accessibilityLevel('no') // 设置为不可识别
      
      // 分组内的子组件,随分组状态变化
      Text('订单编号:123456')
        .accessibilityLevel('auto') // 由系统自动判断
    }
    .width('100%')
    .padding(20)
  }
}

重点总结:

  1. 取值说明
    • yes:必须被识别
    • no:不可识别
    • no-hide-descendants:当前组件及所有子组件都不可识别
    • auto:由系统根据分组等条件判断(默认值)
  2. 继承规则:如果父组件设置为no-hide-descendants,子组件无论怎么设置都会被屏蔽。
  3. 分组影响:当父组件启用无障碍分组(accessibilityGroup(true)),子组件的accessibilityLevel设置会被忽略。

五、无障碍虚拟节点:为自绘制组件提供信息

对于自定义绘制的组件(如 Canvas),无障碍服务无法自动获取内容,这时候需要用accessibilityVirtualNode手动提供节点信息。

@Entry
@Component
struct AccessibilityVirtualNodeExample {
  @Builder VirtualContent() {
    Column {
      Text('图表标题:月度销售数据')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
      Text('横轴:月份,纵轴:销售额(万元)')
        .fontSize(14)
        .fontColor(Color.Gray)
    }
    .width(200)
    .padding(10)
  }

  build() {
    Canvas()
      .width('100%')
      .height(200)
      .onDraw((context) => {
        // 绘制图表的逻辑
        context.fillColor('#F0F0F0');
        context.fillRect(0, 0, this.width, this.height);
        // ... 省略具体绘制代码
      })
      .accessibilityLevel('yes')
      .accessibilityVirtualNode(this.VirtualContent) // 提供虚拟节点信息
  }
}

重点总结:

  1. 适用场景:自绘制组件(如 Canvas、自定义图形)必须通过accessibilityVirtualNode提供可朗读内容,否则无障碍服务无法获取信息。
  2. 节点规则:虚拟节点中的组件仅用于无障碍信息获取,不会实际显示在界面上,但需要保证结构清晰。
  3. 数据同步:如果自绘制内容动态变化,虚拟节点的内容也需要相应更新,确保朗读信息准确。

六、选中状态管理:支持单选和多选场景

在列表、设置页等场景中,需要明确组件的选中状态,HarmonyOS 提供了accessibilityChecked(多选)和accessibilitySelected(单选)两个属性。

@Entry
@Component
struct SelectionStateExample {
  @State selectedItems: Set<number> = new Set([1, 3]); // 多选状态
  @State currentTab: number = 0; // 单选状态

  build() {
    Column {
      // 多选框组
      Text('喜欢的水果:')
        .fontSize(16)
        .marginBottom(10)
      
      Row {
        Checkbox('苹果')
          .isChecked(this.selectedItems.has(1))
          .onChange((isChecked) => {
            if (isChecked) this.selectedItems.add(1);
            else this.selectedItems.delete(1);
          })
          .accessibilityLevel('yes')
          .accessibilityChecked(this.selectedItems.has(1)) // 同步多选状态
          
        Checkbox('香蕉')
          .isChecked(this.selectedItems.has(2))
          .onChange((isChecked) => {
            if (isChecked) this.selectedItems.add(2);
            else this.selectedItems.delete(2);
          })
          .accessibilityLevel('yes')
          .accessibilityChecked(this.selectedItems.has(2))
      }
      .width('100%')
      .marginBottom(30)
      
      // 单选标签页
      Row {
        Text('标签页1')
          .fontSize(16)
          .padding(10)
          .backgroundColor(this.currentTab === 0 ? Color.Blue : Color.Gray)
          .fontColor(Color.White)
          .accessibilityLevel('yes')
          .accessibilitySelected(this.currentTab === 0) // 同步单选状态
          .onClick(() => this.currentTab = 0)
          
        Text('标签页2')
          .fontSize(16)
          .padding(10)
          .backgroundColor(this.currentTab === 1 ? Color.Blue : Color.Gray)
          .fontColor(Color.White)
          .accessibilityLevel('yes')
          .accessibilitySelected(this.currentTab === 1)
          .onClick(() => this.currentTab = 1)
      }
      .width('100%')
    }
    .width('100%')
    .padding(20)
  }
}

重点总结:

  1. 模式区别
    • accessibilityChecked:用于多选场景,可同时选中多个组件
    • accessibilitySelected:用于单选场景,同一组内只能有一个选中
  2. 状态同步:必须与组件的实际选中状态保持一致,否则会导致无障碍服务识别错误。
  3. 冲突处理:不能同时使用这两个属性,会造成状态冲突。如果需要切换模式,必须先将另一个属性设为undefined

七、组件角色定义:自定义屏幕朗读方式

有时候系统默认的朗读方式不符合需求,比如一个自定义的滑块组件,我们可以用accessibilityRole指定其角色类型。

@Entry
@Component
struct AccessibilityRoleExample {
  @State volume: number = 50;

  @Builder VolumeSlider() {
    Row {
      Text('音量调节')
        .fontSize(14)
      Text(`${this.volume}%`)
        .fontSize(14)
        .marginStart(10)
    }
    .width('100%')
  }

  build() {
    Column {
      // 自定义滑块组件,指定为滑块角色
      Stack {
        Rectangle()
          .width('90%')
          .height(8)
          .backgroundColor(Color.Gray)
          .cornerRadius(4)
        
        Rectangle()
          .width(`${this.volume * 0.9}%`)
          .height(8)
          .backgroundColor(Color.Blue)
          .cornerRadius(4)
        
        Circle()
          .width(20)
          .height(20)
          .backgroundColor(Color.White)
          .border({ width: 2, color: Color.Blue })
          .marginStart(`${this.volume * 0.9 - 10}%`)
          .shadow({ radius: 5, color: '#33000000' })
      }
      .width('100%')
      .height(40)
      .accessibilityLevel('yes')
      .accessibilityRole(AccessibilityRoleType.SLIDER) // 指定为滑块角色
      .accessibilityText(`音量滑块,当前${this.volume}%`)
      .onClick(() => {
        // 模拟滑块交互
        if (this.volume < 100) this.volume += 10;
        else this.volume = 0;
      })
      
      // 自定义按钮,指定为开关角色
      Toggle({ type: ToggleType.Switch, isOn: this.volume > 0 })
        .onChange((isOn) => {
          this.volume = isOn ? 50 : 0;
        })
        .accessibilityLevel('yes')
        .accessibilityRole(AccessibilityRoleType.SWITCH) // 指定为开关角色
        .accessibilityText(isOn ? '音量已开启' : '音量已关闭')
    }
    .width('100%')
    .padding(20)
  }
}

重点总结:

  1. 角色类型AccessibilityRoleType枚举提供了 124 种角色类型,包括按钮、滑块、开关、图表等,覆盖大部分常用组件。
  2. 朗读优化:指定角色后,屏幕朗读会使用该角色的标准朗读方式,比如滑块会播报 "音量滑块,当前 50%,左右滑动调整"。
  3. 自定义角色:如果枚举中没有合适的类型,可以使用ROLE_NONE,但不建议,尽量选择最接近的标准角色。

八、焦点控制:管理无障碍走焦顺序

在复杂界面中,合理的焦点顺序能让视障用户更顺畅地导航,accessibilityNextFocusIdaccessibilityDefaultFocus可以帮助我们精确控制焦点。

@Entry
@Component
struct FocusControlExample {
  build() {
    Column {
      Text('用户名:')
        .fontSize(16)
        .marginBottom(5)
      
      TextInput({ placeholder: '请输入用户名' })
        .id('username')
        .width('80%')
        .accessibilityLevel('yes')
        .accessibilityNextFocusId('password') // 下一个焦点是密码输入框
      
      Text('密码:')
        .fontSize(16)
        .margin({ top: 20, bottom: 5 })
      
      TextInput({ placeholder: '请输入密码' })
        .id('password')
        .width('80%')
        .accessibilityLevel('yes')
        .accessibilityNextFocusId('remember') // 下一个焦点是记住密码
      
      Row {
        Checkbox('记住密码')
          .id('remember')
          .accessibilityLevel('yes')
          .accessibilityNextFocusId('login') // 下一个焦点是登录按钮
          
        Button('登录')
          .id('login')
          .width(100)
          .accessibilityLevel('yes')
          .accessibilityDefaultFocus(true) // 设置为默认首焦点
      }
      .width('100%')
      .marginTop(20)
    }
    .width('100%')
    .padding(20)
  }
}

重点总结:

  1. 默认首焦点accessibilityDefaultFocus(true)设置组件为页面加载时的默认焦点,通常用于第一个可操作元素。
  2. 走焦顺序accessibilityNextFocusId('组件id')指定按下焦点键后的下一个焦点组件,id 必须唯一且存在。
  3. 跨进程场景:对于嵌入式组件(如EmbeddedComponent),可能需要配合accessibilityUseSamePage解决跳焦问题,避免焦点在进程间丢失。

九、滚动控制与焦点样式:细节优化提升体验

最后,还有一些细节属性可以进一步优化无障碍体验,比如滚动触发和焦点框样式。

@Entry
@Component
struct ScrollAndFocusExample {
  @State listData: string[] = Array(20).fill('').map((_, i) => `列表项 ${i + 1}`);

  build() {
    List {
      ForEach(this.listData, (item, index) => {
        ListItem {
          Text(item)
            .fontSize(16)
            .padding(15)
            .accessibilityLevel('yes')
        }
        .accessibilityScrollTriggerable(true) // 支持滚动触发
      })
    }
    .width('100%')
    .height(300)
    .accessibilityScrollTriggerable(false) // 整个列表不自动滚动,由子项控制
    
    // 焦点框绘制在顶层,避免被遮挡
    Text('重要按钮')
      .width(120)
      .height(48)
      .backgroundColor(Color.Blue)
      .fontColor(Color.White)
      .borderRadius(24)
      .accessibilityLevel('yes')
      .accessibilityFocusDrawLevel(FocusDrawLevel.TOP) // 焦点框绘制在顶层
  }
}

重点总结:

  1. 滚动触发accessibilityScrollTriggerable(true)表示当焦点切换到容器但当前页面无可聚焦组件时,自动滚动到下一个可聚焦组件。建议在 List、Grid 等滚动组件上设置。
  2. 焦点框层级accessibilityFocusDrawLevel(FocusDrawLevel.TOP)将焦点绿框绘制在 Z 序顶层,避免被父组件或兄弟组件遮挡,提升可见性。
  3. 跨进程场景accessibilityUseSamePage用于解决嵌入式组件的跳焦问题,根据场景选择SEMI_SILENTFULL_SILENT模式忽略部分 page 事件。

十、总结:构建包容性应用的核心要点

通过上面的讲解,相信大家对 HarmonyOS 的无障碍属性有了全面的认识。咱们再梳理一下开发时的核心要点:

  1. 基础三要素accessibilityText(必填文本)、accessibilityDescription(补充说明)、accessibilityLevel(可识别性)是最基础的无障碍属性,任何组件都应优先配置。
  2. 分组与聚焦:合理使用accessibilityGroup整合相关组件,通过accessibilityNextFocusIdaccessibilityDefaultFocus优化焦点导航路径。
  3. 状态与角色accessibilityChecked/Selected同步选中状态,accessibilityRole指定组件类型,让屏幕朗读更准确。
  4. 自绘与滚动:自绘制组件必须通过accessibilityVirtualNode提供信息,滚动容器合理设置accessibilityScrollTriggerable
  5. 细节优化accessibilityFocusDrawLevel确保焦点框可见,accessibilityUseSamePage解决跨进程跳焦问题。

无障碍开发不是额外的负担,而是让应用惠及更多用户的重要途径。从现在开始,在项目中逐步加入这些属性,不仅能提升应用的包容性,还能满足更多场景的开发需求。记得根据 API 版本适配不同功能,参考官方文档确认参数支持情况,让我们一起打造更友好的 HarmonyOS 应用体验!

Logo

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

更多推荐