改进应用无障碍功能要遵循的原则

为了帮助有无障碍功能需求的用户,Android 框架允许您创建无障碍服务,该服务可以向用户呈现应用中的内容,还可以代表用户操作应用。

Android 提供了一些系统无障碍服务,其中包括:

为了帮助具有无障碍功能需求的用户顺利使用您的应用,您的 应用必须遵循本页中介绍的最佳做法,这些做法基于 在让应用使用起来更没有 障碍中所述的准则。

以下各部分介绍的每项最佳做法都可以进一步改进应用的无障碍功能:

标签元素
用户必须能够理解应用中每个有意义的互动界面元素的内容和用途。
添加了 无障碍操作
通过添加无障碍操作,您可以让无障碍服务的用户在应用中完成关键用户体验流程。
使用内置无障碍功能
Compose 默认提供许多无障碍行为。利用预定义的无障碍行为,只需很少或无需额外工作即可让组件使用起来没有障碍。Compose 还提供了支持默认功能未涵盖的更具体无障碍功能要求的方法。
使用除颜色之外的提示
用户必须能够清楚地区分界面中各类元素。为此,除了颜色之外,还应使用图案和位置表示这些差异。
让媒体内容使用起来更没有障碍
在应用的视频或音频内容中添加说明,这样使用这些内容的用户就无需完全依靠视觉和听觉提示。

标签元素

请务必针对应用中的每个互动界面元素,为用户提供实用且具有描述性的标签。每个标签都必须说明特定元素的语义,即元素的含义和用途。TalkBack 等屏幕阅读器可以向用户读出这些标签。

在大多数情况下,Compose API 和 Material 默认提供 无障碍支持。不过,如果您需要手动指定界面元素的语义属性,请使用 semantics 修饰符和 contentDescription 属性。如需详细了解语义,请参阅 语义

以下各部分介绍了其他几种标签技术。

可修改的元素

为可修改的元素(如文本字段)添加标签时,除了让相应的示例文字可由屏幕阅读器读出之外,最好还在元素本身中显示这些文字,以列举有效输入的示例。在这些情况下,您可以使用占位文本,也称为提示文本。

在以下示例中,TextField 具有提供提示文本的 placeholder 参数。

val usernameState = rememberTextFieldState()
TextField(
    state = usernameState,
    lineLimits = TextFieldLineLimits.SingleLine,
    placeholder = { Text("Enter Username") }
)

此外,文本字段通常还具有相应的描述性标签,用于说明用户必须输入的内容。

在以下示例中,TextField 具有提供无障碍功能说明的 label 参数。

TextField(
    state = rememberTextFieldState(initialText = "Hello"),
    label = { Text("Label") }
)

如需详细了解文本和用户输入,请参阅配置文本字段

集合中的元素

为集合的元素添加标签时,每个标签都必须是唯一的。 这样,系统的无障碍服务在读出标签时就可以特指屏幕上的 1 个元素。这种对应关系可让用户知道何时在界面中循环浏览,或何时将焦点移至他们已发现的元素。

例如,如果您有 LazyColumnLazyRow,请使用 semantics 修饰符为每个项分配唯一的 collectionItemInfo,如以下代码段所示:

MilkyWayList(
    modifier = Modifier
        .semantics {
            collectionInfo = CollectionInfo(
                rowCount = milkyWay.count(),
                columnCount = 1
            )
        }
) {
    milkyWay.forEachIndexed { index, text ->
        Text(
            text = text,
            modifier = Modifier.semantics {
                collectionItemInfo =
                    CollectionItemInfo(index, 0, 0, 0)
            }
        )
    }
}

如需详细了解列表和网格的语义属性,请参阅 列表和项信息

相关内容组

如果应用显示的多个界面元素构成一个自然组(如歌曲的详细信息或消息的属性),应将这些元素整理到一个父容器中(如 ColumnRowBox)。使用父容器的 semantics 修饰符将 mergeDescendants 设置为 true

