语义

除了可组合项所携带的主要信息(例如 Text 可组合项的文本字符串)之外,还可以提供有关 UI 元素的更多补充信息。

Compose 中关于组件含义和作用的信息称为 语义,它是一种向无障碍功能、自动填充和测试等服务提供有关可组合项的额外上下文的方式。例如,相机图标在视觉上可能只是一张图片,但其语义含义可能是“拍照”。

通过将适当的语义与适当的 Compose API 结合使用,您可以尽可能多地向无障碍服务提供有关组件的信息,然后这些服务会决定如何向用户呈现该组件。

Material、Compose UI 和 Foundation API 都带有遵循其特定角色和功能的内置语义,但您也可以根据您的特定要求修改现有 API 的这些语义,或为自定义组件设置新的语义。

语义属性

语义属性传达了相应可组合项的含义。例如,Text 可组合项包含一个语义属性 text,因为这是该可组合项的含义Icon 包含一个 contentDescription 属性(如果由开发者设置),该属性以文本形式传达图标的含义。

思考语义属性如何传达可组合项的含义。考虑一个 Switch。它在用户看来是这样的

图 1. 处于“开”和“关”状态的 Switch

为了描述此元素的含义,您可以这样说:“这是一个开关,它是一个可切换的元素,处于‘开’状态。您可以点击它进行互动。”

这正是语义属性的用途。此 Switch 元素的语义节点包含以下属性,如通过 Layout Inspector 可视化所示

Layout Inspector showing the Semantics properties of a Switch composable
图 2. Layout Inspector 显示 Switch 可组合项的语义属性。

Role 指示元素的类型。StateDescription 描述了“开”状态应如何被引用。默认情况下,这是“开”一词的本地化版本,但可以根据上下文使其更具体(例如,“已启用”)。ToggleableState 是 Switch 的当前状态。OnClick 属性引用了用于与此元素交互的方法。

跟踪应用中每个可组合项的语义属性可以解锁许多强大的可能性

  • 无障碍服务使用这些属性来表示屏幕上显示的 UI 并允许用户与它互动。对于 Switch 可组合项,Talkback 可能会宣布:“开;开关;双击切换”。用户可以双击屏幕以关闭开关。
  • 测试框架使用这些属性来查找节点、与它们互动并进行断言
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

默认情况下,基于 Compose foundation 库构建的可组合项和修饰符已为您设置了相关属性。您可以选择手动更改这些属性,以改善特定用例的无障碍支持,或更改可组合项的合并或清除策略

为了向无障碍服务传达组件的特定内容类型,您可以应用各种不同的语义。这些附加内容将支持主要的语义信息,并帮助无障碍服务微调组件的呈现方式、宣告方式或互动方式。

如需完整的语义属性列表,请参阅 SemanticsProperties 对象。如需完整的可能无障碍操作列表,请参阅 SemanticsActions 对象。

标题

应用通常包含文本丰富的内容屏幕,如长文章或新闻页面,这些内容通常通过标题划分为不同的子部分

A blog post with article text in a scrollable container.
图 3. 在可滚动容器中显示文章文本的博文。

有无障碍需求的用户可能难以轻松地在此类屏幕上导航。为了改善导航体验,某些无障碍服务允许在部分或标题之间直接进行更轻松的导航。为此,请通过定义组件的语义属性来指示其为 heading

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

提醒和弹窗

如果您的组件是提醒或弹窗,例如 Snackbar,您可能需要向无障碍服务发出信号,表明可以向用户传达新的结构或内容更新。

类似提醒的组件可以用 liveRegion 语义属性标记。这允许无障碍服务自动通知用户此组件或其子组件的更改

PopupAlert(
    message = "You have a new message",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Polite
    }
)

在大多数情况下,当用户的注意力只需要短暂地集中在警报或屏幕上重要变化的內容时,您应该使用 liveRegionMode.Polite

您应谨慎使用 liveRegion.Assertive 以避免破坏性反馈。它应在用户必须了解时间敏感內容的情况下使用

PopupAlert(
    message = "Emergency alert incoming",
    modifier = Modifier.semantics {
        liveRegion = LiveRegionMode.Assertive
    }
)

实时区域不应用于频繁更新的内容,例如倒计时器,以避免不断反馈导致用户不堪重负。

类似窗口的组件

