Compose 中的语义

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

组合旁边存在一个名为语义树的并行树。此树以无障碍服务和测试框架可以理解的替代方式描述您的界面。无障碍服务使用该树向有特定需求的用户描述应用。测试框架使用该树与您的应用进行交互,并对应用进行断言。语义树不包含有关如何绘制可组合项的信息,但包含有关可组合项的语义含义的信息。

典型的界面层次结构及其语义树
图 1. 典型的界面层次结构及其语义树。

如果您的应用由 Compose 基础和 Material 库中的可组合项和修饰符组成,系统会自动为您填充并生成语义树。但是,在添加自定义低级别可组合项时,您必须手动提供其语义。有时,您的树无法正确或完全表示屏幕上元素的含义,在这种情况下,您可以调整树。

例如,请考虑该自定义日历可组合项:

包含可选日期元素的自定义日历可组合项
图 2. 包含可选日期元素的自定义日历可组合项。

在此示例中,整个日历实现为单个低级可组合项,使用 Layout 可组合项并直接绘制为 Canvas。如果您不执行任何其他操作,无障碍服务将无法接收到足够的有关可组合项的内容以及用户在日历中所做选择的信息。例如,如果用户点击包含 17 的日期,则无障碍服务框架只会接收整个日历控件的说明信息。在这种情况下,TalkBack 无障碍服务会读出“日历”,或者稍微好一点的读出“四月日历”,而用户可能会好奇究竟选中了哪一天。为了提高此可组合项的无障碍性,您需要手动添加语义信息。

语义属性

具有一定语义含义的界面树中的所有节点在语义树中都有一个并行节点。语义树中的节点包含这些属性,这些属性传达了对应可组合项的含义。例如,Text 可组合项包含语义属性 text,因为这是该可组合项的含义。Icon 包含 contentDescription 属性(如果由开发者设置),该属性以文字形式传达 Icon 的含义。基于 Compose 基础库构建的可组合项和修饰符已经为您设置了相关属性。(可选)使用 semanticsclearAndSetSemantics 修饰符自行设置或替换属性。例如,向节点添加自定义无障碍操作,为可切换的元素提供备用状态说明,或指明应将某个文本可组合项视为标题

如需直观呈现语义树,请使用布局检查器工具,或在测试中使用 printToLog() 方法。这会在 Logcat 中输出当前的语义树。

class MyComposeTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun MyTest() {
        // Start the app
        composeTestRule.setContent {
            MyTheme {
                Text("Hello world!")
            }
        }
        // Log the full semantics tree
        composeTestRule.onRoot().printToLog("MY TAG")
    }
}

此测试的输出将如下所示:

    Printing with useUnmergedTree = 'false'
    Node #1 at (l=0.0, t=63.0, r=221.0, b=120.0)px
     |-Node #2 at (l=0.0, t=63.0, r=221.0, b=120.0)px
       Text = '[Hello world!]'
       Actions = [GetTextLayoutResult]

思考语义属性如何传达可组合项的含义。以 Switch 为例。以下是用户看到的内容:

图 3.处于“开启”和“关闭”状态的开关。

要描述此元素的含义,您可以这样说:“这是一个开关,它是一个处于“开启”状态的可切换元素。您可以点击它与之交互。”

这正是语义属性的用途。此 Switch 元素的语义节点包含以下属性,如布局检查器所示:

显示 Switch 可组合项的语义属性的布局检查器
图 4. 显示 Switch 可组合项的语义属性的布局检查器。

Role 表示元素的类型。StateDescription 描述了应如何引用“开启”状态。默认情况下,这是“On”一词的本地化版本,但可以根据上下文更具体(例如,“Enabled”)。ToggleableState 是 Switch 的当前状态。OnClick 属性引用用于与此元素交互的方法。如需查看语义属性的完整列表,请查看 SemanticsProperties 对象。如需查看可能的无障碍操作的完整列表,请查看 SemanticsActions 对象。

