Compose 中的语义

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

组合旁边存在一个并行树,称为语义。此树以另一种方式描述您的界面,即 对于无障碍服务测试而言是可理解的 框架。无障碍服务使用该树向符合以下条件的用户描述应用: 满足特定需求测试框架使用该树与您的应用交互 并对其做出断言语义树不包含 有关如何绘制可组合项的信息,但它包含 可组合项的语义含义。

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

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

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

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

在此示例中,整个日历实现为单个低级可组合项,使用 Layout 可组合项并直接绘制为 Canvas。如果您不采取任何其他措施,无障碍服务将无法获得足够多的流量 与可组合项的内容以及用户在 日历。例如,如果用户点击包含 17 的日期,则无障碍服务框架只会接收整个日历控件的说明信息。在这种情况下,TalkBack 无障碍服务 播报“日历”或者“4 月日历”用户 就会好奇具体选择了哪一天为了进一步提高此可组合项的 因此您需要手动添加语义信息。

语义属性

具有一定语义含义的界面树中的所有节点在语义树中都有一个并行节点。语义树中的节点包含这些属性,这些属性传达了对应可组合项的含义。例如,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介绍了 “启用”状态。默认情况下,这是 “启用”一词,但可以根据具体的 上下文。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 可组合项在内部使用 clickable 修饰符,其中包含以下代码 semantics 修饰符。因此,该按钮的后代节点会合并在一起。 请阅读无障碍功能文档,详细了解应在何时更改 合并行为

基础库和 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()

通过设置 useUnmergedTree 参数的 匹配器与 true 匹配,就像使用 onRoot 匹配器一样。

合并行为

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

每个语义属性都有定义的合并策略。例如, ContentDescription 属性将所有 ContentDescription 后代值添加到 列表。通过检查语义属性的合并策略, SemanticsProperties.kt 中的 mergePolicy 实现。媒体资源 接受父值或子值、将值合并为列表或 字符串,完全不允许合并并抛出异常,或任何其他 自定义合并策略

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

包含图片、一些文字和书签图标的列表项
图 8.包含图片、一些文字和书签图标的列表项。

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

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

调整语义树

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

其他资源