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

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

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

  • TalkBack:帮助低视力或失明人士。它会通过合成语音播报内容,并根据用户手势在应用中执行操作。
  • 开关控制:可帮助有运动障碍的用户。它会突出显示互动元素,并根据用户按下的按钮执行操作。它允许仅使用一个或两个按钮来控制设备。

为帮助有无障碍服务需求的用户成功使用您的应用,您的应用必须遵循本文所述的最佳实践,这些最佳实践以提高应用的无障碍性中所述的准则为基础。

以下各部分介绍了每种最佳实践,它们可以进一步提高应用的无障碍性:

标签元素
用户必须能够理解应用中每个有意义的互动式界面元素的内容和用途。
添加无障碍操作
通过添加无障碍操作,您可以让无障碍服务的用户在您的应用中完成关键的用户体验流程。
使用内置无障碍功能
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") }
)

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

集合中的元素

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

例如,如果您有 LazyColumn 或 LazyRow,请使用 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)
            }
        )
    }
}

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

相关内容组

如果您的应用显示多个构成自然分组的界面元素(例如歌曲的详细信息或消息的属性),请将这些元素排列在父级容器(例如 Column、Row 或 Box)中。使用父级容器的 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")
        }
    }
}

在对相关元素进行分组时(如上例所示),请仅使父级容器可互动。避免向内部子元素添加 clickable 或 focusable 修饰符。请改为将修饰符应用于父级 Row 或 Column。

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

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

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

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

文字中的标题

某些应用使用标题总结屏幕上显示的多组文字。如果某个特定元素表示标题,您可以通过在 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 语义属性,请参阅类似窗口的组件。

装饰性元素

如果界面中的某个元素仅用于视觉间距或视觉外观,请在该元素上设置适当的属性,以指明无障碍服务可以忽略该元素。

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

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

在以下示例中,Icon 和 Text 是自定义切换开关内的装饰性子元素。为了防止无障碍服务单独遍历这些子项,您可以使用父级 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.clickable 或 Modifier.combinedClickable 中使用 onClickLabel 或 onLongClickLabel 等参数为标准点按操作提供描述性标签。

对于无法映射到标准点按的复杂互动,请使用 customActions。

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

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

例如,如果您的应用允许用户通过滑动来关闭某个内容,您可以通过自定义无障碍操作来提供此功能:

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 中,clickable 和 combinedClickable 等标准互动修饰符具有内置参数(即 onClickLabel 和 onLongClickLabel),您可以使用这些参数提供操作说明,如以下示例所示:

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 中,使用 Button、Switch 和 Checkbox 等内置可组合项创建无障碍界面。这些组件预封装了 semantics 修饰符(例如 role 和 stateDescription),您可以使用这些修饰符来提高应用的无障碍性。

将语义应用于自定义组件

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

不过,某些组件需要比标准修饰符提供的更具体的信息。在这些情况下,请寻找专用修饰符(例如 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.Switch 或 Role.Button)、stateDescription(例如“开”“关”“已选中”或“未选中”)以及任何相关的操作标签。如需了解详情,请参阅自定义组件。

使用除颜色之外的提示

为了帮助有色觉缺陷的用户,请使用除颜色之外的提示区分应用屏幕中的界面元素。这些方法包括使用不同的形状或大小、提供文本或视觉图案,或添加基于音频或触觉的反馈来标记元素之间的差异。

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

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

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

如果您正在开发一款包含媒体内容(例如视频剪辑或录音)的应用,请尝试帮助具有不同类型无障碍需求的用户理解这些内容。具体来说,请尝试执行以下操作:

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

其他资源

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

Codelab

查看内容