状态播报与走焦顺序:两个进阶细节决定体验上限

基础标注做完之后,体验的差距往往体现在两个细节上:一个是"可切换状态的控件怎么读"(选中/未选中、开/关),另一个是"焦点按什么顺序走"。这两件事处理好了,用户的感知是"这个应用很顺手";处理不好,就是"能用但别扭"。这篇把这两个进阶点合在一起讲。

一、状态型控件:系统默认读法不够用时怎么办

很多控件有"可切换状态"——收藏按钮有"已收藏/未收藏",开关有"开/关",复选框有"选中/未选中"。

屏幕朗读对这类控件有默认处理:它会从控件的语义属性推导出状态说明标签。比如一个 Toggle 开关,系统能自动读"开"或"关";一个设置了 accessibilitySelected 的复选框,系统能读"已选中"或"未选中"。

大部分情况下,这套默认推导够用了。但有些场景,默认的状态说明标签和业务语义对不上。这时候就需要 accessibilityStateDescription 来覆盖。

1.1 accessibilityStateDescription 是什么

accessibilityStateDescription 允许你自定义控件状态的朗读标签。官方文档的说明是:

屏幕朗读模式下,若指定了可点击控件的状态说明标签,当用户聚焦控件或执行双击操作后,屏幕朗读会播报指定的状态说明标签。

也就是说,这个属性定的文本,会在聚焦和双击时被播报,替代系统默认的状态描述。

1.2 代码示例:收藏按钮

一个经典的收藏按钮例子:

@Entry
@Component
export struct FavoriteButton {
  @State private isSelected: boolean = false;

  build() {
    NavDestination() {
      Column() {
        Button() {
          Image(this.isSelected ? $r('app.media.favorIcon') : $r('app.media.unfavorIcon'))
            .width(30)
            .height(30)
        }
        .accessibilityStateDescription(this.isSelected ? '已收藏' : '未收藏')
        .accessibilitySelected(this.isSelected)
        .onClick(() => {
          this.isSelected = !this.isSelected;
        })
      }
    }
  }
}

这里三个无障碍属性配合:

  • accessibilitySelected():告诉系统这个按钮的选中状态(这也会影响系统默认推导)
  • accessibilityStateDescription():自定义状态朗读标签,用"已收藏/未收藏"替代系统可能推导出的"已选中/未选中"
  • 状态文本跟随 isSelected 动态变化

为什么"已收藏"比"已选中"好?因为"已选中"是通用的、抽象的,用户听到"已选中"还得结合上下文想"选中了什么状态";而"已收藏"直接说清了业务含义。用业务语言替代泛化语言,是自定义状态描述的核心价值。

二、走焦顺序:焦点该怎么走由你定

2.1 默认走焦顺序的局限

API 18 起,屏幕朗读支持单指左右滑动切换焦点。默认的走焦顺序是"按节点树顺序"——也就是组件在代码里的声明顺序、层级结构决定的遍历顺序。

问题在于,节点树顺序不等于视觉顺序。视觉上从左到右、从上到下排列的控件,在节点树里可能因为布局嵌套而顺序错乱。用户按直觉左滑,焦点却跳到了一个视觉上在别处的位置,这就很别扭。

2.2 accessibilityNextFocusId 自定义走焦

当默认顺序不满足需求时,用 accessibilityNextFocusId 指定"下一个焦点的 id"。

官方建议的走焦方向很明确:按照画面显示的自上而下、自左向右的方向设置。

参数说明:

属性说明
nextId下一个焦点组件的 id
nextFocusParams扩展参数(API 26 起)

2.3 代码示例

下面的例子里,视觉顺序是 A→B→C→D→E,但通过自定义,把走焦顺序改成了 A→C→D→E(跳过了 B):

@Entry
@Component
export struct NextFocusIdDemo {
  build() {
    NavDestination() {
      Column({ space: 10 }) {
        Button('按钮A, Next为column')
          .fontSize(20).width('80%')
          .accessibilityNextFocusId('next_column')  // A 的下一个焦点指向 column

        Button('按钮B')
          .fontSize(20).width('80%')  // B 没有任何 customId 指向它,被跳过

        Column() {
          Button('按钮C')
          Button('按钮D')
        }
        .id('next_column')  // A 指向的容器

        Button('按钮E')
          .id('btn_e')
          .fontSize(20).width('80%')
      }
    }
  }
}

关键机制:

  • Button A 通过 accessibilityNextFocusId('next_column') 指向下个焦点
  • Button B 因为没有任何控件的 nextFocusId 指向它,在走焦链路中被跳过
  • 目标要设 .id(),和 nextId 对应

2.4 API 26 的新参数

API 26.0.0 起,accessibilityNextFocusId 新增了 nextFocusParams 参数,可以通过 isConsiderDescendants 控制焦点搜索时是否考虑目标元素后代中的可聚焦节点

这个参数在目标是一个复杂容器(内部还有可聚焦子元素)时有用。默认 false 表示只把容器本身当焦点;设为 true 则允许焦点深入到容器的后代节点。

三、走焦顺序设计的几个原则

3.1 遵循视觉顺序

焦点顺序应该和用户的视觉扫描顺序一致——通常就是自上而下、自左向右。这是官方的首要建议,也是用户在滑动时的心智模型。

3.2 避免焦点跳跃

走焦路径应该平滑、连续,不要从一个控件突然跳到视觉上很远的地方。空间上的突兀跳跃会让用户失去方位感。

3.3 不要遗漏关键控件

被跳过的控件要慎重。上面例子里跳过 B 是为了演示,但实际业务里,跳过意味着用户在走焦序列里永远访问不到它。除非 B 确实不可交互或不需要被朗读,否则不要随意让它"掉出"走焦链路。

四、两个细节的关系

状态播报和走焦顺序看起来互不相关,本质都是在解决同一个问题:让用户的听觉体验和视觉体验对齐。

  • 状态播报:让"听觉上的状态"对齐"视觉上的状态"
  • 走焦顺序:让"听觉上的位置"对齐"视觉上的位置"

一个管"信息对不对",一个管"顺序顺不顺"。两者叠加,决定了一个无障碍应用的上限。

五、总结一下下

这篇的两个主题,可以各用一句话收尾:

  1. 状态播报:系统默认读法不够贴业务时,用 accessibilityStateDescription 覆盖,用业务语言替代泛化语言
  2. 走焦顺序:默认按节点树走焦不满足视觉顺序时,用 accessibilityNextFocusId 重新定义,遵循自上而下、自左向右
Logo

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

更多推荐