这样,无障碍服务就可以在单次语音中逐个读出内部元素的内容说明。这样整合相关元素有助于采用辅助技术的用户更有效地发现屏幕上的信息。

在以下代码段中,Row 可组合项充当父容器。 在 Row 中,相关元素显示了博文的元数据,包括作者的头像、作者的姓名和预计阅读时间。 将 mergeDescendants 设置为 true 会将这些内部元素分组,以便无障碍服务将它们视为一个单元。

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

对相关元素进行分组时(如上例所示),请仅使父容器具有互动性。避免向内部子元素添加 clickablefocusable 修饰符。而应将修饰符应用于父级 RowColumn

由于无障碍服务在单次语音中读出内部元素的说明,因此务必确保每条说明都简明扼要地传达出元素的含义。

注意 :一般来说,为群组创建内容说明时,请避免汇总其子项的文本。这样做会使群组的说明变得脆弱,并且当子项的文本发生变化时,群组的说明可能不再与可见文本匹配。

在列表或网格上下文中,屏幕阅读器可能会合并列表或网格元素的子文本节点的文本。最好避免修改此通知。

如需详细了解如何合并语义,请参阅合并和清除

文字中的标题

某些应用使用标题总结屏幕上显示的多组文字。 如果特定的元素表示一个标题,您可以通过在 semantics 修饰符中设置 heading 属性,表明它的无障碍服务用途。

@Composable
private fun Subsection(text: String) {
    Text(
        text = text,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier.semantics { heading() }
    )
}

无障碍服务的用户可以选择浏览标题,而不是浏览段落或字词。这种灵活性可改善文字浏览体验。

如需详细了解 heading 语义属性,请参阅 标题

无障碍窗格标题

在 Android 9(API 级别 28)及更高版本中,您可以为屏幕的窗格提供使用起来没有障碍的标题。 出于无障碍功能方面的考虑,窗格是窗口中视觉上不同的部分。

为了让无障碍服务能够理解与窗口行为类似的窗格行为,请为应用的窗格指定描述性标题。这样一来,当窗格的外观或内容发生变化时,无障碍服务就可以为用户提供更精细的信息。

ShareSheet(
    message = "Choose how to share this photo",
    modifier = Modifier
        .fillMaxWidth()
        .align(Alignment.TopCenter)
        .semantics { paneTitle = "New bottom sheet" }
)

如需详细了解 paneTitle 语义属性,请参阅 与窗口类似的组件

装饰性元素

如果界面中某个元素的存在只是为了让内容看起来间距合理或布局美观,请在该元素上设置适当的属性,以表明无障碍服务可以忽略它。

对于 ImageIcon 可组合项,请设置 contentDescription = null。对于其他纯粹的装饰性元素(不提供任何上下文或功能),您可以使用 hideFromAccessibility。此语义属性会告知无障碍服务忽略该项。

如果互动可组合项包含装饰性、非互动的子元素,请使用 clearAndSetSemantics 确保无障碍服务不会遍历它们。请注意,clearAndSetSemantics 会完全清除元素及其子元素的默认语义。这样,您就可以定义新的统一无障碍元素。通常,您会对复杂的自定义组件使用此方法。

在以下示例中,IconText 是自定义切换开关内的装饰性子元素。为了防止无障碍服务单独遍历这些子元素,您可以使用父级 Row 上的 clearAndSetSemantics 清除它们的语义。这样,无障碍服务就会将整个 Row 视为可遍历的切换开关:

// Developer might intend this to be a toggleable.
// Using `clearAndSetSemantics`, on the Row, a clickable modifier is applied,
// a custom description is set, and a Role is applied.

