鸿蒙 Accessibility Kit 无障碍开发全解析

前言

记得那是,我回老家看望爷爷奶奶。奶奶戴着老花镜,费力地在手机上戳来戳去,嘴里念叨着:“这字儿咋这么小?我咋点都点不准…”。爷爷在旁边叹气:“上次想查个收租日期,鼓捣了半天都没找着,最后还是麻烦邻居家小孩帮忙的。”

那一刻,我心里特别难受。我想起我们团队刚开发的"老年宝助手大全"应用,原本是想帮老年人解决生活问题的,可没想到对他们来说还是那么难用。按钮太小、文字模糊、操作复杂,这些在我们年轻人看来不是问题的问题,却成了爷爷奶奶使用手机的巨大障碍。

奶奶拉着我的手说:“孙儿啊,你们年轻人做的这些东西,咋就不能替我们想想呢?我也想和你视频聊天,也想看看天气预报,也想自己管理好家里的事情…”

这席话让我彻夜难眠。我意识到,我们虽然打着"为老年人设计"的旗号,却忽略了最基本的无障碍需求。技术的发展不应该成为老年人的数字鸿沟,而应该成为帮助他们更好生活的工具。

从那天起,我开始深入研究鸿蒙系统的 Accessibility Kit。我邀请了多位老年用户参与测试,观察他们的使用习惯;我参加了老年人数字素养培训班,倾听他们的真实需求;我在项目中不断尝试和优化,甚至为此重构了整个应用的UI架构和交互流程。

现在,三年过去了,我已经成为了团队中的无障碍开发专家。我主导开发的"老年宝助手大全"应用获得了鸿蒙开发者大赛的无障碍开发专项奖,更重要的是,奶奶现在每天都会用这个应用看天气、记事情,爷爷也能自己管理收租和接送孙子了。上次回家,奶奶高兴地跟我说:“现在手机就像我的小帮手,啥都能教我,啥都能帮我做。”

看到爷爷奶奶能用我们的应用轻松生活,我知道我所做的一切都是值得的。今天,我想把这些年积累的经验和技巧分享给大家,希望能帮助更多开发者为老年人设计出真正好用的应用。这不仅是一篇技术指南,更是我对无障碍开发的理解和感悟。我相信,只要我们用心去做,每个人都能开发出真正人人可用的优质应用。

Accessibility Kit 概述

什么是 Accessibility Kit?

在开始具体的开发实践前,让我们先了解一下 Accessibility Kit 究竟是什么。简单来说,它是鸿蒙系统为开发者提供的一套无障碍服务框架,帮助我们的应用更好地适配各种障碍用户的需求。

我第一次接触 Accessibility Kit 时,曾以为它只是为视障用户设计的屏幕朗读功能。但深入了解后才发现,它的能力远不止于此:

  • 无障碍焦点管理:让组件能够被屏幕朗读等辅助功能识别和操作
  • 无障碍朗读文本:为组件设置合适的朗读内容,确保视障用户获取准确信息
  • 无障碍状态查询:了解系统辅助功能的开启状态,以便应用做出相应调整
  • 无障碍事件发送:主动触发无障碍事件,如聚焦到特定组件或播报重要信息

核心价值

在我看来,Accessibility Kit 的价值不仅仅体现在技术层面,更体现在人文关怀上:

  • 缩小数字鸿沟:让不同年龄、不同能力的用户都能平等使用科技产品
  • 提升整体体验:无障碍设计往往也能让普通用户获得更好的使用体验
  • 扩大应用覆盖:一款无障碍友好的应用,能够触达更广泛的用户群体
  • 符合时代要求:随着全球对数字包容的重视,无障碍已成为应用合规的重要部分

系统架构与能力范围

系统架构

Accessibility Kit 的系统架构设计得非常清晰,主要分为三个层次:

  • 系统服务层:这是底层基础,提供屏幕朗读、大字体、高对比度等核心辅助功能
  • 开放能力层:这是我们开发者直接接触的部分,提供各种API接口
  • 应用层:这是我们施展拳脚的地方,通过调用开放能力,实现应用的无障碍适配

能力范围

在实际开发中,我发现以下几项能力最为常用:

  • 无障碍状态查询:了解用户是否开启了屏幕朗读等功能,以便调整应用行为
  • 无障碍事件发送:在关键时刻主动播报信息,提升用户体验
  • 组件无障碍属性设置:为UI元素添加必要的无障碍信息,这是最基础也最重要的工作

开发实战:13个核心场景全解析

在这三年的无障碍开发实践中,我遇到过无数的挑战。从最初连基本的焦点管理都搞不清楚,到现在能够从容应对各种复杂场景,我走过了很多弯路,也积累了很多宝贵的经验。

记得第一次尝试实现卡片自动居中功能时,我整整花了一个星期。我查遍了官方文档,试遍了各种API,甚至还去鸿蒙开发者论坛发了求助帖。最后,当我终于通过 onAccessibilityFocus 回调和 scrollToIndex 方法实现这个功能时,那种成就感至今难忘。

现在,我把这些年遇到的最常见、最核心的13个无障碍开发场景整理出来,每个场景都包含了我的真实开发故事、遇到的问题、解决的思路以及最终的实现代码。我希望通过这些具体的场景,能够帮助你少走一些弯路,更快地掌握 Accessibility Kit 的核心能力。

这些场景涵盖了从基础的文本标注到复杂的焦点管理,从静态界面到动态内容,从简单控件到复杂布局等各个方面。无论你是刚开始接触无障碍开发的新手,还是已经有一定经验的开发者,相信都能从中获得启发。

场景一:标注屏幕朗读内容

屏幕朗读是视障用户使用手机的主要方式,因此确保屏幕朗读能够正确解读我们的应用界面至关重要。

我的经验总结

  • 对于普通文本控件,直接使用显示文本即可,这样视障用户和普通用户获取的信息是一致的
  • 对于有颜色、图标等视觉信息的控件,一定要用无障碍文本补充这些信息
  • 对于纯图标按钮等非文本控件,必须设置无障碍文本,否则视障用户无法知道这个控件的功能

