改进 Compose 无障碍功能的关键步骤

为了帮助有无障碍需求的用户成功使用您的应用,请设计您的 来满足关键的无障碍要求

考虑最小触摸目标尺寸

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

Material 组件,例如 CheckboxRadioButtonSwitch SliderSurface - 在内部设置此最小尺寸,但仅限 该组件可以接收用户操作的时间。例如,当 Checkbox 的 其 onCheckedChange 参数设置为非 null 值,则该复选框会包含 内边距,宽度和高度至少为 48 dp。

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

onCheckedChange 参数设置为 null 时,内边距不会 因为无法直接与该组件交互。

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

图 1.无内边距的复选框。

在实现 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)
        }
    }
}

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

描述视觉元素

定义 ImageIcon 可组合项时,没有任何 使 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;列表项中的元素合并在一起,而无障碍功能 服务将其视为一个元素。

可能会有这样一组可组合项:它们组成了一个逻辑组,但该逻辑组不可点击,也不是列表项的组成部分。你仍需要使用无障碍功能 来将它们视为一个元素例如,假设有一个可组合项, 会显示用户的头像、姓名和一些额外信息:

一组包含用户名的界面元素。选择名称。

您可以使用 mergeDescendants 让 Compose 合并这些元素 参数(位于 semantics 修饰符中)。这样,无障碍服务 仅选择合并的元素以及后代的所有语义属性 会合并在一起

@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 修饰符, 该组件的无障碍属性

一般来讲,您可以在 并模仿您可以发现的任何无障碍行为。 此外,大量使用 Foundation 修饰符,而不是界面级修饰符 修饰符,因为这些因素包括开箱即用的无障碍功能注意事项。

使用多个对象测试您的自定义组件实现 服务来验证其行为

其他资源