在这里插入图片描述

你是不是也在想——“鸿蒙这么火,我能不能学会?”
答案是:当然可以!
这个专栏专为零基础小白设计,不需要编程基础,也不需要懂原理、背术语。我们会用最通俗易懂的语言、最贴近生活的案例,手把手带你从安装开发工具开始,一步步学会开发自己的鸿蒙应用。
不管你是学生、上班族、打算转行,还是单纯对技术感兴趣,只要你愿意花一点时间,就能在这里搞懂鸿蒙开发,并做出属于自己的App!
📌 关注本专栏《零基础学鸿蒙开发》,一起变强!
每一节内容我都会持续更新,配图+代码+解释全都有,欢迎点个关注,不走丢,我是小白酷爱学习,我们一起上路 🚀

前言

一个只有图标、没有文字的操作按钮,对视觉用户来说可能很直观:心形代表收藏,垃圾桶代表删除,分享箭头代表分享。但切到屏幕朗读后,如果组件只暴露了“按钮”这个类型,而没有可朗读的名称,用户得到的信息就可能只剩下“按钮”。

这类问题并不需要重做页面。ArkUI 已提供无障碍文本、无障碍说明、无障碍分组、无障碍重要性等通用属性;对于选择状态,还可以优先使用 Checkbox、Toggle 等本身具有状态语义的组件。HarmonyOS 7 对应 API 26.0.0,本文以 HarmonyOS 7 / API 26 为基线,做一次从图标按钮到表单区域的完整页面无障碍补全。华为官方也已经提供“提升屏幕朗读无障碍体验”专题,覆盖按钮标注、多维信息整体朗读、控件状态变化、自定义控件播报状态等场景。

一、为什么只有一个图标时,读屏很难知道它是什么意思

先看一个常见写法:

Button({ type: ButtonType.Circle }) {
  Image($r('app.media.ic_favorite'))
    .width(24)
    .height(24)
}
.onClick(() => {
  // 收藏操作
})

这里视觉上已经是一个完整按钮,但 Image 本身没有“收藏”这段文字。官方对无障碍文本的定义很明确:当组件没有可供屏幕朗读的文本信息时,可以通过 accessibilityText 提供无障碍文本;如果组件本身已有文本,设置的无障碍文本会作为朗读内容使用。accessibilityDescription 则用于进一步解释操作及可能产生的后果。HarmonyOS 7 的 ArkUI API 变更资料中仍可以确认 CommonMethod 提供 accessibilityGroup(value: boolean)、accessibilityText(value: string)、accessibilityDescription(value: string)、accessibilityLevel(value: string) 等接口。

所以这个问题的关键并不是“给图标起一个文件名”,而是让真正承担交互的组件向无障碍服务暴露完整语义。

可以改成:

Button({ type: ButtonType.Circle }) {
  Image($r('app.media.ic_favorite'))
    .width(24)
    .height(24)
    .accessibilityLevel('no')
}
.width(48)
.height(48)
.accessibilityText('收藏')
.accessibilityDescription('将当前内容加入收藏')
.onClick(() => {
  // 收藏操作
})

这里真正需要关注三件事。

第一,accessibilityText('收藏') 回答的是“这是什么”。

第二,accessibilityDescription('将当前内容加入收藏') 回答的是“执行后会发生什么”。

第三,图标只是按钮内部的视觉内容,因此示例通过 accessibilityLevel('no') 避免它再成为一个没有独立交互价值的朗读目标。无障碍重要性支持 "auto"、"yes"、"no" 和 "no-hide-descendants" 等取值,其中 "no" 表示当前组件不被无障碍辅助服务识别,"no-hide-descendants" 则会同时忽略当前组件及全部子组件。

这里也能看出一个设计原则:**不要把 Image 本身加上 onClick 就直接当按钮使用。**如果交互语义本来就是按钮,优先使用 Button 承载点击行为,再把 Image 放进 Button。官方 Button 指南明确支持 Button 作为容器添加图片、文字等子组件。

二、无障碍文本和无障碍说明不是一回事

accessibilityText 和 accessibilityDescription 很容易被写成两句差不多的话:

.accessibilityText('删除')
.accessibilityDescription('删除按钮')

这种配置虽然有信息,但第二句没有增加多少价值。

更合理的思路是:

.accessibilityText('删除')
.accessibilityDescription('删除当前草稿')

对于风险更高的操作,还可以把后果写清楚:

