根據預設,Compose 應用程式中的無障礙功能螢幕閱讀器行為會按照預期的閱讀順序實作,這類順序通常由左至右依序為從上到下。不過,某些應用程式版面配置需要額外的提示,演算法就無法判斷實際的閱讀順序。在以 View 為基礎的應用程式中,您可以使用 traversalBefore
和 traversalAfter
屬性修正這類問題。從 Compose 1.5 開始,Compose 提供了同等靈活的 API,但新增一個概念模型。
isTraversalGroup
和 traversalIndex
是語意屬性,可讓您控制無障礙功能和 TalkBack 焦點順序,在預設排序演算法不適用的情況下。isTraversalGroup
可識別具有語意意義的群組,traversalIndex
則會調整這些群組中個別元素的順序。您可以單獨使用 isTraversalGroup
,也可以搭配 traversalIndex
使用,進一步自訂。
在應用程式中使用 isTraversalGroup
和 traversalIndex
可控制螢幕閱讀器遍歷順序。
使用 isTraversalGroup
將元素分組
isTraversalGroup
是一個布林值屬性,用於定義語意節點是否為週遊群組。這類節點的函式是做為界線或邊界的節點之一,用於組織節點的子項。
在節點上設定 isTraversalGroup = true
表示在移動到其他元素之前,系統會先造訪該節點的所有子項。您可以在無法使用螢幕閱讀器的可聚焦節點 (例如 欄、列或 Box) 上設定 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" → "左欄。"→「位於右側」
如要正確排序片段,請修改原始程式碼片段,將 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 會按正確順序讀出句子片段:
「這句話位於」→「左欄」。→ "此句子為" →「在右側。」
進一步自訂遍歷順序
traversalIndex
是一種浮點屬性,可讓您自訂 TalkBack 遍歷順序。如果將元素分組在一起,使 TalkBack 無法正常運作,請將 traversalIndex
與 isTraversalGroup
搭配使用,進一步自訂螢幕閱讀器排序。
traversalIndex
屬性具備下列特性:
- 系統會優先套用
traversalIndex
值較低的元素。 - 可以是正數或負數。
- 預設值為
0f
。 - 只會影響螢幕閱讀器可聚焦的節點,例如文字或按鈕等螢幕元素。舉例來說,除非一併對資料欄設定
isTraversalGroup
,否則僅對資料欄設定traversalIndex
就不會有任何作用。
以下範例說明如何搭配使用 traversalIndex
和 isTraversalGroup
。
範例: Traverse 錶面
錶面是標準遍歷順序無法運作的常見情境。本節中的範例是時間挑選器,可讓使用者穿越錶面上的數字,並選取小時和分鐘位置的數字。
在以下簡化的程式碼片段中,有一個 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 碼會在使用者存取畫面上所有其他元素後,於最後讀取。這是因為所有其他元素都有預設的 0f
traversalIndex
,而時鐘文字元素會在所有其他 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
的預設值),表示浮動方塊位於所有其他螢幕元素之前。
接下來,您可以將浮動方塊和其他元素放入 Scaffold,以實作 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 會以下列順序與元素互動:
懸浮動作按鈕 → 頂端應用程式列 → 文字範例 0 到 6 → 底部應用程式列
其他資源
- 無障礙設計:所有 Android 應用程式開發作業常見的基本概念和技巧
- 建構無障礙應用程式:您可以透過幾個重要步驟來打造無障礙應用程式
- 提升應用程式無障礙程度的基本原則:改善應用程式的無障礙設計時,請記住下列重要原則
- 無障礙設計測試:測試 Android 無障礙功能的原則和工具