开发实例

@Entry
@Component
export struct AccessibilityTextDemo {
  title: string = '屏幕朗读内容标注示例';
  shortText: string = '提交';
  // 补充了视觉信息:这是一个表单提交按钮
  longText: string = '确认提交表单';

  build() {
    NavDestination() {
      Column() {
        Blank()
        Button(this.shortText)
          // 设置无障碍文本,确保视障用户了解按钮的完整功能
          .accessibilityText(this.longText)
          .align(Alignment.Center)
          .fontSize(20)
        Blank()
      }
      .width('100%')
      .height('100%')
    }
    .title(this.title)
  }
}

场景二:禁用屏幕朗读焦点

在开发过程中,我曾经遇到过一个问题:屏幕朗读会聚焦到一些装饰性的元素上,比如页面分割线、纯装饰性的图标等,这会打断用户的操作流程,让浏览变得非常不顺畅。

我的解决方案

  • 对于纯装饰性的控件,使用 accessibilityLevel("no") 将其设置为不可聚焦
  • 对于包含多个装饰性元素的容器,可以使用 accessibilityLevel("no-hide-descendants") 禁用所有子元素的焦点
  • 合理使用 accessibilityGroup(true),将相关元素组合,减少不必要的焦点

开发实例

@Entry
@Component
export struct AccessibilityLevelDemo {
  title: string = '禁用屏幕朗读焦点示例';
  @State message: string = '主要内容';
  @State decorativeText: string = '装饰性文本';

  build() {
    NavDestination() {
      Column() {
        Row() {
          Text(this.message)
            .fontSize(40)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.Blue)
            .margin({ left: 40 })
        }
        .width('100%')
        .height('50%')
        
        // 这个文本只是装饰性的,不需要获取焦点
        Row() {
          Text(this.decorativeText)
            .fontSize(40)
            .fontWeight(FontWeight.Bold)
            .fontColor(Color.Grey)
            .margin({ left: 40 })
            // 将装饰性文本设置为不可聚焦
            .accessibilityLevel("no")
        }
        .width('100%')
        .height('50%')
      }
      .height('100%')
    }
    .title(this.title)
  }
}

场景三:多维嵌套场景优化

我记得第一次处理嵌套控件的无障碍问题时,遇到了一个很头疼的情况:屏幕朗读会重复播报信息。比如一个显示时间和地点的卡片,用户会听到三次播报:先是整个卡片的信息,然后是时间,最后是地点。这不仅冗余,还会让用户感到困惑。

我的解决思路

  • 将多个相关的控件组合成一个无障碍组,使用 accessibilityGroup(true)
  • 在父控件上统一设置完整的无障碍文本,包含所有子控件的信息
  • 确保子控件不会被单独聚焦,避免信息重复

开发实例

@Entry
@Component
export struct NestedAccessibilityDemo {
  title: string = '多维嵌套场景优化示例';

  build() {
    NavDestination() {
      Column() {
        Text('天气卡片示例:')
          .width('100%')
          .fontSize(14)
          .fontColor(Color.Black)
          .margin({bottom: 12, left: 20})
        
        // 天气卡片:包含时间和地点信息
        Row(){
          Text("08:30") // 时间信息
            .fontSize(32)
            .fontColor(Color.Red)
            .fontWeight(FontWeight.Bold)
            .textAlign(TextAlign.Center)
            .margin({right: 20})

          Text("上海") // 位置信息
            .fontSize(20)
            .fontColor(Color.Green)
            .fontWeight(FontWeight.Bold)
            .textAlign(TextAlign.Center)
        }
        // 统一设置完整的无障碍文本
        .accessibilityText("当前时间 08:30,位置 上海")
        // 设置为无障碍组,避免子控件单独聚焦
        .accessibilityGroup(true)
        .height(50)
        .margin({bottom: 50, left: 20})
      }
      .alignItems(HorizontalAlign.Start)
      .padding(10)
    }
    .title(this.title)
  }
}

场景四:组合场景优化

在开发包含多个元素的复合控件时,比如用户信息卡片、商品卡片等,我发现一个常见的问题:屏幕朗读会逐个聚焦到卡片内的每个元素,让用户很难获得完整的信息。

我的优化方法

  • 将表示同一个对象的多个组件组合成一个无障碍单元
  • 只在父容器上设置一个完整的无障碍文本,包含所有子元素的关键信息
  • 使用 accessibilityGroup(true) 确保整个卡片作为一个整体被聚焦

开发实例

@Entry
@Component
export struct CombinedAccessibilityDemo {
  title: string = '组合场景优化示例';

  build() {
    NavDestination() {
      Column() {
        Text('用户信息卡片:')
          .width('100%')
          .fontSize(14)
          .fontColor(Color.Black)
          .margin({bottom: 12, left: 20})
        
        // 用户信息卡片
        Row() {
          Image($r('app.media.avatar'))
            .width(60)
            .height(60)
            .borderRadius(30)
            .margin({right: 16})
          
          Column() {
            Text('张三')
              .fontSize(18)
              .fontWeight(FontWeight.Bold)
            Text('高级工程师')
              .fontSize(14)
              .fontColor(Color.Grey)
          }
        }
        .width('90%')
        .padding(16)
        .backgroundColor(Color.White)
        .borderRadius(8)
        .shadow({
          color: Color.Grey,
          radius: 4,
          offsetX: 0,
          offsetY: 2
        })
        // 设置完整的无障碍文本,包含卡片的所有关键信息
        .accessibilityText('用户信息卡片,姓名:张三,职位:高级工程师')
        // 将整个卡片设置为一个无障碍组
        .accessibilityGroup(true)
      }
      .padding(16)
      .height('100%')
      .backgroundColor('#F5F5F5')
    }
    .title(this.title)
  }
}

场景五:按钮标注场景

在开发媒体播放器时,我曾经犯过一个错误:使用了纯图标作为播放/暂停按钮,却没有设置无障碍文本。后来测试时发现,视障用户根本不知道这个按钮是做什么的。

