← 返回蜂巢洞察

如何使用Jetpack Compose开发易于使用的Android应用程序:一份全面的指南

让移动应用程序具备无障碍使用功能,就能确保包括那些有视觉、听觉、运动或认知障碍的人在内的所有用户,都能有效地与这些应用程序进行交互并使用它们。 过去,无障碍设计往往被视为事后才需要考虑的问题——只有在发布前有足够时间时才会被纳入考量范围。 如今,这种观念已经发生了根本性的变化。随着《欧洲无障碍法案》等法规在欧盟各市场开始生效,开发具备无障碍功能的软件逐渐成为一项法律和商业上的必备要求。 更重要的是,开发无障碍应用程序本身就是良好的工程实践。这样的应用能够提升所有用户的使用体验,同时也能确保你不会遗漏掉大量潜在的用户群体。 与传统的Android View系统相比,Jetpack Compose

让移动应用程序具备无障碍使用功能,就能确保包括那些有视觉、听觉、运动或认知障碍的人在内的所有用户,都能有效地与这些应用程序进行交互并使用它们。

过去,无障碍设计往往被视为事后才需要考虑的问题——只有在发布前有足够时间时才会被纳入考量范围。

如今,这种观念已经发生了根本性的变化。随着《欧洲无障碍法案》等法规在欧盟各市场开始生效,开发具备无障碍功能的软件逐渐成为一项法律和商业上的必备要求。

更重要的是,开发无障碍应用程序本身就是良好的工程实践。这样的应用能够提升所有用户的使用体验,同时也能确保你不会遗漏掉大量潜在的用户群体。

与传统的Android View系统相比,Jetpack Compose在实现无障碍功能方面更为便捷。由于Compose采用声明式设计且以状态驱动机制运行,因此它能够在处理视觉用户界面的同时,也管理相关的语义元数据。

在这份全面的指南中,你将了解到Jetpack Compose中的无障碍功能是如何实现的,如何使用语义树结构,如何应用基本及高级的无障碍设计技巧,以及如何通过自动化测试、Google的无障碍扫描工具、Android Studio的布局检查器以及TalkBack等功能来全面检测你的应用程序是否具备无障碍使用能力。

先决条件

要跟随本指南进行学习,你需要满足以下条件:

  • 对Kotlin和Jetpack Compose有基本的了解(包括Composable组件、修饰器以及状态管理等相关概念)。

  • 拥有Android Studio开发环境(版本需为Hedgehog、Iguana、Jellyfish、Koala、Ladybug或更高版本)。

  • 拥有一台运行Android 9.0(API 28)或更高版本的Android设备或模拟器,并且该设备能够访问Google Play商店,以便安装并使用当前版本的Google无障碍扫描工具。

目录

为什么在现代Android开发中可访问性如此重要

根据世界卫生组织的数据,有超过13亿人存在严重的残疾问题,这占全球人口的约16%。这些残疾人包括那些患有永久性视力障碍、听力损失、身体运动受限以及认知或神经系统方面问题的人。

除了永久性的残疾外,用户还经常会遇到暂时性或特定环境下的使用限制。例如,一个人如果用一只手抱着婴儿,就会在动作上受到限制;在强烈的正午阳光下使用手机的人会暂时出现视力障碍;而在嘈杂的机场候机厅里,人们的听力也会受到影响。

设计一个具备可访问性的应用程序,就能确保你的界面能够在所有这些情况下以及更多类似场景中依然能够被正常使用。

除了体现同理心和采用包容性设计之外,可访问性也越来越受到国际法律的约束。

在欧盟,《欧洲可访问性法案》规定了一系列产品和服务必须具备可访问性,这些产品和服务包括智能手机、电脑、电子商务服务、消费者银行服务、电子书以及公共交通服务。实际上,产品的合规性通常是通过符合EN 301 549这一欧洲数字产品和服务的可访问性标准来检验的。

在美国,《美国残疾人法案》适用于州和地方政府(其中第二章明确规定网页和移动应用程序必须符合WCAG 2.1 AA级标准),也适用于公共场所。另外,《康复法》的第508条要求联邦机构的信息和通信技术也必须具备可访问性。

如果未能达到这些标准,企业可能会面临法律处罚并损害自身品牌声誉。相反,优先考虑可访问性能够扩大你的目标市场范围,并提高用户的留存率。

应用的质量也会影响其传播效果:谷歌的Android Vitals文档指出,如果用户感知到的应用程序崩溃频率过高,就会影响该应用在Google Play平台上的被发现几率。

Android可访问性的核心概念

要想使用Jetpack Compose来设计具备可访问性的应用程序,你就必须了解Android操作系统是如何与辅助技术进行交互的。

了解Android可访问性服务

Android系统内置了几种可访问性服务,这些服务会以后台进程的形式运行。它们会拦截应用程序的用户界面,并将其转化为其他形式的感官反馈或输入方式,从而帮助有障碍的用户使用应用程序。

  • TalkBack(屏幕阅读器): TalkBack是Android内置的屏幕阅读器,专为盲人及视力障碍用户设计。它能够识别界面中的各种元素,将视觉内容及其描述转化为合成语音或触觉反馈,并支持通过线性滑动手势进行非视觉导航。

  • Switch Access: 适用于那些因运动能力严重受限而无法使用物理触摸屏的用户。Switch Access允许用户通过一个或多个物理开关(如脚踏键、吸吮装置或单一按钮),或连接的键盘来控制设备。该功能会依次扫描屏幕上可聚焦的元素,当目标元素被高亮显示时,用户便可对其进行选择。

  • Voice Access: 用户可以通过语音指令来操控整个手机,例如“点击打开”“向下滚动”或“输入‘hello’”。系统会根据交互元素的辅助功能标签为其分配对应的数字代码,这样用户就可以通过数字命令来执行相应操作。

  • “选择后朗读”功能: 用户可以选中屏幕上的特定段落、按钮或图标,让其被大声读出来,而无需启动完整的TalkBack导航功能。

所有这些无障碍服务都有一个共同的要求:它们不会直接与视觉像素进行交互。相反,它们会读取你的应用程序生成的语义树

Compose语义树的工作原理

当你在Jetpack Compose中构建用户界面时,Compose会生成两个相互独立但彼此关联的内部树结构:

  1. 布局树(UI树): 这棵树包含了用于在屏幕上测量位置并绘制像素的视觉渲染节点(例如Canvas、Box、Row、Column、Text和Image等)。

  2. 语义树: 这棵树与布局树并行运行。它包含描述每个元素的含义、用途、状态以及交互功能的元数据。

层次 视觉布局树(UI与像素) 语义树(无障碍功能与TalkBack服务)
容器 Row(modifier = Modifier.Clickable { ... }) 合并后的无障碍节点
子元素1 ImageIcons.Default.Star) 合并到父元素的描述中
子元素2 Text("4.5") 合并到父元素的描述中
子元素3 Text("评分") 合并到父元素的描述中
最终效果 在屏幕上绘制独立的视觉像素 TalkBack服务会播报: "评分:4.5星,按钮"

