修改遍历顺序

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

isTraversalGrouptraversalIndex 是语义属性,可让您在 Compose 的默认排序算法不足的情况下影响无障碍服务的遍历顺序。isTraversalGroup 用于标识需要自定义的语义重要组,而 traversalIndex 则调整这些组内单个元素的顺序。您可以单独使用 isTraversalGroup 来表示组内的所有元素应一起选择,或与 traversalIndex 结合使用以进行进一步自定义。

在您的应用中使用 isTraversalGrouptraversalIndex 来控制屏幕阅读器遍历顺序。

对元素进行分组以进行遍历

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

代码生成类似如下的输出

Layout with two columns of text, with the left column reading 'This
  sentence is in the left column' and the right column reading 'This sentence is on the right.'
图 1. 包含两句话(一句在左列,一句在右列)的布局。

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

"This sentence is in" → "This sentence is" → "the left column." → "on the right."

为了正确排序片段,修改原始代码段,将 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." → "This sentence is" → "on the right."

自定义遍历顺序

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

traversalIndex 属性具有以下特征

  • traversalIndex 值较低的元素优先处理。
  • 可以是正数或负数。
  • 默认值为 0f
  • 为了让遍历索引影响遍历行为,它必须设置在可由无障碍服务选择和聚焦的组件上,例如文本或按钮等屏幕元素。
    • 例如,仅在 Column 上设置 traversalIndex 将无效,除非该列也设置了 isTraversalGroup

以下示例展示了如何同时使用 traversalIndexisTraversalGroup

表盘是标准遍历顺序不起作用的常见场景。本节中的示例是一个时间选择器,用户可以在其中遍历表盘上的数字,并选择小时和分钟位的数字。

A clock face with a time picker above it.
图 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 的语义设置为 isTraversalGroup = truetraversalIndex 的更改仍然适用。但是,如果没有 CircularLayout 将它们绑定,表盘的十二位数字将在访问完屏幕上所有其他元素后最后读取。这是因为所有其他元素的默认 traversalIndex 都是 0f,而时钟文本元素在所有其他 0f 元素之后读取。

API 注意事项

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

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