合并与清除

随着无障碍服务在屏幕元素中导航,确保这些元素以适当的粒度分组、分离甚至隐藏至关重要。当屏幕中的每个底层可组合项都独立突出显示时,用户需要进行大量交互才能在屏幕上移动。如果元素合并得过于激进,用户可能无法理解哪些元素在逻辑上属于同一组。如果屏幕上存在纯装饰性元素,则可以将其从无障碍服务中隐藏。在这些情况下,您可以使用 Compose API 来合并、清除和隐藏语义。

合并语义

当您将 clickable 修饰符应用于父可组合项时,Compose 会自动合并其下的所有子元素。要了解交互式 Compose Material 和 Foundation 组件如何默认使用合并策略,请参阅交互式元素部分。

组件由多个可组合项组成是很常见的。

这些可组合项可以形成一个逻辑组,并且每个都可以包含重要信息,但您可能仍希望无障碍服务将其视为一个元素。

A group of UI elements including a user's name. The name is selected.
例如,想象一个显示用户头像、姓名和一些额外信息的可组合项

图 1. 一组 UI 元素,包括用户的姓名。姓名处于选中状态。

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

您可以通过在语义修饰符中使用 mergeDescendants 参数,使 Compose 合并这些元素。

A group of UI elements including a user's name. All the elements are selected together.
这样,无障碍服务将组件视为一个实体,并且所有后代语义属性都将合并。

无障碍服务现在一次性聚焦整个容器,并合并其内容

图 2. 一组 UI 元素,包括用户的姓名。所有元素均同时选中。

List item with image, some text, and a bookmark icon
每个语义属性都有一个定义的合并策略。

@Composable
private fun ArticleListItem(
    openArticle: () -> Unit,
    addToBookmarks: () -> Unit,
) {

    Row(modifier = Modifier.clickable { openArticle() }) {
        // Merges with parent clickable:
        Icon(
            painter = painterResource(R.drawable.ic_logo),
            contentDescription = "Article thumbnail"
        )
        ArticleDetails()

        // Defies the merge due to its own clickable:
        BookmarkButton(onClick = addToBookmarks)
    }
}

例如,ContentDescription 属性将所有后代 ContentDescription 值添加到列表中。您可以通过查看 SemanticsProperties.kt 中其 mergePolicy 的实现来检查语义属性的合并策略。属性可以采用父值或子值,将值合并到列表或字符串中,根本不允许合并并抛出异常,或采用任何其他自定义合并策略。

The merged tree contains multiple texts in a list inside the Row node. The unmerged tree contains separate nodes for each Text composable.
在其他情况下,您可能期望子语义合并到父语义中,但这并未发生。在以下示例中,我们有一个包含子元素的 clickable 列表项父级,我们可能期望父级合并所有子元素

图 3. 包含图像、文本和书签图标的列表项。

当用户按下 clickableRow 时,会打开文章。在内部,有一个 BookmarkButton 用于收藏文章。此嵌套按钮显示为未合并,而行内的其余子内容已合并

图 4. 合并树在 Row 节点内包含列表中的多个文本。unmerged 树为每个 Text 可组合项包含单独的节点。

有些可组合项设计上不会自动合并到父级下。

当子级也在合并时,父级无法合并其子级,无论是通过显式设置 mergeDescendants = true,还是因为它们本身就是会合并的组件,例如按钮或可点击项。了解某些 API 如何合并或阻止合并可以帮助您调试一些潜在的意外行为。

当子元素在其父级下构成一个逻辑且合理的组时,请使用合并。

但如果嵌套子项需要手动调整或移除其自身的语义,其他 API 可能更适合您的需求(例如,clearAndSetSemantics)。

// 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?")
    }
}

清除并设置语义

如果语义信息需要完全清除或覆盖,clearAndSetSemantics 是一个强大的 API。

当组件需要清除其自身及其后代语义时,请使用带有空 lambda 的此 API。当其语义必须被覆盖时,将新内容包含在 lambda 内部。

请注意,当使用空 lambda 清除时,清除的语义不会发送给任何使用此信息的消费者,例如无障碍功能、自动填充或测试。当使用 clearAndSetSemantics{/*semantic information*/} 覆盖内容时,新语义会替换元素及其后代的所有先前语义。

  • 以下是一个自定义开关组件的示例,它由一个带有图标和文本的可交互行表示
    • 尽管图标和文本包含一些语义信息,但它们共同并不能表明此组件是可切换的。
  • 合并不足以满足需求,因为您必须提供有关该组件的更多信息。
  • 由于上述代码段创建了一个自定义开关组件,您需要添加开关能力,以及 stateDescriptiontoggleableStaterole 语义。这样,组件状态和相关操作就可以使用了,例如 TalkBack 会播报“双击以切换”而不是“双击以激活”。

通过清除原始语义并设置新的、更具描述性的语义,无障碍服务现在可以识别这是一个可切换组件,并且可以交替状态。

使用 clearAndSetSemantics 时,请考虑以下事项:

因为设置此 API 后服务将不会收到任何信息,所以最好谨慎使用。

@Composable
fun WatermarkExample(
    watermarkText: String,
    content: @Composable () -> Unit,
) {
    Box {
        WatermarkedContent()
        // Mark the watermark as hidden to accessibility services.
        WatermarkText(
            text = watermarkText,
            color = Color.Gray.copy(alpha = 0.5f),
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .semantics { hideFromAccessibility() }
        )
    }
}

@Composable
fun DecorativeExample() {
    Text(
        modifier =
        Modifier.semantics {
            hideFromAccessibility()
        },
        text = "A dot character that is used to decoratively separate information, like •"
    )
}

语义信息可能被 AI 代理和类似服务用于理解屏幕,因此只应在必要时清除。

可以在 API lambda 中设置自定义语义。

修饰符的顺序很重要——无论其他合并策略如何,此 API 都会清除其应用位置之后的所有语义。

  • 隐藏语义
    • 在某些情况下,元素不需要发送到无障碍服务——可能是它们的额外信息对于无障碍功能是多余的,或者它们纯粹是视觉装饰性且非交互式的。在这些情况下,您可以使用 hideFromAccessibility API 隐藏元素。
    • 以下示例是可能需要隐藏的组件:一个跨越组件的冗余水印,以及一个用于装饰性分隔信息的字符。
    • 在这里使用 hideFromAccessibility 可确保水印和装饰从无障碍服务中隐藏,但仍保留其语义以用于其他用例,例如测试。
  • 用例细分
    • 以下是帮助您清晰区分上述 API 的用例总结:
Table with differentiated API use cases.
当内容不打算供无障碍服务使用时