为了帮助有无障碍功能需求的用户成功使用您的应用,请在设计应用时充分考虑关键的无障碍功能要求。
考虑最小触摸目标尺寸
屏幕上可供用户点击、触摸或可与用户互动的所有元素都应足够大,让用户能够进行可靠的互动。调整这些元素的大小时,请务必将最小尺寸设置为 48dp,以正确遵循 Material Design 无障碍指南。
Material 组件(如 Checkbox
、RadioButton
、Switch
、Slider
和 Surface
)可在内部设置此最小尺寸,但仅限该组件可以接收用户操作时。例如,当 Checkbox
的 onCheckedChange
参数设置为非 null 值时,该复选框将添加内边距,使宽度和高度至少为 48 dp。
@Composable private fun CheckableCheckbox() { Checkbox(checked = true, onCheckedChange = {}) }
当 onCheckedChange
参数设置为 null 时,系统不会添加内边距,因为无法直接与组件互动。
@Composable private fun NonClickableCheckbox() { Checkbox(checked = true, onCheckedChange = null) }
在实现 Switch
、RadioButton
或 Checkbox
等选择控件时,您通常需要将可点击行为提升到父级容器,将可组合项上的点击回调设置为 null
,并向父级可组合项添加 toggleable
或 selectable
修饰符。
@Composable private fun CheckableRow() { MaterialTheme { var checked by remember { mutableStateOf(false) } Row( Modifier .toggleable( value = checked, role = Role.Checkbox, onValueChange = { checked = !checked } ) .padding(16.dp) .fillMaxWidth() ) { Text("Option", Modifier.weight(1f)) Checkbox(checked = checked, onCheckedChange = null) } } }
当可点击可组合项的尺寸小于最小触摸目标尺寸时,Compose 仍会增加触摸目标尺寸。为此,它会将触摸目标尺寸扩展到可组合项的边界之外。
以下示例包含一个非常小的可点击 Box
。触摸目标区域会自动扩展到 Box
的边界之外,因此点按 Box
旁边的区域仍会触发点击事件。
@Composable private fun SmallBox() { var clicked by remember { mutableStateOf(false) } Box( Modifier .size(100.dp) .background(if (clicked) Color.DarkGray else Color.LightGray) ) { Box( Modifier .align(Alignment.Center) .clickable { clicked = !clicked } .background(Color.Black) .size(1.dp) ) } }
为防止不同可组合项的触摸区域之间可能重叠,请始终为可组合项使用足够大的最小尺寸。在本例中,这意味着使用 sizeIn
修饰符设置内部框的最小尺寸:
@Composable private fun LargeBox() { var clicked by remember { mutableStateOf(false) } Box( Modifier .size(100.dp) .background(if (clicked) Color.DarkGray else Color.LightGray) ) { Box( Modifier .align(Alignment.Center) .clickable { clicked = !clicked } .background(Color.Black) .sizeIn(minWidth = 48.dp, minHeight = 48.dp) ) } }
添加点击标签
您可以使用点击标签为可组合项的点击行为添加语义。点击标签描述了用户与可组合项互动时发生的情况。无障碍服务使用点击标签来向有特定需求的用户描述应用。
通过在 clickable
修饰符中传递参数来设置点击标签:
@Composable private fun ArticleListItem(openArticle: () -> Unit) { Row( Modifier.clickable( // R.string.action_read_article = "read article" onClickLabel = stringResource(R.string.action_read_article), onClick = openArticle ) ) { // .. } }
或者,如果您无权访问可点击修饰符,请在 semantics 修饰符中设置点击标签:
@Composable private fun LowLevelClickLabel(openArticle: () -> Boolean) { // R.string.action_read_article = "read article" val readArticleLabel = stringResource(R.string.action_read_article) Canvas( Modifier.semantics { onClick(label = readArticleLabel, action = openArticle) } ) { // .. } }
描述视觉元素
在定义 Image
或 Icon
可组合项时,Android 框架无法自动理解应用显示的内容。您需要传递视觉元素的文字说明。
假设有一个屏幕,用户可以通过这个屏幕与朋友分享当前页面。此屏幕包含一个可点击的分享图标:
仅根据图标,Android 框架无法向视障用户描述该图标。Android 框架需要图标的额外文本说明。
contentDescription
参数描述视觉元素。请使用已本地化的字符串,因为它对用户可见。
@Composable private fun ShareButton(onClick: () -> Unit) { IconButton(onClick = onClick) { Icon( imageVector = Icons.Filled.Share, contentDescription = stringResource(R.string.label_share) ) } }
某些视觉元素纯粹只是装饰,您可能不想向用户传达这些元素。如果将 contentDescription
参数设置为 null
,即表示您向 Android 框架指明此元素没有关联的操作或状态。
@Composable private fun PostImage(post: Post, modifier: Modifier = Modifier) { val image = post.imageThumb ?: painterResource(R.drawable.placeholder_1_1) Image( painter = image, // Specify that this image has no semantic meaning contentDescription = null, modifier = modifier .size(40.dp, 40.dp) .clip(MaterialTheme.shapes.small) ) }
给定视觉元素是否需要 contentDescription
由您决定。询问自己该元素是否传达了用户执行其任务所需的信息。如果不能,最好去除说明。
合并元素
借助 TalkBack 和开关控制等无障碍服务,用户可以在屏幕上的各个元素之间移动焦点。请务必以正确的粒度聚焦元素。当屏幕中的每个低级别可组合项都独立获得焦点时,用户必须进行大量互动才能在屏幕上移动。如果元素过于激进地合并在一起,用户可能不知道哪些元素是一起的
将 clickable
修饰符应用于可组合项时,Compose 会自动合并该可组合项包含的所有元素。这同样适用于 ListItem
;列表项中的元素会合并在一起,无障碍服务也会将它们视为一个元素。
可能会有这样一组可组合项:它们组成了一个逻辑组,但该逻辑组不可点击,也不是列表项的组成部分。您仍然希望无障碍服务将这些元素视为一个元素。例如,假设有一个可组合项,它会显示用户的头像、姓名和一些额外信息:
您可以在 semantics
修饰符中使用 mergeDescendants
参数,让 Compose 能够合并这些元素。这样,无障碍服务仅会选择合并后的元素,并且后代的所有语义属性都会合并。
@Composable private fun PostMetadata(metadata: Metadata) { // Merge elements below for accessibility purposes Row(modifier = Modifier.semantics(mergeDescendants = true) {}) { Image( imageVector = Icons.Filled.AccountCircle, contentDescription = null // decorative ) Column { Text(metadata.author.name) Text("${metadata.date} • ${metadata.readTimeMinutes} min read") } } }
无障碍服务现在可同时专注于整个容器,合并其内容:
添加自定义操作
我们来看看以下列表项:
当您使用 Talkback 等屏幕阅读器听取屏幕上显示的内容时,它会先选择整个项,然后选择书签图标。
在长列表中,此操作可能需要反复执行多次。更好的方法是定义允许用户为项添加书签的自定义操作。请注意,您还必须明确移除书签图标本身的行为,以确保无障碍服务不会选择该图标。这是使用 clearAndSetSemantics
修饰符实现的:
@Composable private fun PostCardSimple( /* ... */ isFavorite: Boolean, onToggleFavorite: () -> Boolean ) { val actionLabel = stringResource( if (isFavorite) R.string.unfavorite else R.string.favorite ) Row( modifier = Modifier .clickable(onClick = { /* ... */ }) .semantics { // Set any explicit semantic properties customActions = listOf( CustomAccessibilityAction(actionLabel, onToggleFavorite) ) } ) { /* ... */ BookmarkButton( isBookmarked = isFavorite, onClick = onToggleFavorite, // Clear any semantics properties set on this node modifier = Modifier.clearAndSetSemantics { } ) } }
描述元素的状态
可组合项可以为语义定义 stateDescription
,Android 框架使用该语义来读取可组合项所处的状态。例如,可切换的可组合项可以处于“已选中”或“未选中”状态。在某些情况下,您可能需要替换 Compose 使用的默认状态说明标签。为此,您可以在将可组合项定义为可切换的可组合项之前,明确指定状态说明标签:
@Composable private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) { val stateSubscribed = stringResource(R.string.subscribed) val stateNotSubscribed = stringResource(R.string.not_subscribed) Row( modifier = Modifier .semantics { // Set any explicit semantic properties stateDescription = if (selected) stateSubscribed else stateNotSubscribed } .toggleable( value = selected, onValueChange = { onToggle() } ) ) { /* ... */ } }
定义标题
应用有时会在一个屏幕上的可滚动容器中显示大量内容。例如,一个屏幕可以显示用户正在阅读的某篇文章的完整内容:
有无障碍需求的用户难以浏览此类屏幕。为了协助导航,请指明哪些元素是标题。在前面的示例中,每个子部分标题都可以定义为无障碍标题。某些无障碍服务(如 Talkback)允许用户直接从标题导航到标题。
在 Compose 中,您可以通过定义可组合项的 semantics
属性来指示可组合项是标题:
@Composable private fun Subsection(text: String) { Text( text = text, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.semantics { heading() } ) }
处理自定义可组合项
每当将应用中的某些 Material 组件替换为自定义版本时,都必须牢记无障碍功能注意事项。
假设您要将 Material Checkbox
替换为您自己的实现。您可能会忘记添加 triStateToggleable
修饰符,用于处理此组件的无障碍属性。
一般来讲,请在 Material 库中查看该组件的实现,并模拟您能找到的任何无障碍行为。此外,应大量使用 Foundation 修饰符(而不是界面级修饰符),因为 Foundation 修饰符包含开箱即用的无障碍功能注意事项。
使用多个无障碍服务测试自定义组件实现,以验证其行为。
其他资源
- 无障碍功能:所有 Android 应用开发通用的基本概念和技术
- 构建无障碍应用:为了让您的应用使用起来更没有障碍,您可以采取哪些关键步骤
- 改进应用无障碍功能的原则:在努力让应用使用起来更没有障碍时,应牢记的主要原则
- 测试无障碍功能:Android 无障碍功能的测试原则和工具