@Composable
fun FavoriteToggle() {
    val checked = remember { mutableStateOf(true) }
    Row(
        modifier = Modifier
            .toggleable(
                value = checked.value,
                onValueChange = { checked.value = it }
            )
            .clearAndSetSemantics {
                stateDescription = if (checked.value) "Favorited" else "Not favorited"
                toggleableState = ToggleableState(checked.value)
                role = Role.Switch
            },
    ) {
        Icon(
            imageVector = Icons.Default.Favorite,
            contentDescription = null // not needed here

        )
        Text("Favorite?")
    }
}

如需详细了解如何清除语义,请参阅 清除和设置语义

添加了 无障碍操作

请务必确保无障碍服务的用户能够完成应用中的所有用户体验流程。

如果自定义可组合项的互动以不明显的方式更改了应用的状态,请使用 Modifier.clickableModifier.combinedClickable 中的 onClickLabelonLongClickLabel 等参数为标准轻触操作提供描述性标签。

对于无法映射到标准轻触操作的复杂互动,请使用 customActions

例如,如果您的应用允许用户将某个项拖动到另一个位置,或在列表中的某个项上滑动,您可以通过向无障碍服务公开该操作,提供完成这些用户体验流程的替代方法。这样, TalkBack、语音操控或开关控制的用户就可以执行原本 只能通过手势执行的操作。

在 Compose 中,您可以使用 CustomAccessibilityAction 通过 semantics 修饰符中的 customActions 属性定义自定义无障碍操作。

例如,如果您的应用允许用户滑动某个项以将其关闭,您可以通过自定义无障碍操作公开该功能:

SwipeToDismissBox(
    modifier = Modifier.semantics {
        // Represents the swipe to dismiss for accessibility
        customActions = listOf(
            CustomAccessibilityAction(
                label = "Remove article from list",
                action = {
                    removeArticle()
                    true
                }
            )
        )
    },
    state = rememberSwipeToDismissBoxState(),
    backgroundContent = {}
) {
    ArticleListItem()
}

实现自定义无障碍操作后,用户可以通过操作菜单访问该操作。

如需详细了解自定义操作,请参阅自定义操作

让可执行的操作易于理解

当界面元素支持轻触并按住等操作时,TalkBack 等无障碍服务会将其读作“点按两次并按住以长按”。

此通用语音不会向用户提供有关轻触并按住操作的任何背景信息。

为了让此通知更有用,请为该操作指定有意义的说明。

在 Compose 中,clickablecombinedClickable 等标准互动修饰符具有内置参数(即 onClickLabelonLongClickLabel),您可以使用这些参数为操作提供说明, 如以下示例所示:

var contextMenuPhotoId by rememberSaveable { mutableStateOf<Int?>(null) }
val haptics = LocalHapticFeedback.current
LazyVerticalGrid(columns = GridCells.Adaptive(minSize = 128.dp)) {
    items(photos, { it.id }) { photo ->
        ImageItem(
            photo,
            Modifier
                .combinedClickable(
                    onClick = { activePhotoId = photo.id },
                    onLongClick = {
                        haptics.performHapticFeedback(HapticFeedbackType.LongPress)
                        contextMenuPhotoId = photo.id
                    },
                    onLongClickLabel = stringResource(R.string.open_context_menu)
                )
        )
    }
}
if (contextMenuPhotoId != null) {
    PhotoActionsSheet(
        photo = photos.first { it.id == contextMenuPhotoId },
        onDismissSheet = { contextMenuPhotoId = null }
    )
}

这样,TalkBack 就会读出“打开上下文菜单”,帮助用户了解该操作的用途。

您还可以直接在 semantics 修饰符中指定标签。

如需详细了解如何响应轻触和点击,请参阅 轻触和按压以及互动元素

使用内置无障碍功能

设计应用的界面时,请利用内置无障碍功能,避免重新实现已有的功能。Material、Compose 界面和 Foundation API 默认实现并提供许多无障碍功能实践。

在 Jetpack Compose 中,使用 ButtonSwitchCheckbox 等内置可组合项创建无障碍界面。这些组件预先打包了 semantics 修饰符(如 rolestateDescription),您可以使用这些修饰符让应用使用起来更没有障碍。