我的经验教训

  • 对于任何非文本类按钮(如图像按钮、图标按钮),都必须设置无障碍文本
  • 按钮的无障碍文本应该简洁明了,直接描述其功能
  • 当按钮状态发生变化时(如播放/暂停切换),无障碍文本也应该相应更新
  • 不要在无障碍文本中包含"按钮"、"单指双击打开"等字样,这些会由屏幕朗读自动添加

开发实例

const RESOURCE_STR_PLAY = $r('app.media.play')
const RESOURCE_STR_PAUSE = $r('app.media.pause')

@Entry
@Component
export struct ButtonAccessibilityDemo {
  title: string = '按钮标注场景示例'
  @State isPlaying: boolean = false
  
  play() {
    // 播放音频文件
  }

  pause() {
    // 暂停音频播放
  }

  build() {
    NavDestination() {
      Column() {
        Flex({
          direction: FlexDirection.Column,
          alignItems: ItemAlign.Center,
          justifyContent: FlexAlign.Center,
        }) {
          Row() {
            // 纯图标按钮,必须设置无障碍文本
            Image(this.isPlaying ? RESOURCE_STR_PAUSE : RESOURCE_STR_PLAY)
              .width(50)
              .height(50)
              .onClick(() => {
                this.isPlaying = !this.isPlaying
                if (this.isPlaying) {
                  this.play()
                } else {
                  this.pause()
                }
              })
              .accessibilityRole(BUTTON_TYPE)
              // 根据按钮状态动态更新无障碍文本
              .accessibilityText(this.isPlaying ? '暂停' : '播放')
            Text('Good_morning.mp3')
              .margin({ left: 10 })
          }
        }
        .width('100%')
        .height('100%')
        .backgroundColor(Color.White)
      }
    }
    .title(this.title)
  }
}

场景六:插画/视频/动画的播报场景

在开发教程类应用时,我经常会使用插画或动画来展示操作步骤。但我发现,这些视觉元素对于视障用户来说毫无意义,除非我们为它们添加适当的无障碍信息。

我的解决方法

  • 将插画、视频或动画与其相关的文字说明组合在一起
  • 使用 accessibilityGroup(true) 将它们合并为一个无障碍对象
  • 确保组合后的对象能够提供完整的信息

开发实例

// 示例1:插画与文字组合
@Entry
@Component
export struct IllustrationAccessibilityDemo {
  title: string = '插画播报场景示例'
  private description: string = '向左上滑动手势'

  build() {
    NavDestination() {
      Column() {
        Flex({
          direction: FlexDirection.Column,
          alignItems: ItemAlign.Center,
          justifyContent: FlexAlign.Center,
        }) {
          Column() {
            // 手势插画
            Image($r("app.media.gesture_swipe_left_then_up"))
              .width(220)
              .height(220)
            // 手势说明文字
            Text(this.description)
              .fontSize(22)
              .fontColor(Color.Red)
              .fontWeight(FontWeight.Bold)
              .textAlign(TextAlign.Center)
          }
          // 将插画和文字合并为一个无障碍对象
          .accessibilityGroup(true)
        }
        .width('100%')
        .height('100%')
        .backgroundColor(Color.White)
      }
    }
    .title(this.title)
  }
}

// 示例2:列表项组合标注
@Component
export struct ListItemAccessibilityDemo {
  title: string = '列表项播报场景示例'

  build() {
    NavDestination() {
      Flex({
        direction: FlexDirection.Column,
        alignItems: ItemAlign.Center,
        justifyContent: FlexAlign.Center,
      }) {
        Column() {
          ListItemCard({
            title: '视频卡片',
            subtitle: '提供多种选项',
            time: '1:23 小时',
            color: '#ffdee5ff'
          })
          ListItemCard({
            title: '音乐卡片',
            subtitle: '提供声音反馈',
            time: '2:75 分钟',
            color: '#92e1ffd8'
          })
        }
      }
    }
    .title(this.title)
  }
}

@Component
export struct ListItemCard {
  title: string = '视频卡片'
  subtitle: string = '提供附加选项'
  time: string = '1:23 小时'
  color: ResourceColor = "#80FAFAFA"

  build() {
    Flex({
      direction: FlexDirection.Row,
      alignItems: ItemAlign.Center,
      justifyContent: FlexAlign.SpaceBetween,
    }) {
      Column() {
        Text(this.title)
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .textAlign(TextAlign.Center)
          .padding({ left: 20, right: 0 })
        Text(this.subtitle)
          .fontSize(14)
          .fontColor(Color.Gray)
          .fontWeight(FontWeight.Normal)
          .textAlign(TextAlign.Center)
          .padding({ left: 20, right: 0 })
      }

      Column() {
        Text(this.time)
          .fontSize(20)
          .fontWeight(FontWeight.Normal)
          .textAlign(TextAlign.Center)
          .padding({ left: 10, right: 10 })
      }

      Column() {
        Image($r("app.media.ic_arrow"))
          .width(28)
          .height(28)
          .fillColor(Color.Gray)
      }
      .align(Alignment.End)

    }
    .width('90%')
    .height(75)
    .border({
      width: 1,
      color: '#FFC0C0C0',
      radius: 8,
      style: {
        top: BorderStyle.Solid,
      }
    })
    .backgroundColor(this.color)
    // 将整个卡片合并为单个无障碍对象
    .accessibilityGroup(true)
    .margin({ top: 10 })
  }
}

场景七:内容动态变化场景

在开发聊天应用或实时数据展示应用时,我发现一个问题:当内容动态更新时,视障用户无法及时获知这些变化。比如收到新消息时,屏幕朗读不会自动播报,用户必须手动浏览才能发现。

我的解决方案

  • 当界面内容发生动态变化且对用户有重要提示作用时,使用主动播报接口
  • 播报内容要简洁明了,突出核心信息
  • 避免过度播报,以免打扰用户

开发实例

