API 默认设置

Material、Compose UI 和 Foundation API 默认实现并提供了许多无障碍实践。它们包含内置语义,这些语义遵循其特定角色和功能,这意味着大多数无障碍功能支持只需很少甚至无需额外工作即可提供。

为适当的目的使用适当的 API 通常意味着组件带有预定义的无障碍行为,可涵盖标准用例,但请记住仔细检查这些默认设置是否符合您的无障碍需求。如果不是,Compose 还提供了满足更具体要求的方法。

了解 Compose API 中的默认无障碍语义和模式有助于理解如何在使用它们时考虑无障碍功能,以及如何支持更自定义组件中的无障碍功能。

最小触摸目标尺寸

屏幕上任何用户可以点击、触摸或互动able的元素都应足够大,以确保可靠的互动。在调整这些元素的大小时,请确保将最小尺寸设置为 48dp,以正确遵循Material Design 无障碍指南

Material 组件(例如 CheckboxRadioButtonSwitchSliderSurface)在内部设置此最小尺寸,但仅当组件可以接收用户操作时。例如,当 CheckboxonCheckedChange 参数设置为非 null 值时,复选框会包含内边距,使其宽度和高度至少为 48 dp。

@Composable
private fun CheckableCheckbox() {
    Checkbox(checked = true, onCheckedChange = {})
}

A checkbox with the default padding with a width and height of 48 dp.
图 1. 带默认内边距的复选框。

onCheckedChange 参数设置为 null 时,不包含内边距,因为无法直接与组件互动。

@Composable
private fun NonClickableCheckbox() {
    Checkbox(checked = true, onCheckedChange = null)
}

A checkbox that has no padding.
图 2. 不带内边距的复选框。

在实现 SwitchRadioButtonCheckbox 等选择控件时,通常通过将可组合项上的点击回调设置为 null,并将 toggleableselectable 修饰符添加到父级可组合项来将可点击行为提升到父级容器。

@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)
        }
    }
}

A checkbox next to the text 'Option' that is being selected and deselected.
图 3. 具有可点击行为的复选框。

当可点击可组合项的大小小于最小触摸目标尺寸时,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)
        )
    }
}

A very small clickable box that is expanded to a larger touch target by tapping next to the box.
图 4. 扩展到更大触摸目标的非常小的可点击框。

为了防止不同可组合项的触摸区域之间可能发生重叠,请始终为可组合项使用足够大的最小尺寸。在此示例中,这意味着使用 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)
        )
    }
}

The very small box from the previous example is increased in size to create a larger touch target.
图 5. 更大的框触摸目标。

图形元素

当您定义 ImageIcon 可组合项时,Android 框架无法自动理解应用正在显示什么。您需要传递图形元素的文本描述。

想象一下这样一个屏幕:用户可以与朋友分享当前页面。此屏幕包含一个可点击的分享图标。

A strip of four clickable icons, with the 'share' icon highlighted.
图 6. 一排可点击图标,其中“分享”图标处于选中状态。

仅凭图标,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 主要用于图形元素,例如图像。Material 组件(例如 ButtonText)和可操作行为(例如 clickabletoggleable)带有其他预定义的语义,这些语义描述了它们的固有行为,并且可以通过其他 Compose API 进行更改。

互动元素

Material 和 Foundation Compose API 通过 clickabletoggleable 修饰符 API 使界面元素可供用户互动。由于可互动组件可能包含多个元素,因此 clickabletoggleable 默认会合并其子元素的语义,以便将组件视为一个逻辑实体。

例如,一个 Material Button 可能包含一个子图标和一些文本。Material Button 默认会合并其子元素的语义,而不是将子元素视为单独的个体,以便无障碍服务可以相应地对其进行分组。

Buttons with unmerged versus merged children semantics.
图 7. 具有未合并和已合并子元素语义的按钮。

类似地,使用 clickable 修饰符还会导致可组合项将其后代语义合并到一个实体中,并以相应的操作表示形式发送到无障碍服务。

Row(
    // Uses `mergeDescendants = true` under the hood
    modifier = Modifier.clickable { openArticle() }
) {
    Icon(
        painter = painterResource(R.drawable.ic_logo),
        contentDescription = "Open",
    )
    Text("Accessibility in Compose")
}

您还可以在父级可点击项上设置特定的 onClickLabel,以便向无障碍服务提供额外信息,并提供更完善的操作表示形式。

Row(
    modifier = Modifier
        .clickable(onClickLabel = "Open this article") {
            openArticle()
        }
) {
    Icon(
        painter = painterResource(R.drawable.ic_logo),
        contentDescription = "Open"
    )
    Text("Accessibility in Compose")
}

以 TalkBack 为例,此 clickable 修饰符及其点击标签将使 TalkBack 能够提供“双击以打开本文”的操作提示,而不是更通用的默认反馈“双击以激活”。

此反馈会根据操作类型而变化。长按会提供 TalkBack 提示“双击并按住以”,后跟一个标签。

Row(
    modifier = Modifier
        .combinedClickable(
            onLongClickLabel = "Bookmark this article",
            onLongClick = { addToBookmarks() },
            onClickLabel = "Open this article",
            onClick = { openArticle() },
        )
) {}

在某些情况下,您可能无法直接访问 clickable 修饰符(例如,当它设置在较低的嵌套层中时),但仍想更改默认的通知标签。为此,请通过使用 semantics 修饰符并在此处设置点击标签来修改操作表示形式,从而将 clickable 的设置与修改通知分开。

@Composable
private fun ArticleList(openArticle: () -> Unit) {
    NestedArticleListItem(
        // Clickable is set separately, in a nested layer:
        onClickAction = openArticle,
        // Semantics are set here:
        modifier = Modifier.semantics {
            onClick(
                label = "Open this article",
                action = {
                    // Not needed here: openArticle()
                    true
                }
            )
        }
    )
}

在这种情况下,您无需两次传递点击操作,因为现有的 Compose API(例如 clickableButton)会为您处理此问题。这是因为合并逻辑可确保使用最外层修饰符标签和操作来获取存在的信息。

在前面的示例中,openArticle() 点击操作由 NestedArticleListItem 自动深层传递到其 clickable 语义中,并且可以在第二个语义修饰符操作中保留为 null。但是,点击标签取自第二个语义修饰符 onClick(label = \"Open this article\"),因为它在第一个中不存在。

您可能会遇到期望子语义合并到父语义中但未发生的情况。如需了解更多深入信息,请参阅合并和清除

自定义组件

对于自定义组件,通常的经验法则是,查看 Material 库或其他 Compose 库中类似组件的实现,并在合理的情况下模仿或修改其无障碍行为。

例如,如果您要用自己的实现替换 Material Checkbox,查看现有的 Checkbox 实现会提醒您添加 triStateToggleable 修饰符,该修饰符处理此组件的无障碍属性。

此外,请大量使用 Foundation 修饰符,因为它们开箱即用地包含了无障碍功能注意事项,以及本节中介绍的现有 Compose 实践。

您还可以在清除和设置语义部分中找到自定义切换组件的示例,以及在API 指南中找到有关如何在自定义组件中支持无障碍功能的更详细信息。