类似窗口的自定义组件,例如 ModalBottomSheet,需要额外的信号来将其与周围内容区分开。为此,您可以使用 paneTitle 语义,以便无障碍服务可以适当地表示任何相关的窗口或窗格更改,以及其主要语义信息

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

作为参考,请参阅 Material 3 如何为其组件使用 paneTitle

错误组件

对于其他内容类型,例如类似错误的组件,您可能希望为有无障碍需求的用户扩展主要语义信息。在定义错误状态时,您可以将组件的 error 语义告知无障碍服务,并提供扩展的错误消息。

在此示例中,TalkBack 会读取主要错误文本信息,然后是附加的扩展消息

Error(
    errorText = "Fields cannot be empty",
    modifier = Modifier
        .semantics {
            error("Please add both email and password")
        }
)

进度跟踪组件

对于跟踪进度的自定义组件,您可能需要通知用户其进度变化,包括当前进度值、其范围和步长。您可以使用 progressBarRangeInfo 语义来做到这一点——这确保无障碍服务了解进度变化,并能相应地更新用户。不同的辅助技术也可能有独特的方式来提示进度增加和减少。

ProgressInfoBar(
    modifier = Modifier
        .semantics {
            progressBarRangeInfo =
                ProgressBarRangeInfo(
                    current = progress,
                    range = 0F..1F
                )
        }
)

列表和项目信息

在包含许多项目的自定义列表和网格中,无障碍服务收到更多详细信息(如项目总数和索引)可能会有所帮助。

通过分别在列表和项目上使用 collectionInfocollectionItemInfo 语义,在此长列表中,除了文本语义信息外,无障碍服务还可以告知用户他们在总集合中处于哪个项目索引

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

状态描述

可组合项可以为语义定义一个 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() }
            )
    ) {
        /* ... */
    }
}

自定义操作

自定义操作可用于更复杂的触摸屏手势,例如滑动以关闭或拖放,因为这些手势对于有运动障碍或其他残疾的用户来说可能难以互动。

为了让 滑动以关闭手势更易于访问,您可以将其链接到自定义操作,并在其中传递关闭操作和标签

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 等无障碍服务会高亮显示该组件,并提示其菜单中有更多可用操作,其中代表了滑动以关闭操作

Visualization of TalkBack action menu
图 4. TalkBack 操作菜单的可视化。

自定义操作的另一个用例是包含多个可用操作的长列表项,因为用户逐个遍历每个操作可能会很繁琐

=Visualization of Switch Access navigation on screen
图 5. Switch Access 在屏幕上的导航可视化。

为了改善导航体验(这对于基于交互的辅助技术,如“开关控制”或“语音控制”特别有用),您可以使用容器上的自定义操作,将操作移出单独的遍历,并移入单独的操作菜单。

ArticleListItemRow(
    modifier = Modifier
        .semantics {
            customActions = listOf(
                CustomAccessibilityAction(
                    label = "Open article",
                    action = {
                        openArticle()
                        true
                    }
                ),
                CustomAccessibilityAction(
                    label = "Add to bookmarks",
                    action = {
                        addToBookmarks()
                        true
                    }
                ),
            )
        }
) {
    Article(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = openArticle,
    )
    BookmarkButton(
        modifier = Modifier.clearAndSetSemantics { },
        onClick = addToBookmarks,
    )
}

在这些情况下,请务必使用 clearAndSetSemantics 修饰符手动清除原始子项的语义,因为您正在将它们移到自定义操作中。

以 Switch Access 为例,选择容器后,其菜单会打开并列出可用的嵌套操作

Switch Access highlight of the article list item
图 6. Switch Access 对文章列表项的高亮显示。
Visualization of Switch Access action menu.
图 7. Switch Access 操作菜单的可视化。

语义树

组合描述了应用的 UI,并通过运行可组合项生成。组合是一种由描述 UI 的可组合项组成的树结构。

在组合旁边,还存在一个并行树,称为语义树。此树以另一种方式描述您的 UI,可供无障碍功能服务和测试框架理解。无障碍服务使用该树向有特殊需求的用户描述应用。测试框架使用该树与您的应用互动并对其进行断言。语义树不包含如何绘制可组合项的信息,但它包含有关可组合项的语义含义的信息。

A typical UI hierarchy and its semantics tree
图 8. 典型的 UI 层次结构及其语义树。

