默认情况下,Compose 应用中的辅助功能屏幕阅读器行为以预期的阅读顺序实现,通常是从左到右,然后从上到下。但是,某些类型的应用布局,算法无法在没有其他提示的情况下确定实际的阅读顺序。在基于视图的应用中,您可以使用 traversalBefore
和 traversalAfter
属性修复此类问题。从 Compose 1.5 开始,Compose 提供了一个同样灵活的 API,但采用了新的概念模型。
isTraversalGroup
和 traversalIndex
是语义属性,可用于在默认排序算法不合适的情况下控制辅助功能和 TalkBack 的焦点顺序。isTraversalGroup
识别语义上重要的组,而 traversalIndex
调整这些组中各个元素的顺序。您可以单独使用 isTraversalGroup
,或与 traversalIndex
结合使用以进行进一步自定义。
在您的应用中使用 isTraversalGroup
和 traversalIndex
来控制屏幕阅读器的遍历顺序。
使用 isTraversalGroup
对元素进行分组
isTraversalGroup
是一个布尔属性,用于定义 语义 节点是否为遍历组。此类节点的功能是充当边界或边框,以组织节点的子节点。
在节点上设置 isTraversalGroup = true
表示在移动到其他元素之前,先访问该节点的所有子节点。您可以在非屏幕阅读器可聚焦节点(例如 Columns、Rows 或 Boxes)上设置 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 ) } }
代码生成的输出类似于以下内容
由于未设置任何语义,因此屏幕阅读器的默认行为是从左到右、从上到下遍历元素。由于此默认设置,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 正确工作,请将 traversalIndex
与 isTraversalGroup
结合使用以进一步自定义屏幕阅读器的排序。
traversalIndex
属性具有以下特征
- 具有较低
traversalIndex
值的元素优先级更高。 - 可以是正数或负数。
- 默认值为
0f
。 - 仅影响屏幕阅读器可聚焦的节点,例如屏幕上的元素(如文本或按钮)。例如,仅在列上设置
traversalIndex
不会有任何效果,除非列也设置了isTraversalGroup
。
以下示例显示了如何一起使用 traversalIndex
和 isTraversalGroup
。
示例:遍历钟面
钟面是标准遍历顺序不起作用的常见场景。本节中的示例是一个时间选择器,用户可以在其中遍历钟面上的数字,并为小时和分钟时段选择数字。
在以下简化代码段中,有一个 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
来绑定它们,则钟面的 12 个数字将在屏幕上访问所有其他元素后最后读取。发生这种情况是因为所有其他元素的默认 traversalIndex
为 0f
,并且时钟文本元素在所有其他 0f
元素之后读取。
示例:自定义浮动操作按钮的遍历顺序
在此示例中,traversalIndex
和 isTraversalGroup
控制 Material Design 浮动操作按钮 (FAB) 的遍历顺序。此示例的基础是以下布局
默认情况下,此示例中的布局具有以下 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 → 底部应用栏
其他资源
- 辅助功能: 所有 Android 应用开发共有的基本概念和技术
- 构建可访问的应用: 您可以采取的关键步骤,使您的应用更易于访问
- 改进应用辅助功能的原则: 在努力使您的应用更易于访问时,请牢记的关键原则
- 辅助功能测试: Android 辅助功能的测试原则和工具