控制遍历顺序

默认情况下,Compose 应用中辅助功能屏幕阅读器的行为以预期的阅读顺序实现,通常是从左到右,然后从上到下。但是,在某些类型的应用布局中,算法无法确定实际的阅读顺序,而无需其他提示。在基于视图的应用中,您可以使用 traversalBeforetraversalAfter 属性修复此类问题。从 Compose 1.5 开始,Compose 提供了一个同样灵活的 API,但具有新的概念模型。

isTraversalGrouptraversalIndex 是语义属性,可让您在默认排序算法不合适的情况下控制辅助功能和 TalkBack 的焦点顺序。isTraversalGroup 识别语义上重要的组,而 traversalIndex 调整这些组内各个元素的顺序。您可以单独使用 isTraversalGroup,或与 traversalIndex 结合使用以进行进一步自定义。

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

使用 isTraversalGroup 对元素进行分组

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

在节点上设置 isTraversalGroup = true 表示在移动到其他元素之前,会先访问该节点的所有子元素。您可以对不可聚焦屏幕阅读器的节点设置 isTraversalGroup,例如 Columns、Rows 或 Boxes。

以下示例使用 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
  • 仅影响屏幕阅读器可聚焦的节点,例如屏幕上的元素(如文本或按钮)。例如,仅在列上设置 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 = true,则 traversalIndex 更改仍然适用。但是,如果没有 CircularLayout 将它们绑定,则时钟面的十二位数字将在屏幕上所有其他元素都被访问后最后读取。发生这种情况是因为所有其他元素的默认 traversalIndex0f,并且时钟文本元素在所有其他 0f 元素之后读取。

示例:自定义浮动操作按钮的遍历顺序

在此示例中,traversalIndexisTraversalGroup 控制 Material Design 浮动操作按钮 (FAB) 的遍历顺序。此示例的基础是以下布局

A layout with a top app bar, sample text, a floating action button, and
  a bottom app bar.
图 3. 具有顶部应用栏、示例文本、浮动操作按钮和底部应用栏的布局。

默认情况下,此示例中的布局具有以下 TalkBack 顺序

顶部应用栏 → 示例文本 0 到 6 → 浮动操作按钮 (FAB) → 底部应用栏

您可能希望屏幕阅读器首先聚焦于 FAB。要在 FAB 等 Material 元素上设置 traversalIndex,请执行以下操作

@Composable
fun FloatingBox() {
    Box(modifier = Modifier.semantics { isTraversalGroup = true; traversalIndex = -1f }) {
        FloatingActionButton(onClick = {}) {
            Icon(imageVector = Icons.Default.Add, contentDescription = "fab icon")
        }
    }
}

在此代码段中,创建一个具有 isTraversalGroup 设置为 true 的框,并在同一框上设置 traversalIndex-1f 低于 0f 的默认值)表示浮动框位于屏幕上所有其他元素之前。

接下来,您可以将浮动框和其他元素放入脚手架中,后者实现 Material Design 布局

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ColumnWithFABFirstDemo() {
    Scaffold(
        topBar = { TopAppBar(title = { Text("Top App Bar") }) },
        floatingActionButtonPosition = FabPosition.End,
        floatingActionButton = { FloatingBox() },
        content = { padding -> ContentColumn(padding = padding) },
        bottomBar = { BottomAppBar { Text("Bottom App Bar") } }
    )
}

TalkBack 以以下顺序与元素交互

FAB → 顶部应用栏 → 示例文本 0 到 6 → 底部应用栏

其他资源