如果您的应用包含来自 Compose foundation 和 material 库的可组合项和修饰符,则语义树会自动为您填充和生成。但是,当您添加自定义低级可组合项时,您必须手动提供其语义。也可能出现您的树无法正确或完全表示屏幕上元素含义的情况,在这种情况下,您可以调整该树。

例如,考虑此自定义日历可组合项

A custom calendar composable with selectable day elements
图 9. 具有可选日期元素的自定义日历可组合项。

在此示例中,整个日历被实现为一个单独的低级可组合项,它使用 Layout 可组合项并直接绘制到 Canvas。如果您不执行任何其他操作,无障碍服务将不会收到有关可组合项内容和用户在日历中选择的足够信息。例如,如果用户点击包含 17 的日期,无障碍框架只会收到整个日历控件的描述信息。在这种情况下,TalkBack 无障碍服务会宣布“日历”或稍好一点的“四月日历”,用户将不知道选择了哪一天。为了使此可组合项更易于访问,您需要手动添加语义信息。

合并树和未合并树

如前所述,UI 树中的每个可组合项可能设置了零个或多个语义属性。当可组合项未设置任何语义属性时,它不包含在语义树中。这样,语义树只包含实际包含语义含义的节点。但是,有时为了传达屏幕上显示的正确含义,合并某些节点子树并将其视为一个节点也很有用。这样,您就可以将一组节点作为一个整体来推理,而不是单独处理每个子代节点。根据经验,使用无障碍服务时,此树中的每个节点都代表一个可聚焦元素。

此类可组合项的一个示例是 Button。您可以将按钮视为单个元素,即使它可能包含多个子节点

Button(onClick = { /*TODO*/ }) {
    Icon(
        imageVector = Icons.Filled.Favorite,
        contentDescription = null
    )
    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
    Text("Like")
}

在语义树中,按钮后代元素的属性被合并,按钮在树中表示为单个叶节点

Merged single leaf semantics representation
图 10. 合并的单叶语义表示。

可组合项和修饰符可以通过调用 Modifier.semantics (mergeDescendants = true) {} 来指示它们希望合并其后代元素的语义属性。将此属性设置为 true 表示应合并语义属性。在 Button 示例中,Button 可组合项内部使用了包含此 semantics 修饰符的 clickable 修饰符。因此,按钮的后代节点被合并。阅读无障碍功能文档,了解何时应更改可组合项中的合并行为

Foundation 和 Material Compose 库中的多个修饰符和可组合项已设置此属性。例如,clickabletoggleable 修饰符会自动合并其后代元素。此外,ListItem 可组合项也会合并其后代元素。

检查树

语义树实际上是两棵不同的树。有一棵是合并的语义树,它会在 mergeDescendants 设置为 true 时合并子代节点。还有一棵是未合并的语义树,它不进行合并,而是保持每个节点完整。无障碍服务使用未合并的树并应用自己的合并算法,同时考虑 mergeDescendants 属性。测试框架默认使用合并的树。

您可以使用 printToLog() 方法检查这两棵树。默认情况下,与前面的示例一样,会记录合并树。要改为打印未合并树,请将 onRoot() 匹配器的 useUnmergedTree 参数设置为 true

composeTestRule.onRoot(useUnmergedTree = true).printToLog("MY TAG")

Layout Inspector 允许您通过在视图过滤器中选择偏好的选项来显示合并和未合并的语义树

Layout Inspector view options, allowing both the display of the merged and the unmerged Semantics tree
图 11. Layout Inspector 视图选项,允许同时显示合并和未合并的语义树。

对于树中的每个节点,Layout Inspector 都会在属性面板中显示该节点的合并语义和已设置的语义。

Semantics properties merged and set
图 12. 合并和设置的语义属性。

默认情况下,测试框架中的匹配器使用合并的语义树。这就是您可以通过匹配按钮内部显示的文本来与 Button 互动的原因

composeTestRule.onNodeWithText("Like").performClick()

通过将匹配器的 useUnmergedTree 参数设置为 true 来覆盖此行为,与 onRoot 匹配器一样。

调整树

如前所述,您可以覆盖或清除某些语义属性,或更改树的合并行为。这在您创建自己的自定义组件时尤其相关。如果不设置正确的属性和合并行为,您的应用可能无法访问,并且测试行为可能与您预期不同。如果您想了解更多关于测试的信息,请参阅测试指南