.accessibilityText('删除')
.accessibilityDescription('删除当前草稿,操作前会再次确认')

官方文档对二者的职责划分也是如此:无障碍文本负责提供组件本身缺少的文字信息,无障碍说明用于补充仅靠组件属性和无障碍文本无法得知的操作后果;存在说明时,朗读会在组件文本之后继续播报说明。

因此,不建议把所有按钮都机械补成“XX按钮,点击XX”。真正需要补的是用户无法从名称本身判断的信息。

三、accessibility group:一条业务信息不要拆成五次朗读

页面中的另一个典型问题,是一张信息卡片由多个 Text 拼成:

订单
待支付
68 元
今天 18:00 前支付

如果这些信息属于同一个不可独立操作的业务单元,让读屏逐个停留,用户需要多次滑动才能知道这张卡片表达了什么。

这种场景可以考虑无障碍分组:

Row() {
  Column({ space: 4 }) {
    Text('订单')
    Text('待支付')
    Text('68 元')
    Text('今天 18:00 前支付')
  }
  .alignItems(HorizontalAlign.Start)
}
.width('100%')
.padding(16)
.accessibilityGroup(true)
.accessibilityText('订单,待支付,金额68元,今天18点前支付')

accessibilityGroup(true) 的含义是把当前组件和子组件作为一个整体的可选中组件,无障碍服务不再分别关注它的子组件内容。

这很好用,但也很容易用错。

如果卡片里面还有“支付”“取消”两个真正需要独立操作的按钮,就不应该简单把整张卡片全部 group 掉。否则子组件不再作为独立的无障碍目标,反而可能损失操作入口。

所以分组的判断标准不是“它们视觉上在一个 Row 里”,而是:这些子节点在无障碍交互中是否应该被当成一个整体。

四、选中和禁用状态,不能只靠颜色表达

视觉页面经常用颜色区分状态:蓝色表示选中,灰色表示禁用,实心心形表示已收藏。

问题在于,颜色和图标变化并不等于无障碍语义发生了变化。

对于真正属于“选择”的交互,优先考虑使用具有对应状态模型的标准组件。例如 Checkbox 从 API version 8 开始提供,select(value: boolean) 用来设置选中状态,onChange 返回状态变化结果。HarmonyOS 当前文档同时明确提供 enabled 控制组件是否可交互。

例如:

@State private receiveNotice: boolean = false;

Checkbox({ name: 'receiveNotice' })
  .select(this.receiveNotice)
  .accessibilityText('接收更新通知')
  .onChange((value: boolean) => {
    this.receiveNotice = value;
  })

如果业务就是一个勾选项,不建议为了视觉效果自己画一个 Image,再维护一份 @State,最后还要额外想办法把状态暴露给无障碍系统。

禁用状态同理。不要只是把按钮改成灰色:

Button('提交')
  .enabled(this.canSubmit)

官方对 enabled(false) 的行为说明是组件进入不可交互状态,不再响应点击、触摸、拖拽、按键、焦点和鼠标事件,同时 UI 状态也会变化。

对于自定义 Native ArkUI 节点,官方还提供了 ArkUI_AccessibilityState,用于描述选中、勾选、禁用等无障碍状态,该结构从 API version 12 开始提供。本文的 ArkTS 页面没有必要为了普通 Checkbox/Button 再下沉到 Native API,但这个接口也说明了一个核心原则:状态是无障碍信息的一部分,而不仅仅是视觉样式。

具体的屏幕朗读措辞可能随系统版本、组件和辅助服务而不同,因此实际项目仍应在目标 HarmonyOS 版本和目标设备上逐项验证,而不是根据界面颜色推断读屏结果。

五、图片按钮应该把朗读信息放在哪里

再看一个更完整的图片按钮:

Button({ type: ButtonType.Circle }) {
  Image($r('app.media.ic_share'))
    .width(24)
    .height(24)
    .accessibilityLevel('no')
}
.width(48)
.height(48)
.accessibilityText('分享')
.accessibilityDescription('打开分享面板')
.onClick(() => {
  // 打开分享面板
})

这里把无障碍文本设置在 Button 上,而不是设置在内部 Image 上。

原因很直接:真正可以执行操作的是 Button。Image 只是它的视觉表现。

如果把 Image 和 Button 都做成可识别节点,就可能出现两个焦点;如果只给 Image 写“分享”而 Button 本身仍没有名称,组件树中的交互语义又会变得割裂。