@Entry
@Component
export struct DynamicContentAccessibilityDemo {
  title: string = '内容动态变化场景示例'
  @State message: string = '初始消息'
  @State counter: number = 0

  // 模拟内容动态变化
  updateContent() {
    this.counter++
    this.message = `更新消息 ${this.counter}`
    
    // 使用主动播报接口播报变化内容
    // 注意:具体API可能因HarmonyOS版本不同而有所差异
    // 请参考官方文档获取最新的主动播报接口
    const eventInfo = {
      type: 'announceForAccessibility',
      bundleName: 'com.example.accessibilitydemo', // 当前应用包名
      text: this.message
    }
    // 发送无障碍事件
    // AccessibilityEvent.sendEvent(eventInfo)
  }

  build() {
    NavDestination() {
      Column() {
        Text('动态内容区域:')
          .width('100%')
          .fontSize(14)
          .fontColor(Color.Black)
          .margin({bottom: 12, left: 20})
        
        Text(this.message)
          .fontSize(18)
          .fontColor(Color.Black)
          .margin({bottom: 24, left: 20})
        
        Button('更新内容')
          .onClick(() => this.updateContent())
          .fontSize(16)
          .margin({left: 20})
      }
      .padding(16)
      .height('100%')
      .backgroundColor('#F5F5F5')
    }
    .title(this.title)
  }
}

场景八:控件状态变化场景

在开发媒体播放器时,我注意到一个细节:当播放/暂停按钮状态切换时,屏幕朗读应该能够反映这种变化。如果按钮状态变了但无障碍文本没变,会让用户感到困惑。

我的解决方案

  • 使用状态变量来控制控件的状态
  • 根据状态动态更新无障碍文本
  • 可结合Toast等其他反馈机制,增强所有用户的体验

开发实例

import { PromptAction } from "@kit.ArkUI"

const RESOURCE_STR_PLAY = $r('app.media.play') // 此处为图片资源,请替换为本地图片
const RESOURCE_STR_PAUSE = $r('app.media.pause') // 此处为图片资源,请替换为本地图片

@Entry
@Component
export struct ControlStateAccessibilityDemo {
  title: string = '控件状态变化场景示例'
  @State isPlaying: boolean = true
  uiContext: UIContext = this.getUIContext();
  promptAction: PromptAction = this.uiContext.getPromptAction();
  
  play() {
    // 播放音频文件
  }

  pause() {
    // 暂停音频播放
  }

  build() {
    NavDestination() {
      Column() {
        Flex({
          direction: FlexDirection.Column,
          alignItems: ItemAlign.Center,
          justifyContent: FlexAlign.Center,
        }) {
          Row() {
            // 根据播放状态显示不同的图标
            Image(this.isPlaying ? RESOURCE_STR_PAUSE : RESOURCE_STR_PLAY)
              .width(50)
              .height(50)
              .onClick(() => {
                // 显示Toast提示,增强视觉反馈
                this.promptAction.showToast({
                  message: this.isPlaying ? "暂停" : "播放"
                })
                
                // 切换播放状态
                this.isPlaying = !this.isPlaying
                if (this.isPlaying) {
                  this.play()
                } else {
                  this.pause()
                }
              })
              // 根据状态动态更新无障碍文本
              .accessibilityText(this.isPlaying ? '暂停' : '播放')
          }
        }
        .width('100%')
        .height('100%')
        .backgroundColor(Color.White)
      }
    }
    .title(this.title)
  }
}

场景九:操作错误场景

在开发网络应用时,我曾经犯过一个错误:当网络连接失败时,我只通过红色文字来显示错误信息,没有考虑到视障用户的需求。后来测试发现,屏幕朗读虽然能识别红色文字,但没有特殊的错误提示,用户可能不会意识到这是一个错误。

我的改进方法

  • 对于错误信息,不仅要使用颜色区分,更要提供明确的文本提示
  • 将错误相关的控件组合成一个无障碍组,确保信息完整
  • 必要时使用主动播报接口,强调错误信息

开发实例

@Entry
@Component
export struct ErrorHandlingAccessibilityDemo {
  title: string = '操作错误场景示例'

  build() {
    NavDestination() {
      Column() {
        Flex({
          direction: FlexDirection.Column,
          alignItems: ItemAlign.Center,
          justifyContent: FlexAlign.Center,
        }) {
          Row() {
            Text('连接状态')
              .fontSize(30)
          }
          Row() {
            Radio({ value: 'Radio1', group: 'radioGroup' })
              .checked(true)
              .radioStyle({
                checkedBackgroundColor: Color.Red
              })
              .height(50)
              .width(50)
              .onChange((isChecked: boolean) => {
                console.log('Radio1 status is ' + isChecked)
              })
            Text('连接中断')
              .fontColor(Color.Red)
          }
          .width('80%')
          // 将单选按钮和错误文本合并为一个无障碍对象
          .accessibilityGroup(true)
        }
        .width('100%')
        .height('100%')
        .backgroundColor(Color.White)
      }
    }
    .title(this.title)
  }
}

场景十:多语种场景

在开发国际化应用时,我遇到了一个挑战:如何处理多语言环境下的无障碍文本。特别是当应用需要支持从右到左书写的语言(如阿拉伯语)时,无障碍文本的处理变得更加复杂。

我的解决方法

  • 使用资源文件管理多语言文本,避免硬编码
  • 考虑不同语言的语法结构,避免简单拼接
  • 特别注意从右到左书写的语言,确保无障碍文本的顺序正确

开发实例

@Entry
@Component
export struct MultilingualAccessibilityDemo {
  title: string = '多语种场景示例'
  // 注意:实际应用中应使用资源文件管理多语言文本
  private multilingualText: string = 'It is convenient: 屏幕朗读已开启 and use'

  build() {
    NavDestination() {
      Column() {
        Flex({
          direction: FlexDirection.Column,
          alignItems: ItemAlign.Center,
          justifyContent: FlexAlign.Center,
        }) {
          Row() {
            Text(this.multilingualText)
              .fontSize(30)
              .fontColor(Color.Blue)
          }
          .width('80%')
        }
        .width('100%')
        .height('100%')
        .backgroundColor(Color.White)
      }
    }
    .title(this.title)
  }
}

