API 默认值

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

针对适当用途使用适当的 API 通常意味着组件附带涵盖标准使用情形的预定义无障碍功能行为,但请务必仔细检查这些默认行为是否符合您的无障碍需求。如果不行,Compose 还提供了一些方法来满足更具体的要求。

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

最小触摸目标尺寸

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

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

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

一个采用默认内边距的复选框,宽度和高度均为 48 dp。
图 1. 带有默认内边距的复选框。

onCheckedChange 参数设为 null 时,将不会添加内边距,因为用户无法直接与该组件互动。

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

没有内边距的复选框。
图 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)
        }
    }
}

选中和取消选中“选项”文本旁边的复选框。
图 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)
        )
    }
}

一个非常小的可点击框,点按该框旁边即可将其扩展为更大的触摸目标。
图 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)
        )
    }
}

上例中非常小的方框的大小已增加,以创建更大的触摸目标。
图 5. 更大的方形触摸目标。

图形元素

在定义 ImageIcon 可组合项时,Android 框架无法自动了解应用显示的内容。您需要传递图形元素的文字性说明。

假设有一个屏幕,用户可以通过这个屏幕与朋友分享当前页面。此屏幕包含一个可点击的分享图标:

一排包含四个可点击图标的条状图标,其中突出显示了“分享”图标。
图 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 默认会合并其子元素的语义,而不是将其视为个体,以便无障碍服务可以相应地对其进行分组:

采用未合并和合并的子级语义的按钮。
图 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 提示“Double tap and hold to”,后跟标签:

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 修饰符,因为 Foundation 修饰符包含开箱即用的无障碍服务注意事项,以及本部分介绍的现有 Compose 实践。

您还可以在“清除和设置语义”部分中找到自定义切换开关组件的示例,并在 API 指南中详细了解如何在自定义组件中支持无障碍功能。