标准的Material Compose组件(如ButtonCheckboxSliderSwitchText)会自动将相应的语义属性填充到语义树中。例如,一个Button组件会自动将其角色设置为Role.Button,并关联点击事件。

但是,当你创建自定义布局、使用画布进行绘图或设计非标准的交互式部件时,Compose就无法自动理解你的设计意图。在这种情况下,你必须使用Compose提供的语义修饰器来手动丰富语义树的内容。

WCAG 2.1的POUR原则

国际上衡量数字产品无障碍性的标准是Web内容无障碍指南(WCAG)2.1,该规范由万维网联盟(W3C)发布。这些指南围绕四个核心原则进行编排,这些原则的缩写就是POUR

  • 可感知性: 信息和用户界面组件必须以用户能够感知的方式呈现出来。内容不能完全超出用户的任何感官所能察觉的范围。在Android应用中,这意味着要为所有视觉媒体提供描述性文本替代方案,支持动态字体缩放,并保持较高的颜色对比度。

  • 可操作性: 用户界面组件和导航机制必须具备可操作性。无论用户是使用触摸屏、硬件键盘、语音指令还是其他交互设备,他们都应能够顺利完成各种操作。这要求有足够大的触控目标(Android的标准至少为48dp × 48dp),以及合理的导航逻辑。

  • 可理解性: 用户必须能够理解信息以及界面的使用方法。这意味着要编写清晰的标签,提供有意义的错误提示信息,避免突然出现的布局变化,并使表单的结构具有可预测性。

  • 稳健性: 内容必须具备足够的稳健性,以便各种不同的用户代理和辅助技术都能可靠地解析它。在Compose中,这意味着要避免使用临时性的解决方案,遵循标准的语义规则,并尊重系统的用户设置。

Jetpack Compose中的语义功能

在本节中,我们将探讨Compose是如何暴露和操作这些语义元数据的。

什么是语义?