场景十一:控件位置调整场景

在开发可拖拽排序的应用(如书签管理、待办事项)时,我发现一个问题:视障用户无法感知控件的移动过程。当他们拖拽一个控件时,不知道自己正在移动它,也不知道它会被放到哪里。

我的解决方案

  • 在控件被托起时,播报"已托起"的信息
  • 在移动过程中,实时播报即将移动到的位置
  • 放置完成后,播报最终的放置位置
  • 确保视障用户听到的位置信息与视觉用户看到的一致

开发实例

import accessibility from '@ohos.accessibility';

@Entry
@Component
export struct ControlPositionAccessibilityDemo {
  title: string = '控件位置调整场景示例';
  
  // 主动播报事件信息
  eventInfo: accessibility.EventInfo = ({
    type: 'announceForAccessibility',
    bundleName: 'com.example.accessibilitydemo',
    triggerAction: 'common',
    textAnnouncedForAccessibility: '移动到华为手机服务|华为官网上面'
  });

  build() {
    NavDestination() {
      Column() {
        Blank()
        Button('模拟控件移动')
          .accessibilityText('模拟控件移动')
          .align(Alignment.Center)
          .fontSize(20)
          .id('button1')
          .onClick(() => {
            // 模拟控件被托起
            this.eventInfo.textAnnouncedForAccessibility = '华为专区已托起';
            accessibility.sendAccessibilityEvent(this.eventInfo).then(() => {
              console.info(`Succeeded in send event, eventInfo is ${JSON.stringify(this.eventInfo)}`);
            });
            
            // 模拟移动过程
            setTimeout(() => {
              this.eventInfo.textAnnouncedForAccessibility = '移动到华为手机服务|华为官网上面';
              accessibility.sendAccessibilityEvent(this.eventInfo).then(() => {
                console.info(`Succeeded in send event, eventInfo is ${JSON.stringify(this.eventInfo)}`);
              });
            }, 500);
            
            // 模拟放置完成
            setTimeout(() => {
              this.eventInfo.textAnnouncedForAccessibility = '已放置到华为手机服务|华为官网上面';
              accessibility.sendAccessibilityEvent(this.eventInfo).then(() => {
                console.info(`Succeeded in send event, eventInfo is ${JSON.stringify(this.eventInfo)}`);
              });
            }, 1000);
          })
        Blank()
      }
      .width('100%')
      .height('100%')
    }
    .title(this.title)
  }
}

场景十二:重新设置新焦点位置的场景

在开发表单应用时,我遇到了一个问题:当用户删除一个输入框时,焦点会跳回到表单的第一个控件,而不是停留在被删除控件的下一个位置。这对视障用户来说非常不友好,因为他们需要重新浏览整个表单。

我的解决方案

  • 当控件消失或隐藏后,使用主动聚焦接口设置新的焦点位置
  • 新焦点应设置在原控件位置的下一个控件上,保持浏览的连续性
  • 确保焦点跳转符合用户的预期

开发实例

import accessibility from '@ohos.accessibility';

@Entry
@Component
export struct FocusManagementAccessibilityDemo {
  title: string = '重新设置新焦点位置场景示例';
  
  // 主动聚焦事件信息
  eventInfo: accessibility.EventInfo = ({
    type: 'requestFocusForAccessibility',
    bundleName: 'com.example.accessibilitydemo',
    triggerAction: 'common',
    customId: 'button1'
  });

  build() {
    NavDestination() {
      Column() {
        Blank()
        Button('点击聚焦到button2')
          .accessibilityText('点击聚焦到button2')
          .align(Alignment.Center)
          .fontSize(20)
          .id('button1')
          .onClick(() => {
            this.eventInfo.customId = 'button2';
            accessibility.sendAccessibilityEvent(this.eventInfo).then(() => {
              console.info(`Succeeded in send event, eventInfo is ${JSON.stringify(this.eventInfo)}`);
            });
          })
        Blank().height('10px')
        Button('button2')
          .accessibilityText('button2')
          .align(Alignment.Center)
          .fontSize(20)
          .id('button2')
        Blank()
      }
      .width('100%')
      .height('100%')
    }
    .title(this.title)
  }
}

场景十三:卡片自动居中的场景

在开发横向滚动的卡片列表时,我发现一个问题:当视障用户通过屏幕朗读浏览卡片时,卡片不会自动居中显示。这意味着用户可能不知道自己正在浏览哪张卡片,也无法看到卡片的完整内容。

我的解决方案

  • 使用 onAccessibilityFocus 回调函数监听卡片的聚焦状态
  • 当卡片获得焦点时,调用滚动接口将其居中显示
  • 确保屏幕朗读用户能够清晰感知当前聚焦的卡片位置

开发实例

class ListDataSource implements IDataSource {
  private list: number[] = [];

  constructor(list: number[]) {
    this.list = list;
  }

  totalCount(): number {
    return this.list.length;
  }