将语义应用于自定义组件

创建自定义组件时,请注意此组件需要哪种无障碍支持才能发挥其作用。通常,您已使用的标准 Compose API(如 clickabletoggleableselectable)就足够了,因为它们会自动为您填充语义树。

不过,某些组件需要比标准修饰符提供的更具体的信息。在这些情况下,请查找专用修饰符(如 triStateToggleable),如果不存在,请使用低级别 Modifier.semantics 显式提供语义。

例如,考虑一个 TriStateSwitch,这是一个具有三种状态(开启、关闭和不确定)的开关。

虽然标准 toggleable 修饰符假定有两种状态,但 triStateToggleable 修饰符处理了第三种状态的复杂性。它会自动设置无障碍 Role (Switch) 和 State。这样,无障碍服务就会收到准确的信息,而您无需手动定义语义。

以下代码段展示了使用此方法的 TriStateSwitch

@Composable
fun TriStateSwitch(
    state: ToggleableState,
    onClick: () -> Unit,
    modifier: Modifier = Modifier
) {
    // A real implementation would include custom drawing for the switch.
    // This example uses a Box to demonstrate the semantics.
    Box(
        modifier = modifier
            .size(width = 64.dp, height = 40.dp)
            // triStateToggleable handles the semantics (Role and State)
            // automatically, so explicit Modifier.semantics is not needed here.
            .triStateToggleable(
                state = state,
                onClick = onClick,
                role = Role.Switch
            )
            // Add visual feedback based on the state
            .background(
                when (state) {
                    ToggleableState.On -> Color.Green
                    ToggleableState.Off -> Color.Gray
                    ToggleableState.Indeterminate -> Color.Yellow
                }
            )
    )
}

// Usage within another composable:
var state by remember { mutableStateOf(ToggleableState.Off) }
TriStateSwitch(
    state = state,
    onClick = {
        state = when (state) {
            ToggleableState.Off -> ToggleableState.Indeterminate
            ToggleableState.Indeterminate -> ToggleableState.On
            ToggleableState.On -> ToggleableState.Off
        }
    }
)

构建自定义组件时,请确保提供所有相关的语义属性,以实现无障碍功能。例如,如果您的组件模仿标准控件(如开关或按钮),这些属性包括组件的角色(如 Role.SwitchRole.Button)、stateDescription(如“开启”“关闭”“已选中”或“未选中”)以及任何相关的操作标签。如需了解详情,请参阅自定义组件

使用除颜色之外的提示

为了帮助有色觉缺陷的用户,请使用除颜色之外的提示 区分应用屏幕中的界面元素。具体方法包括采用不同的形状或大小、提供文字或视觉图案,或者添加基于音频的反馈或基于轻触手势的触感反馈来表示元素的差异。

图 1 显示了一个 Activity 的两个版本。一个版本仅使用颜色区分工作流程中两种可能的操作。另一个版本采用了 最佳做法:除了颜色之外,还使用了形状和文字来 突出两个选项之间的差异:

左侧是一个带有两个圆形按钮(一个绿色,一个红色)的屏幕。右侧是同一界面,但两个圆形按钮都带有文字标签和有意义的图标。
图 1.仅使用颜色创建界面元素的示例 (左图),以及使用颜色、形状和文字创建界面元素的示例(右图)。

让媒体内容使用起来更没有障碍

如果您开发的应用包含视频剪辑 或音频录音等媒体内容,应设法为具有不同类型的 无障碍功能需求的用户提供支持,让他们能够理解此类内容。具体来说,请尝试执行以下操作:

  • 添加可让用户暂停或停止播放媒体、调整 音量以及切换字幕的控件。
  • 如果视频提供的信息对于完成工作流程至关重要, 应以其他格式提供相同的内容,如转录内容。

其他资源

如需详细了解如何让您的应用使用起来更没有障碍,请参阅下面列出的其他资源:

Codelab

查看内容