在Jetpack Compose中,语义是指通过Modifier.semantics修饰符附加到布局节点上的键值对属性。它们能够传递以下信息:

  • 一个元素的名称(contentDescription

  • 该元素代表哪种类型的UI控件(role = Role.CheckboxRole.ButtonRole.Tab

  • 该元素当前所处的状态(stateDescription = "Checked"progressBarRangeInfo

  • 用户可以对该元素执行哪些操作(onClickonLongClickcustomActions

基本的语义属性与内容描述

最常用的语义属性是contentDescription。它为图标、照片和矢量图形等非文本类型的UI元素提供本地化的文字说明。

以下是如何为视觉组件设置内容描述的方法:

// 这是一个用于执行特定操作的图标按钮
IconButton(onClick = { /* 打开相机 */ }) {
    Icon(
        painter = painterResource(id = R.drawable.ic_camera),
        contentDescription = "打开相机"
    )
}

// 这是一个装饰性背景图案
Image(
    painter = painterResource(id = Rdrawable.decorative_pattern),
    contentDescription = null, // 告诉TalkBack忽略这个元素
    modifier = Modifier.fillMaxWidth()
)

其内部工作原理

当TalkBack导航到Icon元素时,它会读取contentDescription并将其读出声来(例如“打开相机,按钮,双击可激活”)。由于IconButton表示的是Role.Button类型,因此TalkBack会自动添加“按钮”这个词。不过,具体的表述方式可能会因版本和地区设置的不同而有所差异。本指南中的示例仅用于说明目的。

对于那些仅起装饰作用的元素(比如背景图案、分隔线或辅助描述性文本的视觉配图),你应该明确将contentDescription设置为null。这样,Compose就会在生成语义树时忽略这些元素,从而避免屏幕阅读器被无意义的视觉内容所干扰。

自定义的语义属性与状态描述

当某个元素的状态发生动态变化时(比如在播放和暂停状态之间切换,或者在展开和折叠状态之间转换),屏幕阅读器的使用者需要在交互之前了解该元素当前所处的状态。<你可以 在 `Modifier.semantics` 块中设置 `stateDescription` 和 `role` 参数:

var isPlaying by remember { mutableStateOf(false) }

IconButton(
    onClick = { isPlaying = !isPlaying },
    modifier = Modifier.semantics {
        // 明确说明媒体播放器的当前状态
        stateDescription = if (isPlaying) "正在播放音频" else "音频已暂停"
        role = Role.Button
    }
) {
    Icon(
        imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
        contentDescription = if (isPlaying) "暂停" else "播放"
    )
}

内部工作原理

如果没有stateDescription,TalkBack只会识别出“播放”这个动作。用户就必须自己判断音频当前是正在播放还是已经暂停。

通过添加stateDescription,TalkBack会给出这样的提示:"正在播放音频,按钮,双击可切换状态。"这样用户就能立即知道当前的播放状态,以及点击该按钮后会执行什么操作。

如何使用“mergeDescendants”合并语义信息

在复杂的布局中,多个独立的组件往往会被组合起来来表示一个逻辑上的整体。例如,用户个人资料页面可能包含头像、用户名、在线状态标识以及时间戳等信息。

默认情况下,TalkBack会将每一个TextImage子节点都视为独立的元素,因此用户需要滑动屏幕四到五次才能跳过列表中的某一项内容。

你可以使用Modifier.semantics(mergeDescendants = true)将所有子节点合并成一个整体,从而简化无障碍访问功能:

// 不正确的示例:TalkBack会分别识别三个独立元素:"星形图标"、"4.5"、"客户评分"
Row(modifier = Modifier.clickable { /* 导航到评论页面 */ }) {
    Icon(
        imageVector = Icons.Default.Star,
        contentDescription = "星形图标"
    )
    Text(text = "4.5")
    Text(text = "客户评分")
}

// 正确的示例:所有元素被合并为一个整体:"客户评分:4.5星(满分5星),按钮"
Row(
    modifier = Modifier
        .clickable(onClickLabel = "查看所有评论") { /* 导航到评论页面 */ }
        .semantics(mergeDescendants = true) {
            contentDescription = "客户评分:4.5星(满分5星)
        }
) {
    Icon(
        imageVector = Icons.Default.Star,
        contentDescription = null // 因为父元素已经提供了完整的描述信息,所以这个子元素的描述被省略了
    )
    Text(text = "4.5")
    Text(text = "客户评分")
}

内部工作原理

当设置mergeDescendants = true时,Compose会合并所有子组件的无障碍访问相关信息。这样一来,在语义树中就不会生成多个独立的节点,而是只有一个节点会被暴露给无障碍访问框架。TalkBack会将整个Row视为一个整体来识别,并读取父元素中的contentDescription描述。

如何使用 clearAndSetSemantics 清除元素的语义属性

在某些情况下,标准的可组合组件可能会包含一些默认的语义行为,这些行为会干扰你期望实现的无障碍访问功能。

Modifier.clearAndSetSemantics 这个修饰器会清除所有原本会从子组件或默认实现中继承来的语义属性,从而使你可以自定义明确的语义设置:

// 一个用于显示通知数量的复合组件
Box(
    modifier = Modifier.clearAndSetSemantics {
        contentDescription = "有3条未读消息"
        role = Role.Button
    }
) {
    Icon(
        imageVector = Icons.Default.Email,
        contentDescription = "电子邮件图标" // 这些属性被清除并会被忽略
    )
    Text(text = "3") // 这些属性也被清除并会被忽略
}

其内部工作原理

Modifier.semantics 不同,clearAndSetSemantics 会彻底删除由组件子树生成的所有语义属性。在上面的例子中,“电子邮件图标”和“3”这两个文本节点会被完全从语义树中移除,只保留“有3条未读消息”的这一信息。

基本的无障碍访问实践

接下来我们将介绍你在应用程序中应该遵循的核心无障碍访问实践,并解释为什么每个实践都很重要。

如何编写有意义的内容描述

内容描述应当简洁明了地说明一个元素的用途功能,而不是它的视觉外观。

// 具有描述性且以功能为导向的标签
IconButton(onClick = { deleteDraft() }) {
    Icon(
        imageVector = Icons.Default.Delete,
        contentDescription = "删除草稿消息"
    )
}

// 与上下文相关的天气信息
Image(
    painter = painterResource(R.drawable.weather_sunny),
    contentDescription = "当前天气:晴朗,华氏75度"
)

图标描述清楚地告诉用户点击按钮后会发生什么(“删除草稿消息”);而天气图片则直接传达了实际的数据信息,而不是描述图标的艺术风格。

应避免的做法

// 避免使用泛泛或重复的描述
IconButton(onClick = { deleteDraft() }) {
    Icon(
        imageVector = Icons.Default.Delete,
        contentDescription = "垃圾桶图标按钮" // 这种描述包含了视觉样式和元素类型,是不正确的
    )
}

// 在交互式元素上不要使用空字符串作为描述
IconButton(onClick = { openSettings() }) {
    Icon(
        imageVector = Icons.Default.Settings,
        contentDescription = "" // 这样会导致按钮没有可访问的标签
    )
}
使用“图标”或“按钮”这样的词汇其实是多余的,因为TalkBack本身就会说明这些组件的功能。如果在交互元素上设置为空字符串(""),那么TalkBack就没有任何有意义的信息可以读取出来(通常会将其读作“未标记的按钮”),这样一来,屏幕阅读器的用户就无法了解这个按钮的具体作用。Compose的工具会将空的内容描述视为缺失内容来进行处理。

如何确保最小的触摸目标尺寸

谷歌的Material Design设计规范以及Android的无障碍访问指南要求,所有交互元素的最小触摸目标尺寸必须至少为48dp × 48dp,这个尺寸相当于实际的9毫米大小。这一数值完全符合谷歌推荐的7–10毫米范围。 作为对比,WCAG本身也设定了针对网页应用的较低标准:WCAG 2.1的2.5.5成功标准要求在AAA级别时触摸目标尺寸为44×44 CSS像素,而WCAG 2.2的2.5.8成功标准则要求在AA级别时触摸目标尺寸为24×24 CSS像素。因此,48dp这一规定实际上属于Android更为严格的平台标准。

// 错误的做法:将可点击区域的大小严格限制在20dp Icon( imageVector = Icons.Default.Close, contentDescription = "关闭对话框", modifier = Modifier .size(20(dp) .clickable { dismissDialog() } // 触摸目标尺寸仅为20dp x 20dp! ) 当触摸目标尺寸仅为20dp时,用户几乎不可能可靠地点击到该区域,尤其是在高密度显示屏上或行走时使用设备时。谷歌的无障碍访问扫描工具会将这种情况标记为触摸目标尺寸不合规的问题。

如何保持合适的颜色对比度

颜色对比度指的是前景文本与其背景之间的亮度差异。如果对比度过低,视力不佳的人、色盲患者或年龄较大的用户就很难看清文字内容。

WCAG 2.1规定了以下最低对比度要求(Android的无障碍访问文档将WCAG规定的字号对应为sp单位):

  • 普通文本(字号小于18sp,或加粗后小于14sp):最低对比度应为4.5:1

  • 大号文本(字号等于或大于18sp,或加粗后等于或大于14sp):最低对比度应为3.0:1

  • UI组件及图形对象:最低对比度也应为3.0:1

// 使用Material 3提供的颜色代码(这些组合能够保证足够的对比度)
Text(
    text = "账户概览",
    color = MaterialTheme.colorScheme.onPrimaryContainer,
    modifier = Modifier.background(MaterialTheme.colorScheme.primaryContainer)
)

// 显式设置高对比度颜色(例如,黑色与白色的对比度为21:1)
Text(
    text = "订单已确认",
    color = Color(0xFF1B5E20), // 深绿色
    modifier = Modifier.background(Color(0xFFE8F5E9)) // 浅绿色背景
)

Material 3中的颜色代码(如onPrimaryContainerprimaryContainer的组合)是由Material颜色系统生成的,因此这些颜色在浅色和深色主题下都能保证与其对应背景颜色之间的足够对比度。不过,对于你自行定义的品牌颜色,仍然需要单独检查其对比度是否符合标准。

不应采取的措施(反面示例)

// 错误示例:浅灰色文本显示在纯白色背景上(对比度约为1.6:1,不符合WCAG标准)
Text(
    text = "请阅读条款和条件",
    color = Color(0xFFB0B0B0),
    modifier = Modifier.background(Color.White)
)

在白色背景下使用浅灰色文本,是移动网站及原生应用中常见的无障碍访问违规行为。在明亮的环境光下,视力正常的用户以及视力不佳的用户都无法看清这类文字。

如何提供自定义的可点击标签

当用户使用TalkBack导航到可点击元素时,屏幕阅读器会自动显示默认提示信息,例如“双击即可激活”。

你可以通过在Modifier.clickable中设置onClickLabel参数,来自定义这些提示信息,从而更准确地描述用户操作后的结果。

ListItem(
    headlineContent = { Text("无线网络") },
    supportingContent = { Text("已连接至Office_5G") },
    modifier = Modifier.clickable(
        onClickLabel = "打开无线网络设置"
    ) {
        navigateToWifiSettings()
    }
)

TalkBack会发出提示:“无线网络,已连接至Office_5G,双击可打开无线网络设置。”这样用户就能在采取行动之前清楚地了解操作结果。

不应采取的做法(反模式)

// 错误示例:没有添加语义标记的通用可点击元素
Card(
    modifier = Modifier.Clickable { openInvoiceDetails() }
) {
    Text("发票编号 #4092")
}

TalkBack会提示:“发票编号 #4092,双击即可激活。”但用户并不确定点击后是会支付账单、下载PDF文件,还是打开编辑界面。

如何建立标题层次结构

视觉阅读者会通过浏览大字号、加粗的文本标题来理解屏幕的布局和层次结构。屏幕阅读器的用户同样需要这种结构化的信息。

通过使用heading()这个语义修饰符,你可以将某个文本节点指定为可访问性标题。TalkBack的用户可以将导航模式设置为“标题”,然后通过向上或向下滑动来快速切换不同章节。
Column(modifier = Modifier.padding(16.dp)) {
    // 屏幕标题
    Text(
        text = "安全设置",
        style = MaterialTheme.typography.headlineMedium,
        modifier = Modifier.semantics { heading() }
    )

    Spacer(modifier = Modifier.height(16(dp))

    // 第一节标题
    Text(
        text = "双因素认证",
        style = MaterialTheme.typography.titleMedium,
        modifier = Modifier.semantics { heading() }
    )
    
    // 第一节内容...

    Spacer(modifier = Modifier.height(16(dp))

    // 第二节标题
    Text(
        text = "已连接的设备",
        style = MaterialTheme.typography.titleMedium,
        modifier = Modifier.semantics { heading() }
    )
    
    // 第二节内容...
}

TalkBack的用户可以不用依次浏览众多的选项和描述,而是在几秒钟内直接跳到“已连接的设备”这一部分。

不应采取的做法(反模式)

// 错误示例:仅有视觉上的标题显示,但没有语义标记
Text(
    text = "双因素认证",
    style = MaterialTheme.typography.titleLarge // 视觉上看起来像标题,但TalkBack无法识别它为标题
)
如果没有添加semantics { heading() },TalkBack会将这段文字视为普通的正文内容,用户就无法通过标题来导航。

如何使用“实时区域”功能来通知动态更新

当屏幕上的内容异步更新时(比如计时器倒计时、文件上传完成提示或实时验证信息),有视力的用户能够立即看到变化。

但如果没有将发生变化的部分标记为实时区域,屏幕阅读器的用户就无法知道发生了什么变化。 Compose提供了liveRegion = LiveRegionMode.PoliteliveRegion = LiveRegionMode.Assertive两种选项,可以帮助你实现这一功能。
  • LiveRegionMode.Polite: TalkBack会等待当前的音频提示结束之后再读取更新内容。几乎所有状态更新都可以使用这种模式。

  • LiveRegionMode.Assertive: TalkBack会立即宣布变化,先于其他反馈信息进行播放。这种模式仅适用于那些时间敏感、紧急程度高的警报(例如紧急警告)。

var uploadStatus by remember { mutableStateOf("Ready to upload") }

Text(
    text = uploadStatus,
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Polite
    }
)

// 当上传完成时:
LaunchedEffect(Unit) {
    performUpload()
    uploadStatus = "Upload complete! 12 files saved."
}

一旦uploadStatus发生变化,TalkBack会自动播放:“上传完成!已保存12个文件”,这样视障用户就能及时获得信息,而无需自己去搜索屏幕。

如何支持动态文本缩放功能

视力不佳的用户通常会在Android设置中调整系统字体大小(具体位置取决于设备,可能在显示设置或无障碍设置中)。Android 14及更高版本支持最高200%的非线性字体缩放

为了尊重用户的这一偏好,所有文本尺寸都应使用sp(与屏幕密度无关的像素单位)来定义,而不能使用dp或原始像素单位。

// 正确的做法:使用Material Typography(会自动使用sp单位)
Text(
    text = "仪表盘摘要",
    style = MaterialTheme.typography.titleMedium
)

// 正确的做法:明确指定sp尺寸
Text(
    text = "自定义标签",
    fontSize = 18.sp
)

当用户将字体缩放比例设置为1.5倍时,18sp大小的文字实际上会显示为27sp大小,这样文本就能保证清晰可读。

不应该做什么(反例)

// 错误的做法:将固定的dp尺寸转换为sp单位(不会随系统设置调整)
Text(
    text = "固定大小警告",
    fontSize = with(LocalDensity.current) { 16(dp.toSp() } // 这种方式不会进行缩放!
)

如果使用固定的dp单位来设置文本尺寸,那么当用户调整系统字体大小时,这些文本就不会自动变大,这显然违反了无障碍设计的原则。

高级无障碍技术

对于复杂的高阶应用程序,你可以运用一些高级的无障碍技术来处理更为复杂的用户交互场景。

如何控制元素的遍历顺序和焦点顺序

默认情况下,无障碍功能会按照屏幕元素的实际视觉位置和结构顺序(从上到下、从左到右)来遍历这些元素。在多列布局、财务报表或自定义网格中,这种默认的遍历顺序可能会导致提示信息出现混乱,从而影响用户的使用体验。

您可以使用traversalIndexisTraversalGroup来自定义导航顺序:

Column(
    modifier = Modifier.semantics { isTraversalGroup = true }
) {
    Text(
        text = "步骤1:账户信息",
        modifier = Modifier.semantics { traversalIndex = 1f }
    )
    
    Text(
        text = "步骤3:确认操作",
        modifier = Modifier.semantics { traversalIndex = 3f }
    )
    
    Text(
        text = "步骤2:支付详情",
        modifier = Modifier.semantics { traversalIndex = 2f }
    )
}

代码在内部的工作原理如下:

  1. isTraversalGroup = true设置为“true”会创建一个独立的无障碍访问边界,这样TalkBack在跳转到其他屏幕元素之前,会先读取这个容器内的所有元素。

  2. traversalIndex这个浮点数值用于确定各元素的相对顺序(索引值越小,表示优先级越高)。无论这些元素在布局中的实际位置如何,TalkBack都会按照traversalIndex指定的顺序来读取它们:先读取步骤1,然后是步骤2,最后是步骤3。

如何创建自定义的无障碍访问操作

以一款电商产品卡片为例,这种卡片上通常会有“快速查看”、“加入心愿单”和“加入购物车”等按钮。对于使用屏幕阅读器的用户来说,连续浏览20张卡片并依次点击这三个按钮是一件非常繁琐的事情。

通过使用customActions功能,您可以直接将辅助操作关联到父级卡片节点上。当卡片获得焦点时,TalkBack会告知用户这些操作可用,用户就可以打开TalkBack菜单来选择所需的操作。

var isBookmarked by remember { mutableStateOf(false) }

Card(
    modifier = Modifier
        .fillMaxWidth()
        .semantics {
            customActions = listOf(
                CustomAccessibilityAction(
                    label = if (isBookmarked) "从书签中删除" else "加入书签",
                    action = {
                        isBookmarked = !isBookmarked
                        true // 返回true表示操作已完成
                    }
                ),
                CustomAccessibilityAction(
                    label = "分享文章链接",
                    action = {
                        shareArticle()
                        true
                    }
                )
            )
        }
) {
    // 卡片的视觉内容...
}

当TalkBack用户将焦点放在该卡片上时,系统会告知他们这些自定义操作可用。用户可以通过打开TalkBack菜单或使用无障碍访问手势来查看“加入书签”和“分享文章链接”这两个选项。这样的设计既保持了视觉布局的简洁性,又能为高级用户提供高效、便捷的导航体验。

如何设计可访问的表单输入框和文本字段

对于那些需要被用户轻松使用的表单输入字段,仅仅提供视觉上的占位符是远远不够的。这些字段还需要明确的标签、清晰的输入类型提示,以及合理的键盘导航机制:

var emailValue by remember { mutableStateOf("") } OutlinedTextField( value = emailValue, onValueChange = { emailValue = it }, label = { Text("工作邮箱") }, placeholder = { Text("alex@example.com") }, singleLine = true, keyboardOptions = KeyboardOptions( 键盘类型 = KeyboardType.Email, 输入法动作 = ImeAction.Next ), keyboardActions = KeyboardActions( 下一步操作 = { /* 将焦点移至密码输入框 */ } ), modifier = Modifier .fillMaxWidth() .semantics { 内容描述 = "工作邮箱地址输入框" } )

其内部实现原理如下:

  • label组件能够为用户提供持久的上下文信息,即使用户在输入内容后,这些信息依然会显示出来。
  • 键盘类型 = KeyboardType.Email这一设置会让Android系统显示专为输入邮箱地址设计的键盘界面(其中会包含@和.com等快捷键),同时也会向辅助技术工具表明系统期望用户使用标准的邮箱格式进行输入。
  • 输入法动作 = ImeAction.Next这一设置确保了使用硬件键盘的用户,以及在不同设备之间切换的用户,都能通过按回车键顺利地移动到下一个输入字段。

如何传递动态错误信息

当客户端验证失败时,仅仅将输入框的边框设置为红色是远远不够的。还需要添加语义化的错误提示信息,这样屏幕阅读器就能立即向用户告知验证结果。

var password by remember { mutableStateOf("") } val isPasswordInvalid = password.isNotEmpty() && password.length < 8 OutlinedTextField( value = password, onValueChange = { password = it }, label = { Text("密码") }, isError = isPasswordInvalid, supportingText = { if (isPasswordInvalid) { Text( text = "密码长度必须至少为8个字符", color = MaterialTheme.colorScheme.error ) } }, modifier = Modifier .fillMaxWidth() .semantics { if (isPasswordInvalid) { error("密码长度必须至少为8个字符") } } )

其内部实现原理如下:

isError = isPasswordInvalid这一参数负责控制输入框的视觉样式(如红色边框和错误提示图标)。
  • error("...")这一语义化设置会告诉TalkBack系统将这个输入框视为无效字段,并且会在用户将焦点放在该输入框上时立即显示具体的错误信息。

如何管理进度条与异步加载功能

加载状态必须明确说明任务是处于不确定阶段(正在执行中但持续时间未知),还是处于确定阶段(正在向100%完成度推进)。

// 不确定的进度显示(例如初始的网络请求)
CircularProgressIndicator(
    modifier = Modifier.semantics {
        contentDescription = "正在同步消息,请稍候
    }
)

// 确定的进度显示(例如文件下载)
val downloadProgress = 0.65f // 65%

LinearProgressIndicator(
    progress = { downloadProgress },
    modifier = Modifier.semantics {
        progressBarRangeInfo = ProgressBarRangeInfo(
            current = downloadProgress,
            range = 0f..1f
        )
        contentDescription = "更新文件下载进度:${(downloadProgress * 100).toInt}% 已完成"
    }
)

对于确定性的进度指示器,ProgressBarRangeInfo会向辅助技术系统提供最小值、最大值以及当前值。TalkBack会根据这些信息,在进度增加时定期提供语音提示和触觉反馈。

如何处理可展开与可折叠的内容

折叠菜单及可展开的常见问题解答卡片必须明确显示它们当前是处于打开状态还是关闭状态,以及点击这些元素会触发什么操作。

var isExpanded by remember { mutableStateOf(false) }

Column(modifier = Modifier.fillMaxWidth()) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .fillMaxWidth()
            .clickable(
                onClickLabel = if (isExpanded) "折叠部分" else "展开部分"
            ) {
                isExpanded = !isExpanded
            }
            .semantics {
                stateDescription = if (isExpanded) "已展开" else "已折叠"
            }
            .padding(16.dp)
    ) {
        Text(
            text = "常见问题解答",
            style = MaterialTheme.typography.titleMedium,
            modifier = Modifier.weight(1f)
        )
        Icon(
            imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore,
            contentDescription = null // 仅用于装饰:父级Row元素会显示整体状态
        )
    }

    AnimatedVisibility(visible = isExpanded) {
        Text(
            text = "我们的退款政策允许在购买后30天内退货……",
            modifier = Modifier.padding(16(dp)
        )
    }
}

stateDescription用于说明该部分当前是处于展开状态还是折叠状态,而onClickLabel则明确了点击该元素后会执行的操作。图标上的contentDescription被设置为`null`,以避免重复的提示信息。

如何优化懒加载列表及大型数据集合

当用户浏览长度较长的信息流或列表时,他们需要了解自己当前所处的位置,以及该列表中一共有多少条信息。
val messages = remember { listOf("订单已发货", "配送延迟", "付款已完成") }

LazyColumn(
    modifier = Modifier.semantics {
        contentDescription = "通知列表,共${messages.size}条提醒"
    }
) {
    itemsIndexed(messages) { index, messageText -> 
        Card(
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 4(dp)
                .semantics {
                    contentDescription = "第${index + 1}条通知,内容:$messageText"
                }
        ) {
            Text(
                text = messageText,
                modifier = Modifier.padding(16.dp)
            )
        }
    }
}

TalkBack会显示相应的位置索引(例如“第3条通知:订单已发货”),这样使用屏幕阅读器的用户就能清楚地了解自己在列表中的当前浏览位置。

如何测试和调试可访问性

开发具备良好可访问性的软件需要采用多层次的测试策略:自动化的回归测试、设备上的诊断工具、语义树的检查,以及使用TalkBack进行手动验证。

。 >
测试层次 主要测试工具检测内容适用测试阶段
手动验证 TalkBack屏幕阅读器 真实的非视觉用户体验及手势导航功能 在重大功能更新之前进行
视觉审计 Google可访问性扫描工具 自动检测屏幕对比度设置及触摸目标的相关问题 在功能测试阶段进行
语义树检查 Android Studio布局检查工具 实时查看语义树结构及节点属性 在开发过程中持续进行
自动化测试 Compose UI测试框架(ui-test-junit4) 检测触摸目标描述及内容显示是否出现异常 在持续集成流程中执行

如何使用TalkBack进行手动测试

没有什么比直接使用TalkBack来手动浏览应用程序更有效的方法了。

如何启用并使用TalkBack

  1. 打开设备的设置应用,依次进入可访问性选项,然后选择TalkBack

  2. 将开关切换到开启状态,并同意系统权限请求。(如果系统支持的话,也可以使用音量键快捷方式:长按两个音量键几秒钟即可切换TalkBack状态。)

  3. TalkBack的基本手势操作:

    • 向右滑动:将可访问性焦点移至下一个元素。

    • 向左滑动:将可访问性焦点移至上一个元素。

    • 双击:激活当前聚焦的元素。

    • 两指滑动:用于滚动列表或页面。

    • 三指点击(或先向下滑动再向右滑动):打开TalkBack菜单。

在测试TalkBack功能时需要检查以下内容:

  • 你能否闭着眼睛完成注册、登录、搜索和结账等关键操作?

  • 所有的按钮和交互控件是否都配有清晰且具有意义的名称?

  • 焦点是否会按照合理的顺序移动?

  • 错误信息以及界面状态的变化是否会被自动提示出来?

如何编写自动化测试用例来检测Compose的可访问性问题

你可以将自动化可访问性测试集成到JUnit测试中,这样在持续集成服务器上就能及时发现描述缺失或触摸目标尺寸过小的问题。

build.gradle.kts文件中添加依赖项:

androidTestImplementation("androidx.compose.ui:ui-test-junit4")
androidTestImplementation("androidxcompose.ui:ui-test-manifest")

编写Compose可访问性测试用例:

@RunWith(AndroidJUnit4::class)
class AccessibilityTest {

    @get:Rule
    val composeTestRule = createAndroidComposeRule()
    
    @Test
    fun testLoginButton_hasProperTouchTargetAndLabel() {
        composeTestRule.setContent {
            MaterialTheme {
                Button(
                    onClick = { /* 提交登录请求 */ },
                    modifier = Modifier.minimumInteractiveComponentSize()
                ) {
                    Text("登录")
                }
            }
        }

        // 确认该按钮的存在及其文本和功能描述是否正确
        composeTestRule
            .onNodeWithText("登录")
            .assertExists()
            .assertHasClickAction()
            .assertHeightIsAtLeast(48.dp)
            .assertWidthIsAtLeast(48(dp)
        }
    
    @Test
    fun testIconButton_containsContentDescription() {
        composeTestRule.setContent {
            MaterialTheme {
                IconButton(onClick = {}) {
                    Icon(
                        imageVector = Icons.Default.Favorite,
                        contentDescription = "添加到收藏夹"
                    )
                }
            }
        }

        // 确认图标上的内容描述是否在语义树中得到正确显示
        composeTestRule
            .onNode(hasContentDescription("添加到收藏夹"))
            .assertExists()
            .assertHasClickAction()
        }
    }
}

Google可访问性扫描器的使用指南

Google可访问性扫描器是一款用于检测Android应用程序界面是否满足可访问性要求的工具。

如何安装并配置Google可访问性扫描器

  1. 通过Google Play安装:在测试设备上打开Google Play商店,然后安装由Google LLC开发的Accessibility Scanner应用程序。

  2. 在“无障碍设置”中启用该功能:

    • 依次进入“设置”→“无障碍”→“Accessibility Scanner”。

    • 将开关切换到开启状态,并授予必要的屏幕阅读权限。

  3. 找到浮动按钮:屏幕上会出现一个带有勾选图标(✓)的蓝色浮动按钮。

运行扫描并解析结果

打开你的Android应用程序,导航到想要进行检测的界面。点击蓝色的扫描按钮

然后点击截图图标来分析静态界面内容,或者点击录制按钮来检测多步骤的用户操作流程。

扫描工具会用橙色矩形标出所有存在问题的UI元素。常见的问题包括:交互式触摸目标的大小小于48dp、按钮和图片上缺少标签、文本或图像的对比度不足,以及描述信息重复或缺失等。

点击任何被标记为有问题的元素,即可查看详细的故障说明、相关无障碍设置指南链接,以及推荐的修复步骤。请注意,扫描工具仅用于辅助诊断,并不能保证你的应用程序完全符合无障碍设计标准。

7b7f782d-25f4-4b7a-bfc5-177648b34007

图示:Google Accessibility Scanner正在检测AccessibilityDemo应用程序,标出了尺寸过小的24dp触摸目标,并提供了相应的修复建议。

如何使用Android Studio Layout Inspector检查语义信息

Android Studio的Layout Inspector允许你实时查看正在运行的Compose应用程序的结构,并了解操作系统实际接收到的语义信息。

逐步操作指南

  1. 在模拟器或通过USB调试连接的真实设备上运行你的Compose应用程序。

  2. 在Android Studio中,依次选择“视图”→“工具窗口”→“Layout Inspector”。

  3. 在进程选择下拉列表中,选中你的应用程序对应的进程(例如com.example.accessibilitydemo)。

  4. 在左侧的组件树面板中,导航到想要检查的Composable元素(例如“Semantic Merging”下的Card组件)。

  5. 查看右侧的属性面板。你会看到以下专门用于显示语义信息的区域:

    • 合并后的语义信息:当设置mergeDescendants = true时,该选项会显示无障碍服务能够获取到的所有合并后的语义元数据(包括合并后的ContentDescriptionText列表、OnClick事件信息以及容器相关属性)。

    • 明确声明的语义信息:显示直接与该Composable元素关联的语义属性。

c9d918df-794b-41ff-9771-3ec4b772a5f8

图示:Android Studio的布局检查工具正在检测AccessibilityDemo应用程序。左侧显示组件树结构,中间展示可视化的线框图,右侧则呈现合并后的语义属性信息。

使用布局检查工具可以直观地确认以下内容:

  • semantics(mergeDescendants = true)这一设置能够正确地将不同的子组件组合成一个节点。

  • 装饰性图标已被成功从无障碍访问树中排除。

  • 点击动作(OnClick: AccessibilityAction)以及自定义动作都已正确地关联到相应的组件上。

现实世界中的无障碍UI设计模式

以下是一些可供实际使用的、成熟的常见UI设计模式的实现示例:

完全无障碍的登录表单

表单是用户进行操作的重要入口点。这个实现方案结合了标题元素的语义功能、电子邮件输入验证、动态错误提示,以及合适的触摸目标尺寸设置:

@Composable
fun AccessibleLoginForm(
    onLoginSubmitted: (String, String) -> Unit
) {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    var is Submitted by remember { mutableStateOf(false) }

    val isEmailInvalid = isSubmitted && !android.util.Patterns.email_ADDRESS.matcher(email).matches()
    val isPasswordInvalid = isSubmitted && password.length < 8

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp)
    ) {
        // 1. 标题元素
        Text(
            text = "欢迎回来",
            style = MaterialTheme.typography.headlineLarge,
            modifier = Modifier.semantics { heading() }
        )

        Text(
            text = "登录以访问您的账户",
            style = MaterialTheme.typography.bodyMedium,
            color = MaterialTheme.colorScheme.onSurfaceVariant
        )

        Spacer(modifier = Modifier.height(24.dp))

        // 2. 无障碍设计的电子邮件输入框
        OutlinedTextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("电子邮件地址") },
            isError = isEmailInvalid,
            singleLine = true,
            keyboardOptions = KeyboardOptions(
               _keyboardType = KeyboardType.Email,
                imeAction = ImeAction.Next
            ),
            supportingText = {
                if (isEmailInvalid) {
                    Text(
                        text = "请输入有效的电子邮件地址",
                        color = MaterialTheme.colorScheme.error
                    )
                }
            },
            modifier = Modifier
                .fillMaxWidth()
                .semantics {
                    if (isEmailInvalid) {
                        error("请输入有效的电子邮件地址")
                    }
                }
        )

        Spacer(modifier = Modifier.height(16(dp))

        // 3. 无障碍设计的密码输入框
        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("密码") },
            isError = isPasswordInvalid,
            singleLine = true,
            visualTransformation = PasswordVisualTransformation(),
            keyboardOptions = KeyboardOptions(
               _keyboardType = KeyboardType.Password,
                imeAction = ImeAction.Done
            ),
            supportingText = {
                if (isPasswordInvalid) {
                    Text(
                        text = "密码长度必须至少为8个字符",
                        color = MaterialTheme.colorScheme.error
                    )
                }
            },
            modifier = Modifier
                .fillMaxWidth()
                .semantics {
                    if (isPasswordInvalid) {
                        error("密码长度必须至少为8个字符")
                    }
                }
        )

        Spacer(modifier = Modifier.height(24(dp))

        // 4. 无障碍设计的提交按钮
        Button(
            onClick = {
                isSubmitted = true
                if (!isEmailInvalid && !isPasswordInvalid) {
                    onLoginSubmitted(email, password)
                }
            },
            modifier = Modifier
                .fillMaxWidth()
                .minimumInteractiveComponentSize()
                .semantics {
                    contentDescription = "登录以访问您的账户"
                }
        ) {
            Text("登录")
        }
    }
}

为什么这种模式有效:

  1. 屏幕标题:对于使用TalkBack的应用程序来说,用户一进入该页面就能立即听到“欢迎回来”的提示。

  2. 错误提示机制:如果验证失败,error("...")这一代码会确保在文本字段获得焦点时,TalkBack立刻念出相应的错误提示信息。

  3. 键盘操作引导:ImeAction.NextImeAction.Done这些函数能够帮助用户熟练地使用键盘,在不同的输入字段之间切换。

带有自定义操作的电子商务产品卡片

这种模式展示了如何将mergeDescendants = truecustomActions结合使用,从而创建出简洁且便于高级用户使用的界面:

data class Product(
    val id: String,
    val title: String,
    val priceFormatted: String,
    val rating: Float,
    val imageRes: Int
)

@Composable
fun AccessibleProductCard(
    product: Product,
    onCardClick: () -> Unit,
    onToggleFavorite: () -> Unit,
    onAddToCart: () -> Unit
) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .clickable(onClickLabel = "查看产品详情") { onCardClick() }
            .semantics(mergeDescendants = true) {
                // 将所有描述性信息整合成一条连贯的提示语
                contentDescription = "${product.title}, 价格:${product.priceFormatted},评分:${product.rating}星(满分5星)"

                // 将额外的操作功能设置为自定义的无障碍访问选项
                customActions = listOf(
                    CustomAccessibilityAction("加入购物车") {
                        onAddToCart()
                        true
                    },
                    CustomAccessibilityAction("添加到收藏夹") {
                        onToggleFavorite()
                        true
                    }
                )
            }
    ) {
        Row(
            modifier = Modifier.padding(16(dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Image(
                painter = painterResource(product.imageRes),
                contentDescription = null, // 仅用于装饰,实际内容由上面合并的描述语覆盖
                modifier = Modifier
                    .size(80(dp)
                    .clip(RoundedCornerShape(8	dp))
            )

            Spacer(modifier = Modifier.width(16(dp))

            Column(modifier = Modifier.weight(1f)) {
                Text(
                    text = product.title,
                    style = MaterialTheme.typography.titleMedium
                )
                Text(
                    text = product.priceFormatted,
                    style = MaterialTheme.typography.bodyLarge,
                    fontWeight = FontWeight.Bold
                )
                Text(
                    text = "★ ${product.rating}",
                    style = MaterialTheme.typography.bodySmall,
                    color = MaterialTheme.colorScheme.onSurfaceVariant
                )
            }
        }
    }
}

这种设计模式之所以有效,原因如下:

  1. 统一的语义表达方式:用户无需分别滑动图像视图和文本视图,TalkBack会以一条连贯的文字信息来读取整个卡片内容。

  2. 便捷的操作快捷键:用户可以直接通过TalkBack的操作菜单将商品添加到购物车或收藏列表中,而无需进入商品详情页面。

带有选择状态的标签页导航

这种设计模式展示了如何利用stateDescription及自定义标签页提示信息来实现无障碍标签页导航:

@Composable
fun AccessibleTabNavigation(
    tabs: List,
    selectedTabIndex: Int,
    onTabSelected: (Int) -> Unit
) {
    TabRow(
        selected_Tab = selected_Tab,
        modifier = Modifier.semantics {
            contentDescription = "导航标签页,当前选中的是${tabs[selectedTabIndex]}"
        }
    ) {
        tabs.forEachIndexed { index, title => {
            val isSelected = selectedTabIndex == index
            Tab(
                selected =isSelected,
                onClick = { onTabSelected(index) },
                modifier = Modifier.semantics {
                    role = Role.Tab
                    stateDescription = if (isSelected) "已选中" else "未选中"
                    contentDescription = "$title标签页"
                }
            ) {
                Text(
                    text = title,
                    modifier = Modifier.padding(vertical = 16.dp)
                )
            }
        }
    }
}

这种设计模式之所以有效,原因如下:

  1. 明确的角色标识:通过为每个标签项添加Role.Tab属性,可以告知辅助技术系统这些内容是可点击的标签页。

  2. 选择状态提示:

    stateDescription会在用户点击标签页之前告诉他们该标签页当前是否处于激活状态。

带有操作描述的确认对话框

这种设计模式展示了如何构建一个无障碍的确认对话框:

@Composable
fun AccessibleDeleteConfirmationDialog(
    itemName: String,
    onDismissRequest: () -> Unit,
    onConfirmDelete: () -> Unit
) {
    AlertDialog(
        onDismissRequest = onDismissRequest,
        title = {
            Text(
                text = "是否要删除“$itemName”?此操作不可撤销。",
                style = MaterialTheme.typography.headlineSmall,
                modifier = Modifier.semantics { heading() }
            )
        },
        text = {
            Text("您确定要永久删除“$itemName”吗?")
        },
        confirmButton = {
            TextButton(
                onClick = onConfirmDelete,
                modifier = Modifier.semantics {
                    contentDescription = "确认删除“$itemName”
                }
            ) {
                Text("删除", color = MaterialTheme.colorScheme.error)
            }
        },
        dismissButton = {
            TextButton(
                onClick = onDismissRequest,
                modifier = Modifier.semantics {
                    contentDescription = "取消删除并关闭对话框"
                }
            ) {
                Text("取消")
            }
        },
        modifier = Modifier.semantics {
            contentDescription = "删除项目确认对话框"
        }
    )
}

为什么这种设计模式有效:

  1. 明确的焦点控制:当对话框出现时,TalkBack会自动将焦点限制在对话框的范围内,这样用户就不会不小心点击到背景中的元素。

  2. 内容丰富的按钮说明:按钮上的描述会明确说明点击该按钮后会产生的具体后果(例如“确认删除购物清单”,而不仅仅是“删除”)。

无障碍访问性审核检查表

在将您的应用发布到生产环境或提交给应用商店之前,请先仔细核对这份无障碍访问性审核检查表:

视觉设计与排版

  • 可伸缩的文本:所有字体大小都应使用sp单位来定义(切勿使用固定的dp单位),这样文本就可以在放大200%的情况下依然保持清晰的显示效果。

  • 色彩对比度:普通文本的色彩对比度应至少达到4.5:1,而大字体及重要UI组件的色彩对比度则应达到3.0:1,以确保它们与背景之间的对比清晰。

  • 颜色的独立性:切勿仅通过颜色来传达信息。始终应将颜色提示与文字说明或明显的图标结合起来使用。

交互式元素与触摸目标

  • 触摸目标的尺寸:使用Modifier.minimumInteractiveComponentSize()确保所有可点击的交互式组件的最小触摸目标尺寸为48dp × 48dp

  • 操作性的描述:在所有的功能性图标按钮和交互式图像上,都应添加描述性强的、与具体操作相关的contentDescription文本。

  • 装饰性元素:对于纯粹用于装饰的图标和插图,应将contentDescription设置为null,这样TalkBack提供的反馈信息就会更加简洁明了。

  • 点击操作的提示:在卡片、列表项以及自定义按钮上,添加相应的onClickLabel参数,以便用户在点击前能够清楚地了解操作结果。

屏幕结构与导航设计

  • 无障碍访问性标题:使用Modifier.semantics { heading() }为主要的章节标题和屏幕页眉添加标记,以便用户能够快速定位到相应的内容。

  • 语义合并:使用Modifier.semantics(mergeDescendants = true)将相关的视觉元素组合在一起显示。

  • 实时更新提示:对于异步发生的UI变化,应使用liveRegion = LiveRegionMode.Polite进行标记,这样TalkBack就会在这些变化发生时及时向用户发出提示。

  • 表单优化:在所有的文本输入框上,都要设置合适的keyboardTypeimeAction属性。

  • 验证反馈:使用Modifier.semantics { error("...") }来显示输入错误状态,以便用户能够及时了解到自己的操作是否正确。

测试与验证

  • 自动化单元测试:在持续集成过程中添加Compose可访问性检测功能,以发现触摸目标设置错误或标签缺失等问题。

  • 设备端诊断审计:使用Google可访问性扫描工具检查应用程序的所有关键界面,排查对比度或字体大小等方面的缺陷。

  • 语义树分析:利用Android Studio的布局检查工具,验证代码中定义的语义信息及可访问性设置是否正确。

  • 手动屏幕阅读器测试:在启用TalkBack功能的情况下,完成从登录到结账、导航等完整的用户使用流程测试。

结论与后续步骤

在使用Jetpack Compose开发可访问性应用程序时,不能仅仅在项目后期才对代码进行修改。你需要了解Compose的双树架构,并合理设计用户界面,确保视觉元素与语义数据能够准确传达应用程序的意图。

通过应用语义属性、保证触摸目标的大小为48dp、维持符合WCAG标准的对比度比,以及使用TalkBack和Google可访问性扫描工具进行验证,你就能确保自己的Android应用程序对全球所有用户来说都是友好、合规且易于使用的。

重要资源

若想进一步深入了解Android的可访问性功能,请参考以下这些重要资料:

相关文章

技术实践

如何利用WCAG 2.2标准打造更加易于访问的网站

一个网站可能看起来很专业,使用鼠标操作时也能运行得非常顺畅,但对某些用户来说,使用它仍然会遇到困难。 有时,某个表单会仅通过颜色来提示用户出现了问题;固定的页眉可能会完全覆盖当前处于键盘焦点位置的元素;登录表单可能会阻止用户从密码管理工具中复制密码并粘贴到表单中;而某个自定义按钮,用鼠标点击时可以正常使用,但当人们使用键盘操作时却毫无反应。 这些其实都是开发过程中做出的决策,并非只有在进行无障碍性审核时才会出现的问题。 《Web内容无障碍性指南》为识别和消除这些障碍提供了统一的标准。WCAG 2.2是最新的WCAG 2建议标准,世界万维网联盟也建议开发人员和各类组织在可能的情况下使用WCAG

阅读全文
技术实践

如何使用Python构建一个人工智能文件分析工具

如果你曾经打开过一份30页的PDF文件,然后心想“我绝对不可能读完这一切”,那么你就已经理解了为什么文件分析人工智能工具会非常有用。 想象一下,当你上传一篇研究论文、简历、CSV文件、商业报告或PDF文档后,只需简单地问这样一个问题: “其中最重要的发现是什么?” 人工智能工具无需你手动浏览整个文件,就能理解文件的内容,并回答相关问题。 在本教程中,我们正是要构建这样的工具。我们将使用Python编写一个适合初学者的 AI文件分析工具 ,它能够: 从你的电脑中接收文件 将文件上传到人工智能模型中 读取文件的内容 理解自然语言提出的问题 分析文件 给出有用的答案 处理各种类型的问题,而无需我们为

阅读全文
技术实践

如何使用 Shadcn UI 在 React 中构建可扩展的客户身份验证及入职流程

任何具有合规性要求的B2B SaaS产品(比如涉及银行业务、贷款服务、工资发放或加密货币相关的应用)在开发初期都会遇到同样的问题:在允许企业使用你的平台之前,你必须先核实他们的身份。 这意味着需要收集企业的类型信息、审核他们的注册文件,并向用户展示他们的验证进度,但整个流程不能让人感觉像是在填写繁琐的海关表格一样。 本文详细介绍了如何利用Shadcn UI构建一个功能完备的三步客户身份验证流程:包括用于显示操作进度的步骤提示组件、用于选择账户类型的单选组、用于上传文件的区域,以及用于显示验证状态的警告提示。你会看到实际的代码实现,而不仅仅是简化后的示例代码,同时也会了解到每个设计决策背后的理由

阅读全文
技术实践

如何使用针对用户的OAuth访问机制来构建人工智能代理程序【完整手册】

当你的AI代理同时为多个人提供服务时,每一次工具调用都必须明确:该代理究竟是在代表哪位用户行事。让我们通过构建一个能够与Slack和GitHub连接的AI代理来学习如何解决这个问题。 当使用Slack时,系统会使用 해당用户的 workspace;而在GitHub上创建问题时,也会以该用户的身份在其有权访问的仓库中操作。虽然代理可能会犯错,但它绝对不能使用错误用户的权限来进行操作。 解决这个问题的方法分为两个部分,而这两个部分都在本教程的前半部分进行了讲解: 每位用户都需要单独授权。 Alice为自己授权Slack,Bob也为自己授权Slack。 代理传递的是标识符,而不是令牌。 像 alic

阅读全文