修改遍历顺序

遍历顺序是指无障碍服务在界面元素中导航的顺序。在 Compose 应用中,元素会按预期的朗读顺序排列,通常是从左到右,然后从上到下。不过,在某些情况下,Compose 可能需要额外的提示才能确定正确的朗读顺序。

isTraversalGroup 和 traversalIndex 是语义属性,可让您在 Compose 的默认排序算法不够用的情况下,影响无障碍服务的遍历顺序。isTraversalGroup 用于标识需要自定义的语义重要组,而 traversalIndex 用于调整这些组中各个元素的顺序。您可以单独使用 isTraversalGroup 来表示应同时选择组内的所有元素,也可以将其与 traversalIndex 搭配使用,以进一步自定义。

在应用中使用 isTraversalGroup 和 traversalIndex 控制屏幕阅读器的遍历顺序。

对元素进行分组以便遍历

isTraversalGroup 是一个布尔值属性,用于定义语义节点是否为遍历组。此类节点的功能是在组织节点的子节点时充当边界。

在节点上设置 isTraversalGroup = true 意味着,在移至其他元素之前,系统会先访问该节点的所有子节点。您可以在非屏幕阅读器可聚焦节点(例如列、行或框)上设置 isTraversalGroup。

以下示例使用 isTraversalGroup。它会发出四个文本元素。左侧的两个元素属于一个 CardBox 元素,而右侧的两个元素属于另一个 CardBox 元素:

// CardBox() function takes in top and bottom sample text.
@Composable
fun CardBox(
    topSampleText: String,
    bottomSampleText: String,
    modifier: Modifier = Modifier
) {
    Box(modifier) {
        Column {
            Text(topSampleText)
            Text(bottomSampleText)
        }
    }
}

@Composable
fun TraversalGroupDemo() {
    val topSampleText1 = "This sentence is in "
    val bottomSampleText1 = "the left column."
    val topSampleText2 = "This sentence is "
    val bottomSampleText2 = "on the right."
    Row {
        CardBox(
            topSampleText1,
            bottomSampleText1
        )
        CardBox(
            topSampleText2,
            bottomSampleText2
        )
    }
}

该代码会生成类似于以下内容的输出:

包含两列文本的布局,左列显示“This sentence is in the left column”,右列显示“This sentence is on the right”。
图 1. 包含两个句子的布局(左列一个句子,右列一个句子)。

由于未设置任何语义,屏幕阅读器的默认行为是从左到右、从上到下遍历元素。由于此默认设置,TalkBack 会以错误的顺序读出句子片段:

“This sentence is in” →“This sentence is” →“the left column.”→“在右侧”。

如需正确排列 fragment,请修改原始代码段,将 isTraversalGroup 设置为 true:

@Composable
fun TraversalGroupDemo2() {
    val topSampleText1 = "This sentence is in "
    val bottomSampleText1 = "the left column."
    val topSampleText2 = "This sentence is"
    val bottomSampleText2 = "on the right."
    Row {
        CardBox(
//      1,
            topSampleText1,
            bottomSampleText1,
            Modifier.semantics { isTraversalGroup = true }
        )
        CardBox(
//      2,
            topSampleText2,
            bottomSampleText2,
            Modifier.semantics { isTraversalGroup = true }
        )
    }
}

由于 isTraversalGroup 是专门在每个 CardBox 上设置的,因此在对元素进行排序时,CardBox 边界会适用。在这种情况下,系统会先读取左侧的 CardBox,然后读取右侧的 CardBox。

现在,TalkBack 会按正确的顺序读出句子片段:

“This sentence is in” → “the left column.”→“这个句子是”→“在右侧”。

自定义遍历顺序

traversalIndex 是一个浮点属性,可让您自定义 TalkBack 遍历顺序。如果仅将元素分组在一起还不足以让 TalkBack 正常运行,请将 traversalIndex 与 isTraversalGroup 结合使用,以进一步自定义屏幕阅读器顺序。

traversalIndex 属性具有以下特征:

  • traversalIndex 值越小的元素,优先级越高。
  • 可以是正数,也可以是负数。
  • 默认值为 0f。
  • 为了让遍历索引影响遍历行为,必须在可供无障碍服务选择和聚焦的组件上设置遍历索引,例如文本或按钮等屏幕元素。
    • 例如,仅在 Column 上设置 traversalIndex 不会产生任何影响,除非该列也设置了 isTraversalGroup。

以下示例展示了如何将 traversalIndex 和 isTraversalGroup 结合使用。

表盘就是标准遍历顺序不适用的常见场景。本部分中的示例是一个时间选择器,用户可以遍历表盘上的数字,并为小时和分钟槽选择数字。

一个表盘,其上方有一个时间选择器。
图 2. 表盘图片。

在以下简化的代码段中,有一个 CircularLayout,其中绘制了 12 个数字,从 12 开始,沿圆圈顺时针移动:

@Composable
fun ClockFaceDemo() {
    CircularLayout {
        repeat(12) { hour ->
            ClockText(hour)
        }
    }
}

@Composable
private fun ClockText(value: Int) {
    Box(modifier = Modifier) {
        Text((if (value == 0) 12 else value).toString())
    }
}

由于默认的从左到右和从上到下顺序不符合表盘的逻辑,TalkBack 会不按顺序读出数字。如需解决此问题,请使用递增计数器值,如以下代码段所示:

@Composable
fun ClockFaceDemo() {
    CircularLayout(Modifier.semantics { isTraversalGroup = true }) {
        repeat(12) { hour ->
            ClockText(hour)
        }
    }
}

@Composable
private fun ClockText(value: Int) {
    Box(modifier = Modifier.semantics { this.traversalIndex = value.toFloat() }) {
        Text((if (value == 0) 12 else value).toString())
    }
}

如需正确设置遍历顺序,请先将 CircularLayout 设为遍历组,然后设置 isTraversalGroup = true。然后,在将每个时钟文本绘制到布局上时,将其对应的 traversalIndex 设置为计数器值。

由于计数器值不断增加,因此随着数字添加到屏幕,每个时钟值的 traversalIndex 都会变大 - 时钟值 0 的 traversalIndex 为 0,时钟值 1 的 traversalIndex 为 1。 这样,TalkBack 就会按照设置的顺序读出这些内容。现在,系统会按预期顺序朗读 CircularLayout 中的数字。

由于已设置的 traversalIndexes 仅相对于同一分组中的其他索引,因此屏幕顺序的其余部分已保留。换句话说,上述代码段中显示的语义更改只会修改设置了 isTraversalGroup = true 的表盘内的顺序。

请注意,即使不将 CircularLayout's 语义设置为 isTraversalGroup = true,traversalIndex 更改仍然适用。不过,如果没有 CircularLayout 将它们绑定在一起,则系统会在访问屏幕上的所有其他元素后,最后读取表盘的 12 个数字。发生这种情况是因为所有其他元素的默认 traversalIndex 为 0f,并且时钟文本元素会在所有其他 0f 元素之后读取。

API 注意事项

使用遍历 API 时,请考虑以下事项:

  • 应在包含分组元素的父级上设置 isTraversalGroup = true。
  • traversalIndex 应设置在包含语义且会被无障碍服务选择的子组件上。
  • 确保您要调查的所有元素都位于同一 zIndex 级别,因为这也会影响语义和遍历顺序。
  • 确保不会不必要地合并语义,因为这可能会影响遍历索引应用于哪些组件。