ArkUI 官方针对无障碍场景还提供了代码检查规则,要求自定义交互组件通过 accessibilityRole 声明组件类型,例如按钮、编辑框。对于本文这种标准 Button 场景,可以直接使用标准组件;如果业务必须用 Column、Row 等组件自己实现交互,则还需要额外关注角色语义,而不是只有一个 onClick 就结束。

六、表单输入区不要只检查“能不能输入”

一个页面做无障碍检查时,输入框经常被漏掉,因为 TextInput 本身已经能够获得焦点、输入文字,看起来没有问题。

但读屏用户还需要知道三个信息:这是哪个字段、现在是什么状态、填写它有什么要求。

最小表单可以这样组织:

@State private email: string = '';

Column({ space: 8 }) {
  Text('邮箱地址')
    .fontSize(14)

  TextInput({
    text: this.email,
    placeholder: 'name@example.com'
  })
    .width('100%')
    .accessibilityDescription('用于接收更新通知')
    .onChange((value: string) => {
      this.email = value;
    })

  Button('提交')
    .enabled(this.email.length > 0)
    .accessibilityDescription(
      this.email.length > 0
        ? '提交当前表单'
        : '请先填写邮箱地址'
    )
}
.alignItems(HorizontalAlign.Start)

这里特意保留了可见的“邮箱地址”标签,而不是只依赖 placeholder。placeholder 是输入提示,并不应该承担页面上全部的字段说明。

另一个需要谨慎的地方是 accessibilityText:官方定义指出,当组件本身已有文本信息时,无障碍文本可以替代原有文本进行播报。 对编辑组件来说,如果随意用固定的 accessibilityText('邮箱地址') 覆盖内容,就需要特别检查输入后的实际朗读结果。因此表单不要套用“所有组件都加 accessibilityText”的机械规则,而应该以最终读屏顺序和实际播报内容为准。

七、把这些点放进一个最小页面

下面代码把图标按钮、信息分组、Checkbox 状态、输入框和禁用按钮放在同一页。代码按照 HarmonyOS 7 / API 26 当前官方接口组织;其中 ic_favorite、ic_share 是示例应用自己的图片资源名,需要在工程 resources/base/media 中准备对应资源。

@Entry
@Component
struct AccessibilityDemo {
  @State private receiveNotice: boolean = false;
  @State private email: string = '';

  build() {
    Scroll() {
      Column({ space: 20 }) {
        Text('文章详情')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)

        Row({ space: 12 }) {
          Button({ type: ButtonType.Circle }) {
            Image($r('app.media.ic_favorite'))
              .width(24)
              .height(24)
              .accessibilityLevel('no')
          }
          .width(48)
          .height(48)
          .accessibilityText('收藏')
          .accessibilityDescription('将当前文章加入收藏')
          .onClick(() => {
            // 收藏业务
          })

          Button({ type: ButtonType.Circle }) {
            Image($r('app.media.ic_share'))
              .width(24)
              .height(24)
              .accessibilityLevel('no')
          }
          .width(48)
          .height(48)
          .accessibilityText('分享')
          .accessibilityDescription('打开分享面板')
          .onClick(() => {
            // 分享业务
          })
        }

        Row() {
          Column({ space: 4 }) {
            Text('阅读进度')
            Text('第 3 章')
            Text('约 8 分钟')
          }
          .alignItems(HorizontalAlign.Start)
        }
        .width('100%')
        .padding(16)
        .accessibilityGroup(true)
        .accessibilityText('阅读进度,第3章,约8分钟')

        Row({ space: 8 }) {
          Checkbox({ name: 'receiveNotice' })
            .select(this.receiveNotice)
            .accessibilityText('接收更新通知')
            .onChange((value: boolean) => {
              this.receiveNotice = value;
            })

          Text('接收更新通知')
        }
        .width('100%')

        Column({ space: 8 }) {
          Text('邮箱地址')
            .fontSize(14)

          TextInput({
            text: this.email,
            placeholder: 'name@example.com'
          })
            .width('100%')
            .accessibilityDescription('用于接收更新通知')
            .onChange((value: string) => {
              this.email = value;
            })
        }
        .alignItems(HorizontalAlign.Start)
        .width('100%')

        Button('提交')
          .width('100%')
          .enabled(this.email.length > 0)
          .accessibilityDescription(
            this.email.length > 0
              ? '提交当前表单'
              : '请先填写邮箱地址'
          )
      }
      .padding(20)
      .width('100%')
      .alignItems(HorizontalAlign.Start)
    }
    .width('100%')
    .height('100%')
  }
}