跟踪应用中每个可组合项的语义属性可以释放出诸多强大的可能性。请参见以下示例:

  • Talkback 使用这些属性大声朗读屏幕上显示的内容,并让用户顺畅地与屏幕互动。对于 Switch 可组合项,TalkBack 可能会说:“开启;开关;点按两次即可切换”。用户可以点按两次屏幕以关闭“开关”。
  • 测试框架使用这些属性来查找节点、与节点进行交互并做出声明。Switch 的示例测试可以如下所示:
    val mySwitch = SemanticsMatcher.expectValue(
        SemanticsProperties.Role, Role.Switch
    )
    composeTestRule.onNode(mySwitch)
        .performClick()
        .assertIsOff()

合并和未合并的语义树

如前文所述,界面树中的每个可组合项都可能设置了零个或多个语义属性。如果可组合项未设置语义属性,则不会包含在语义树中。这样一来,语义树便仅包含实际包含语义含义的节点。然而,有时为了传达屏幕上所显示内容的正确含义,合并某些节点树并将它们视为一个树也十分有用。这样,您就可以对一组节点进行推断,而不是单独处理每个后代节点。一般来讲,该树中的每个节点都代表使用无障碍服务时可聚焦的一个元素。

此类可组合项的示例包括 Button。即使某个按钮可能包含多个子节点,您也可以将其推断为单个元素:

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

在语义树中,按钮后代的属性已合并,并且按钮在树中呈现为单个叶节点:

合并的单叶语义表示法
图 5. 合并的单叶语义表示法。

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

基础库和 Material Compose 库中的几个修饰符和可组合项已设置此属性。例如,clickabletoggleable 修饰符会自动合并其后代节点。此外,ListItem 可组合项也会合并其后代节点。

检查树

语义树实际上是两个不同的树。有一个合并的语义树,它会在 mergeDescendants 设置为 true 时合并后代节点。此外,还有一个未合并的语义树,它不会应用合并,但会保持每个节点不变。无障碍服务会使用未合并的树,并在考虑 mergeDescendants 属性的情况下应用自己的合并算法。默认情况下,测试框架使用合并的树。

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

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

借助布局检查器,您可以在视图过滤器中选择首选的语义树,同时显示合并的语义树和未合并的语义树:

布局检查器视图选项,允许显示合并和未合并的语义树
图 6. 布局检查器视图选项,允许显示合并和未合并的语义树。

对于树中的每个节点,布局检查器会在属性面板中显示合并语义以及在该节点上设置的语义:

合并并设置语义属性
图 7. 合并并设置了语义属性。

默认情况下,测试框架中的匹配器会使用合并的语义树。因此,您可以通过匹配 Button 中显示的文本来与其进行交互:

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

onRoot 匹配器一样,可通过将匹配器的 useUnmergedTree 参数设置为 true 来替换此行为。

合并行为

当可组合项指示应该合并其后代节点时,这种合并究竟是如何发生的?

每个语义属性都有定义的合并策略。例如,ContentDescription 属性会将所有 ContentDescription 后代值添加到列表中。如需检查语义属性的合并策略,请在 SemanticsProperties.kt 中检查其 mergePolicy 实现。属性可以接受父值或子值,将值合并到列表或字符串中,完全不允许合并并抛出异常,还可以采用任何其他自定义合并策略。

需要注意的是,自身已设置 mergeDescendants = true 的后代将不会包含在合并中。我们来看一个示例:

带有图片、一些文本和书签图标的列表项
图 8. 带有图片、一些文本和书签图标的列表项。

这是一个可点击的列表项。当用户按某个行时,应用会导航到文章详情页面,用户可以在该页面中阅读文章。 列表项内有一个用于为文章添加书签的按钮,该按钮形成了一个嵌套的可点击元素,因此该按钮会在合并树中单独显示。行中的其余内容已合并:

合并的树在 Row 节点内的列表中包含许多文字。未合并的树包含每个 Text 可组合项的单独节点。
图 9. 合并的树在 Row 节点内的列表中包含许多文字。未合并的树包含每个 Text 可组合项的单独节点。

调整语义树

如前所述,您可以替换或清除某些语义属性,或更改树的合并行为。在您创建自己的自定义组件时,尤其要注意这一点。如果没有设置正确的属性和合并行为,您的应用可能无法访问,并且测试的行为可能与预期不同。如需详细了解应调整语义树的一些常见用例,请参阅无障碍功能文档。如需详细了解测试,请参阅测试指南

其他资源