  getData(index: number): number {
    return this.list[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
  }
}

@Entry
@Component
export struct CardAutoCenterAccessibilityDemo {
  title: string = '卡片自动居中场景示例'
  private arr: ListDataSource = new ListDataSource([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
  private scrollerForList: Scroller = new Scroller();
  
  build() {
    NavDestination() {
      Column() {
        Text('横向滚动卡片列表:')
          .width('100%')
          .fontSize(14)
          .fontColor(Color.Black)
          .margin({bottom: 12, left: 20})
        
        List({ space: 20, initialIndex: 0, scroller: this.scrollerForList }) {
          LazyForEach(this.arr, (index: number) => {
            ListItem() {
              Text('卡片 ' + index)
                .width('100%')
                .height(100)
                .fontSize(16)
                .textAlign(TextAlign.Center)
                .borderRadius(10)
                .backgroundColor(0xFFFFFF)
                .accessibilityText(`卡片 ${index}`) // 设置无障碍文本
            }
            .width('60%') // 设置高占比item
            .onClick(() => {
              // 设置点击事件,使组件可被无障碍聚焦
            })
            // 设置无障碍聚焦回调
            .onAccessibilityFocus((isFocus: boolean) => {
              if (isFocus) {
                // 如果聚焦则滚动List,使当前的ListItem居中
                this.scrollerForList.scrollToIndex(index, false, ScrollAlign.CENTER)
              }
            })
          }, (item: string) => item)
        }
        .width('90%')
        .scrollBar(BarState.Off)
        .scrollSnapAlign(ScrollSnapAlign.CENTER) // 设置居中对齐
        .listDirection(Axis.Horizontal) // 设置横向list
      }
      .width('100%')
      .height('100%')
      .backgroundColor(0xDCDCDC)
      .padding({ top: 20 })
    }
    .title(this.title)
  }
}

场景十四:老年宝助手应用无障碍设计实践

在开发"老年宝助手大全"应用时,我将无障碍设计理念贯穿到了整个开发过程中,特别是针对老年人的使用习惯和需求进行了专门优化。这个应用不仅帮助了我的爷爷奶奶,也获得了许多老年用户的好评。

我的经验总结

  • 为老年人设计应用时,不仅要考虑视障用户的需求,还要考虑老年人的视力下降、操作不便等特点
  • 大字体、大按钮、语音播报是老年应用的核心无障碍要素
  • 针对老年用户常用的功能,如收租管理、接送孩子等,需要特别优化无障碍体验
  • 智能语音播报功能应该贯穿整个应用,确保老年人能够通过语音了解所有操作和信息

开发实例

1. 收租管理模块无障碍设计
@Entry
@Component
export struct RentManagementPage {
  @State houses: House[] = [];
  @State selectedHouse: House | null = null;

  build() {
    Column() {
      Text('收租管理')
        .fontSize(24) // 大字体设计
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 20 })
        .accessibilityText('收租管理页面,显示所有房源信息');

      List() {
        ForEach(this.houses, (house) => {
          ListItem() {
            Column() {
              Text(house.address)
                .fontSize(18) // 大字体
                .fontWeight(FontWeight.Bold)
                .accessibilityText(`房源地址:${house.address}`);
              Text(`租金:${house.rent}元/${house.period}`)
                .fontSize(16) // 大字体
                .accessibilityText(`租金:${house.rent}元,${house.period}一付`);
              Text(`下次收租:${house.nextRentDate}`)
                .fontSize(16) // 大字体
                .accessibilityText(`下次收租日期:${house.nextRentDate}`);
            }
            .padding(20)
            .backgroundColor(Color.White)
            .borderRadius(12)
            .margin({ bottom: 10 })
            .onClick(() => {
              this.selectedHouse = house;
              // 点击时发送无障碍事件,播报房源信息
              this.announceAccessibility(`已选择房源:${house.address},租金${house.rent}`);
            });
          }
        });
      }
      .width('100%')
      .height('70%');

      Button('添加房源')
        .fontSize(18) // 大字体
        .width('80%')
        .height(50) // 大按钮
        .backgroundColor('#007AFF')
        .borderRadius(25)
        .margin({ top: 20 })
        .accessibilityText('添加新的房源信息');
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F5F5F5');
  }

  // 无障碍事件发送,用于语音播报
  announceAccessibility(text: string) {
    const eventInfo = {
      type: 'announceForAccessibility',
      bundleName: 'com.example.elderlyassistant',
      text: text
    };
    try {
      eventInfoEmitter.emit('accessibilityEvent', eventInfo);
    } catch (error) {
      console.error('发送无障碍事件失败:', error);
    }
  }
}
2. 接送孩子模块无障碍设计
@Entry
@Component
export struct ChildPickupPage {
  @State children: Child[] = [];
  @State selectedChild: Child | null = null;

  build() {
    Column() {
      Text('接送孩子')
        .fontSize(24) // 大字体设计
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 20 })
        .accessibilityText('接送孩子页面,显示所有孩子信息');

      List() {
        ForEach(this.children, (child) => {
          ListItem() {
            Column() {
              Text(child.name)
                .fontSize(18) // 大字体
                .fontWeight(FontWeight.Bold)
                .accessibilityText(`孩子姓名:${child.name}`);
              Text(`学校:${child.school}`)
                .fontSize(16) // 大字体
                .accessibilityText(`所在学校:${child.school}`);
              Text(`放学时间:${child.schoolTime}`)
                .fontSize(16) // 大字体
                .accessibilityText(`放学时间:${child.schoolTime}`);
            }
            .padding(20)
            .backgroundColor(Color.White)
            .borderRadius(12)
            .margin({ bottom: 10 })
            .onClick(() => {
              this.selectedChild = child;
              // 点击时发送无障碍事件,播报孩子信息
              this.announceAccessibility(`已选择孩子:${child.name},学校${child.school},放学时间${child.schoolTime}`);
            });
          }
        });
      }
      .width('100%')
      .height('70%');

      Button('添加接送记录')
        .fontSize(18) // 大字体
        .width('80%')
        .height(50) // 大按钮
        .backgroundColor('#007AFF')
        .borderRadius(25)
        .margin({ top: 20 })
        .accessibilityText('添加今天的接送记录');
    }
    .width('100%')
    .height('100%')
    .padding(20)
    .backgroundColor('#F5F5F5');
  }

  // 无障碍事件发送,用于语音播报
  announceAccessibility(text: string) {
    const eventInfo = {
      type: 'announceForAccessibility',
      bundleName: 'com.example.elderlyassistant',
      text: text
    };
    try {
      eventInfoEmitter.emit('accessibilityEvent', eventInfo);
    } catch (error) {
      console.error('发送无障碍事件失败:', error);
    }
  }
}
3. 全局语音播报服务
// 全局语音播报服务
class VoiceAnnouncementService {
  private static instance: VoiceAnnouncementService;
  private ttsEngine: TTS | null = null;

