控制遍历顺序

默认情况下,Compose 应用中会实现无障碍屏幕阅读器行为 按照预期的阅读顺序排列,通常是从左到右,然后从上到下。 不过,对于某些类型的应用布局,算法无法确定 实际阅读顺序,没有额外提示。在基于视图的应用中,您可以 请使用 traversalBeforetraversalAfter 属性修复此类问题。 从 Compose 1.5 开始,Compose 提供了同样灵活的 API,但 一种新的概念模式。

isTraversalGrouptraversalIndex 是 可让您控制无障碍功能和 TalkBack 焦点顺序, 默认排序算法并不合适。isTraversalGroup标识 语义重要性分组,而 traversalIndex 会调整 这些组中的各个元素您可以单独使用 isTraversalGroup, 或使用 traversalIndex 进一步自定义。

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

使用 isTraversalGroup 将元素分组

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
  句子位于左列中”右列显示“This sentence is 右边”。
图 1. 包含两个句子(左侧一个)的布局 一列,右列中有一个)。

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

“这句话是”→“这句话是”→“左列”。→“在 。”

要正确地对 Fragment 进行排序,请修改原始代码段,将其设为 isTraversalGrouptrue

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

由于针对每个 CardBox 专门设置了 isTraversalGroup,因此 CardBox 对元素进行排序时会应用边界。在本示例中,左侧 先读取 CardBox,然后读取正确的 CardBox

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

“这句话是”→“左列”。→“这句话是”→“在 。”

进一步自定义遍历顺序

traversalIndex 是一个浮动属性,可让您自定义 TalkBack 遍历顺序。如果将元素组合在一起还不足以让 TalkBack 无法 正常运行,请将 traversalIndex 与以下函数结合使用 isTraversalGroup,用于进一步自定义屏幕阅读器排序方式。

traversalIndex 属性具有以下特征:

  • traversalIndex 值较低的元素优先。
  • 可以是正面或负面。
  • 默认值为 0f
  • 仅影响屏幕阅读器可聚焦节点,例如屏幕上的元素,例如 文字或按钮例如,只在列上设置 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 = truetraversalIndex 更改仍然适用。不过,如果没有 CircularLayout 用于绑定它们,系统会读取表盘的 12 位数字 最后,在访问完屏幕上的所有其他元素之后。发生这种情况 因为所有其他元素的默认 traversalIndex 均为 0f,并且 时钟文本元素在所有其他 0f 元素之后读取。

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

在此示例中,traversalIndexisTraversalGroup 控制着 Material Design 悬浮操作按钮 (FAB) 的遍历顺序。基础 该示例的布局如下所示:

包含顶部应用栏、示例文本、悬浮操作按钮和
  底部应用栏
图 3. 包含顶部应用栏、示例文本、悬浮操作按钮 以及底部应用栏。

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

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

您可能希望屏幕阅读器先将焦点放在 FAB 上。要设置 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 → 底部应用栏

其他资源