这个示例没有调用 Accessibility Kit 的状态查询或主动播报接口,也没有新增 module.json5 权限。这里解决的是 ArkUI 组件自身的无障碍语义问题,主要依赖 ArkUI 通用属性和标准组件。Accessibility Kit 更偏向无障碍状态查询、无障碍事件发送等能力;ArkUI 则负责组件层面的无障碍文本、说明等信息。

八、做一次完整页面读屏检查

代码写完并不代表无障碍检查结束。这个场景最有效的排查方式,是把页面当成一个完全不看的用户,从第一个焦点一直滑到最后一个焦点。

建议按下面的顺序检查:

  1. 先查无名控件。 所有纯图标按钮、图片操作入口、自定义点击区域,确认读屏能回答“这是什么”。
  2. 再查重复焦点。 Button 内部的装饰 Image、卡片里的重复文字有没有被额外聚焦;需要整体表达的信息是否适合 accessibilityGroup(true)。
  3. 再查角色和状态。 按钮是不是按钮,选择项是否使用了合适的标准选择组件,禁用是否真的调用了 enabled(false),而不是只改变透明度或颜色。
  4. 检查操作后果。 删除、提交、跳转、打开弹窗等操作,如果名称本身不足以解释后果,再补 accessibilityDescription。
  5. 检查表单。 输入框是否能理解字段用途,错误、必填、禁用等状态是否仅存在于视觉样式中。
  6. 最后完整走焦。 从页面顶部连续左右滑动,观察是否存在焦点遗漏、重复、顺序不自然或必须“猜图标”的地方。

华为官方的“提升屏幕朗读无障碍体验”指南本身就把按钮标注、多维信息整体朗读、多 UI 控件组合、禁用屏幕朗读焦点、控件状态变化、内容动态变化和自定义控件走焦顺序等列为独立场景。换句话说,无障碍检查不能只盯着一个 accessibilityText 属性,而应该检查整棵交互树。

容易理解错的几个地方

accessibilityGroup(true) 不是“优化朗读”的万能开关。它会让无障碍服务不再分别关注子组件,因此包含独立按钮的区域不能随意整体分组。

accessibilityLevel('no') 和 'no-hide-descendants' 也不是一回事。前者针对当前组件,后者会把当前组件以及子组件一起从无障碍识别范围中排除。

禁用按钮不要只设置 .opacity(0.4)。视觉变灰不会自动等价于交互禁用;ArkUI 的 enabled(false) 才会使组件进入不可交互状态。

有状态的控件尽量不要全部从 Row + Image + onClick 开始手写。Checkbox 已有明确的 select 状态接口;标准组件能够表达业务语义时,优先让组件模型承担状态,再考虑视觉定制。

还有一点很重要:本文代码依据官方接口组织,但没有在具体 HarmonyOS 工程中替你完成编译和真机读屏验证,因此不应把“接口存在”直接等同于“目标设备上的每一句朗读措辞已经验证”。发布前仍应使用项目实际的 API 配置、目标设备和屏幕朗读服务完整走一遍页面。

开发经验总结

做 ArkUI 页面无障碍补全,可以把问题压缩成四个词:名称、角色、状态、关系。

图标按钮缺的是名称,就补 accessibilityText;操作结果不明确,再补 accessibilityDescription。多个文本共同表达一条业务信息时,再判断是否应该使用 accessibilityGroup。装饰性子节点不应该制造额外焦点,可以通过无障碍重要性控制。对于勾选、开关、禁用等状态,优先使用能够真实表达状态的标准组件和 enabled、select 等接口,而不是只改变颜色。

HarmonyOS 7 对应 API 26.0.0,本文使用的 ArkUI 通用无障碍属性在 API 26 当前接口中仍然存在;Checkbox 等标准组件也继续提供明确的状态接口。

真正值得在项目里形成习惯的,不是“看到 Image 就加 accessibilityText”,而是每做完一个页面,都关掉视觉上的先验信息,从第一个无障碍焦点走到最后一个:它是谁、现在是什么状态、能做什么、操作之后会怎样。只要其中一个问题需要用户猜,这个页面就还有继续补充语义的空间。

❤️ 如果本文帮到了你…

  • 请点个赞,让我知道你还在坚持阅读技术长文!
  • 请收藏本文,因为你以后一定还会用上!
  • 如果你在学习过程中遇到bug,请留言,我帮你踩坑!
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