鸿蒙 Accessibility Kit 无障碍开发全解析
本文详细介绍了鸿蒙系统Accessibility Kit的无障碍开发实践,分享了作者从真实用户需求出发,为老年人设计无障碍应用的经验。文章首先通过作者与祖父母互动的故事,阐述了无障碍开发的重要性,随后系统讲解了Accessibility Kit的核心功能和架构设计。重点呈现了13个常见无障碍开发场景的解决方案,包括屏幕朗读内容标注、焦点管理优化等实用技巧。通过具体代码示例展示了如何让应用更友好地服
鸿蒙 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:
- 用于检查应用无障碍属性的工具,可以查看控件的无障碍焦点、文本等信息
- 帮助开发者发现和修复无障碍问题
- 支持模拟不同的无障碍场景
屏幕朗读工具:
- 系统自带的屏幕朗读功能,是测试无障碍效果的最佳工具
- 可以模拟视障用户的使用场景,发现潜在问题
- 建议在开发过程中经常使用,确保应用的无障碍体验
参考资源
官方文档:
- Accessibility Kit 概述:华为官方文档,详细介绍了Accessibility Kit的核心概念和能力
国际标准:
- Web Content Accessibility Guidelines (WCAG):国际公认的无障碍设计标准,虽然针对Web,但很多原则也适用于移动应用
- Mobile Accessibility: How WCAG 2.1 and Other W3C/WAI Guidelines Apply to Mobile:专门针对移动应用的无障碍指南
社区资源:
- 鸿蒙开发者社区:有很多开发者分享的无障碍开发经验和案例
- GitHub:有一些开源的无障碍工具和示例代码
- Stack Overflow:可以搜索和提问无障碍相关的问题
总结与展望
我的无障碍开发之路
回顾这三年的无障碍开发之路,我感慨万千。从最初的迷茫和挫折,到现在的自信和从容,我不仅掌握了 Accessibility Kit 的各种技术,更重要的是,我对技术的本质有了更深的理解。
记得有一次,我在开发者大会上分享无障碍开发经验,一位年轻的开发者问我:“做无障碍开发会不会影响应用的性能和美观?” 我告诉他:“恰恰相反,无障碍开发会让你的应用更稳定、更易用、更有温度。”
现在,我团队开发的每一个应用,从设计阶段就会考虑无障碍需求。我们会在评审会上讨论每个控件的焦点管理,会在测试阶段邀请视障用户参与体验,会在发布后持续收集无障碍相关的反馈。无障碍已经成为我们开发流程中不可或缺的一部分。
无障碍开发的三重境界
通过这三年的实践,我总结出无障碍开发的三重境界:
第一重:达标
- 完成基本的无障碍适配,确保应用能够被屏幕朗读等辅助技术识别
- 满足最低限度的无障碍要求,避免明显的障碍
- 这是入门阶段,也是对开发者的基本要求
第二重:优化
- 深入理解用户需求,主动优化无障碍体验
- 针对不同场景提供个性化的无障碍解决方案
- 关注细节,提升整体无障碍质量
- 这是进阶段,需要持续的学习和实践
第三重:融合
- 将无障碍理念融入产品设计的各个环节
- 无障碍不再是额外的工作,而是产品的固有属性
- 创造出真正人人可用、人人喜爱的优秀应用
- 这是大师阶段,需要对用户需求有深刻的理解
无障碍开发的价值
我常常问自己:无障碍开发的意义到底是什么?
是为了满足合规要求吗?是为了获得奖项吗?是为了提升品牌形象吗?
都不是。无障碍开发的真正意义,是为了让每一个人都能平等地使用技术,都能享受数字化带来的便利。是为了让那位老年用户能够轻松管理收租和接送孩子,是为了让行动不便的老人能够独立使用手机,是为了让视障人士能够顺畅地与世界沟通。
当我收到用户的感谢邮件,当我看到视障用户通过我的应用自信地生活,当我听到团队成员说"无障碍开发让我对技术有了新的认识",我知道,我所做的一切都是值得的。
未来已来
现在,我正在探索如何将人工智能技术应用到无障碍开发中。我相信,未来的无障碍技术将会更加智能、更加个性化:
- 系统可以自动识别界面元素并生成无障碍描述
- 应用可以根据用户的具体障碍类型自动调整行为
- 多种交互方式的融合将为障碍用户提供更多选择
- 更统一、更完善的无障碍标准将让开发变得更加简单
但无论技术如何发展,无障碍开发的核心始终不变:以用户为中心,用技术创造更包容的世界。
最后的话
我不是什么技术大牛,也不是什么无障碍专家。我只是一个普通的开发者,一个希望用技术做一些有意义的事情的人。
如果你问我,无障碍开发难吗?我会告诉你:难,真的很难。它需要你付出更多的时间和精力,需要你不断学习和尝试,需要你站在不同用户的角度思考问题。
但如果你问我,无障碍开发值得吗?我会毫不犹豫地告诉你:值得,非常值得。当你看到自己的应用能够被更多人使用,当你收到用户的感谢和认可,当你知道自己正在为缩小数字鸿沟贡献力量,那种成就感和满足感是任何奖项都无法替代的。
所以,亲爱的开发者朋友们,让我们一起行动起来。从现在开始,从每一行代码开始,将无障碍理念融入我们的开发实践中。
因为真正的技术,应该是人人可用的技术。真正的创新,应该是让每个人都能受益的创新。
让我们一起,用技术创造更美好的未来。
更多推荐
所有评论(0)