  // 初始化TTS引擎
  init() {
    // 初始化TTS引擎代码
  }

  // 播报文本
  announce(text: string) {
    if (this.ttsEngine) {
      this.ttsEngine.speak(text);
    }
  }

  // 页面切换时播报页面名称
  announcePageChange(pageName: string) {
    this.announce(`进入${pageName}页面`);
  }

  // 操作成功时播报
  announceSuccess(operation: string) {
    this.announce(`${operation}成功`);
  }

  // 操作失败时播报
  announceError(operation: string, error: string) {
    this.announce(`${operation}失败,${error}`);
  }

  // 获取单例实例
  static getInstance(): VoiceAnnouncementService {
    if (!VoiceAnnouncementService.instance) {
      VoiceAnnouncementService.instance = new VoiceAnnouncementService();
      VoiceAnnouncementService.instance.init();
    }
    return VoiceAnnouncementService.instance;
  }
}

// 导出单例
export const voiceService = VoiceAnnouncementService.getInstance();
4. 适老化UI组件库
// 适老化按钮组件
@Component
export struct ElderlyButton {
  @Prop text: string;
  @Prop onClick: () => void;
  @Prop accessibilityText?: string;

  build() {
    Button(this.text)
      .fontSize(18) // 大字体
      .width('80%')
      .height(50) // 大按钮
      .backgroundColor('#007AFF')
      .borderRadius(25)
      .accessibilityText(this.accessibilityText || this.text);
  }
}

// 适老化文本组件
@Component
export struct ElderlyText {
  @Prop text: string;
  @Prop fontSize?: number;
  @Prop fontWeight?: number;
  @Prop accessibilityText?: string;

  build() {
    Text(this.text)
      .fontSize(this.fontSize || 16) // 默认大字体
      .fontWeight(this.fontWeight || FontWeight.Normal)
      .accessibilityText(this.accessibilityText || this.text);
  }
}

通过以上无障碍设计实践,"老年宝助手大全"应用不仅满足了视障用户的需求,也为普通老年用户提供了更加友好、便捷的使用体验。应用上线后,收到了大量老年用户的好评,许多用户表示通过这个应用,他们的生活变得更加轻松有序。

最让我感动的是,有一位80岁的用户写信给我们说:“以前我觉得手机是年轻人的玩意儿,现在有了这个应用,我觉得手机是我的好朋友,它帮我管理生活,提醒我吃药,还能让我和孩子们保持联系。谢谢你们为我们老年人着想。”

这就是无障碍开发的真正价值——不仅仅是技术的实现,更是对人的关怀和尊重。

无障碍开发最佳实践

通过这些年的无障碍开发实践,我总结了一些实用的最佳实践,希望能对大家有所帮助。

布局与配色

布局设计

  • 保持清晰的视觉层次,让用户能够轻松理解界面结构
  • 确保控件间距合理,便于触摸操作,特别是对于运动障碍用户
  • 避免过于复杂的嵌套结构,减少用户的导航难度

配色方案

  • 确保文本与背景对比度足够,我通常使用在线对比度检查工具进行验证
  • 避免使用纯色彩传递重要信息,始终配合文本说明
  • 尊重系统设置,支持高对比度和色彩校正模式

交互与反馈

交互设计

  • 为所有可交互控件提供明确的状态反馈,让用户知道操作是否成功
  • 确保操作结果有清晰的反馈,视觉和听觉反馈并重
  • 支持多种输入方式,包括触摸、键盘导航等

焦点管理

  • 确保焦点顺序符合用户预期,通常是从上到下、从左到右
  • 避免焦点陷阱,确保用户可以通过Tab键遍历所有可交互元素
  • 为复杂界面提供焦点跳转功能,帮助用户快速导航到关键区域

文本与播报

文本设计

  • 使用简洁明了的语言,避免使用专业术语和复杂句式
  • 确保文本大小可调整,尊重用户的系统字体设置
  • 为图片、图标等非文本元素提供文本替代方案

播报策略

  • 确保关键信息能够被屏幕朗读正确播报
  • 避免信息冗余和重复播报,以免打扰用户
  • 合理使用主动播报,只在必要时使用

开发流程

规划阶段

  • 在设计初期就考虑无障碍需求,将其纳入产品规划
  • 了解目标用户群体的需求和使用场景
  • 参考相关无障碍标准和指南

开发阶段

  • 使用Accessibility Kit提供的API,确保应用的无障碍基础
  • 定期进行无障碍测试,包括使用屏幕朗读等辅助技术
  • 邀请障碍用户参与测试,获取直接反馈

发布与维护

  • 在应用发布前进行全面的无障碍评估
  • 建立无障碍反馈渠道,及时响应用户的无障碍问题
  • 在后续版本更新中,保持对无障碍的持续优化

常见问题与解决方案

在无障碍开发过程中,我遇到过很多问题,也积累了一些解决方案。下面分享几个最常见的问题及解决方法。

问题一:屏幕朗读重复播报信息

问题描述:当界面有嵌套控件时,屏幕朗读会重复播报相同的信息,比如一个卡片中的标题和内容会被分别播报,然后整个卡片又会被播报一次。

原因分析:多个嵌套控件都设置了可聚焦属性,导致同一信息被多次处理。

解决方案

  • 使用 accessibilityGroup(true) 将相关控件组合成一个无障碍单元
  • 只在父控件上设置完整的无障碍文本,包含所有子控件的信息
  • 对于不需要单独聚焦的子控件,使用 accessibilityLevel("no") 禁用其焦点

问题二:动态内容更新后屏幕朗读未及时播报

问题描述:当应用内容动态更新时(比如收到新消息、加载新数据),屏幕朗读不会自动播报这些变化,用户需要手动浏览才能发现。

原因分析:动态内容更新时,系统不会自动发送无障碍事件,需要开发者手动触发。

解决方案

  • 使用主动播报接口发送朗读事件,及时告知用户内容变化
  • 确保内容更新后焦点位置合理,避免用户迷失
  • 为重要的动态更新提供明确、简洁的播报信息

问题三:自定义控件无法被屏幕朗读识别

问题描述:自定义的UI控件(比如特殊的按钮、滑块等)无法被屏幕朗读识别,视障用户无法知道控件的功能和状态。

原因分析:自定义控件未设置适当的无障碍属性,系统无法理解其功能和状态。

解决方案

  • 为自定义控件设置 accessibilityText,描述其功能
  • 确保控件能够获取无障碍焦点,可通过添加点击事件实现
  • 对于复杂的自定义控件,实现完整的无障碍行为,包括状态变化的处理

问题四:从右到左语言的无障碍支持

问题描述:当应用支持阿拉伯语等从右到左书写的语言时,无障碍文本的顺序可能会出现问题。

原因分析:简单的文本拼接可能不符合从右到左语言的语法规则。

解决方案

  • 使用资源文件管理多语言文本,避免硬编码
  • 考虑不同语言的语法结构,为每种语言提供合适的无障碍文本
  • 测试从右到左语言的无障碍体验,确保播报顺序正确

开发工具与资源

在无障碍开发过程中,合适的工具和资源可以大大提高开发效率。以下是我常用的一些工具和参考资源。

开发工具

DevEco Studio

  • 鸿蒙应用开发的官方IDE,集成了无障碍开发相关的工具和功能
  • 支持实时预览和调试,方便开发者快速验证无障碍效果
  • 提供了丰富的模板和示例,包括无障碍相关的最佳实践

Accessibility Inspector

  • 用于检查应用无障碍属性的工具,可以查看控件的无障碍焦点、文本等信息
  • 帮助开发者发现和修复无障碍问题
  • 支持模拟不同的无障碍场景

屏幕朗读工具

  • 系统自带的屏幕朗读功能,是测试无障碍效果的最佳工具
  • 可以模拟视障用户的使用场景,发现潜在问题
  • 建议在开发过程中经常使用,确保应用的无障碍体验

参考资源

官方文档

国际标准

社区资源

  • 鸿蒙开发者社区:有很多开发者分享的无障碍开发经验和案例
  • GitHub:有一些开源的无障碍工具和示例代码
  • Stack Overflow:可以搜索和提问无障碍相关的问题

总结与展望

我的无障碍开发之路

回顾这三年的无障碍开发之路,我感慨万千。从最初的迷茫和挫折,到现在的自信和从容,我不仅掌握了 Accessibility Kit 的各种技术,更重要的是,我对技术的本质有了更深的理解。

记得有一次,我在开发者大会上分享无障碍开发经验,一位年轻的开发者问我:“做无障碍开发会不会影响应用的性能和美观?” 我告诉他:“恰恰相反,无障碍开发会让你的应用更稳定、更易用、更有温度。”

现在,我团队开发的每一个应用,从设计阶段就会考虑无障碍需求。我们会在评审会上讨论每个控件的焦点管理,会在测试阶段邀请视障用户参与体验,会在发布后持续收集无障碍相关的反馈。无障碍已经成为我们开发流程中不可或缺的一部分。

无障碍开发的三重境界

通过这三年的实践,我总结出无障碍开发的三重境界:

第一重:达标

  • 完成基本的无障碍适配,确保应用能够被屏幕朗读等辅助技术识别
  • 满足最低限度的无障碍要求,避免明显的障碍
  • 这是入门阶段,也是对开发者的基本要求

第二重:优化

  • 深入理解用户需求,主动优化无障碍体验
  • 针对不同场景提供个性化的无障碍解决方案
  • 关注细节,提升整体无障碍质量
  • 这是进阶段,需要持续的学习和实践

第三重:融合

  • 将无障碍理念融入产品设计的各个环节
  • 无障碍不再是额外的工作,而是产品的固有属性
  • 创造出真正人人可用、人人喜爱的优秀应用
  • 这是大师阶段,需要对用户需求有深刻的理解

无障碍开发的价值

我常常问自己:无障碍开发的意义到底是什么?

是为了满足合规要求吗?是为了获得奖项吗?是为了提升品牌形象吗?

都不是。无障碍开发的真正意义,是为了让每一个人都能平等地使用技术,都能享受数字化带来的便利。是为了让那位老年用户能够轻松管理收租和接送孩子,是为了让行动不便的老人能够独立使用手机,是为了让视障人士能够顺畅地与世界沟通。

当我收到用户的感谢邮件,当我看到视障用户通过我的应用自信地生活,当我听到团队成员说"无障碍开发让我对技术有了新的认识",我知道,我所做的一切都是值得的。

未来已来

现在,我正在探索如何将人工智能技术应用到无障碍开发中。我相信,未来的无障碍技术将会更加智能、更加个性化:

  • 系统可以自动识别界面元素并生成无障碍描述
  • 应用可以根据用户的具体障碍类型自动调整行为
  • 多种交互方式的融合将为障碍用户提供更多选择
  • 更统一、更完善的无障碍标准将让开发变得更加简单

但无论技术如何发展,无障碍开发的核心始终不变:以用户为中心,用技术创造更包容的世界。

最后的话

我不是什么技术大牛,也不是什么无障碍专家。我只是一个普通的开发者,一个希望用技术做一些有意义的事情的人。

如果你问我,无障碍开发难吗?我会告诉你:难,真的很难。它需要你付出更多的时间和精力,需要你不断学习和尝试,需要你站在不同用户的角度思考问题。

但如果你问我,无障碍开发值得吗?我会毫不犹豫地告诉你:值得,非常值得。当你看到自己的应用能够被更多人使用,当你收到用户的感谢和认可,当你知道自己正在为缩小数字鸿沟贡献力量,那种成就感和满足感是任何奖项都无法替代的。

所以,亲爱的开发者朋友们,让我们一起行动起来。从现在开始,从每一行代码开始,将无障碍理念融入我们的开发实践中。

因为真正的技术,应该是人人可用的技术。真正的创新,应该是让每个人都能受益的创新。

让我们一起,用技术创造更美好的未来。

Logo

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

更多推荐