控制遍歷順序

根據預設,Compose 應用程式中的無障礙功能螢幕閱讀器行為會按照預期的閱讀順序實作,這類順序通常由左至右依序為從上到下。不過,某些應用程式版面配置需要額外的提示,演算法就無法判斷實際的閱讀順序。在以 View 為基礎的應用程式中,您可以使用 traversalBeforetraversalAfter 屬性修正這類問題。從 Compose 1.5 開始,Compose 提供了同等靈活的 API,但新增一個概念模型。

isTraversalGrouptraversalIndex 是語意屬性,可讓您控制無障礙功能和 TalkBack 焦點順序,在預設排序演算法不適用的情況下。isTraversalGroup 可識別具有語意意義的群組,traversalIndex 則會調整這些群組中個別元素的順序。您可以單獨使用 isTraversalGroup,也可以搭配 traversalIndex 使用,進一步自訂。

在應用程式中使用 isTraversalGrouptraversalIndex 可控制螢幕閱讀器遍歷順序。

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

程式碼會產生類似以下的輸出內容:

含有兩欄文字的版面配置,左欄顯示「This sentence is in the left column」(這個句子在左欄中),而右欄顯示「This sentence on theright」(這是在右側)。
圖 1 含有兩個語句的版面配置 (一個位於左欄,一個在右欄中)。

由於未設定語意,螢幕閱讀器的預設行為是從左到右、由上到下週遊元素。因此,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 無法正常運作,請將 traversalIndexisTraversalGroup 搭配使用,進一步自訂螢幕閱讀器排序。

traversalIndex 屬性具備下列特性:

  • 系統會優先套用 traversalIndex 值較低的元素。
  • 可以是正數或負數。
  • 預設值為 0f
  • 只會影響螢幕閱讀器可聚焦的節點,例如文字或按鈕等螢幕元素。舉例來說,除非一併對資料欄設定 isTraversalGroup,否則僅對資料欄設定 traversalIndex 就不會有任何作用。

以下範例說明如何搭配使用 traversalIndexisTraversalGroup

範例: Traverse 錶面

錶面是標準遍歷順序無法運作的常見情境。本節中的範例是時間挑選器,可讓使用者穿越錶面上的數字,並選取小時和分鐘位置的數字。

上方有時間挑選器的錶面。
圖 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 碼會在使用者存取畫面上所有其他元素後,於最後讀取。這是因為所有其他元素都有預設的 0f traversalIndex,而時鐘文字元素會在所有其他 0f 元素之後讀取。

範例:自訂懸浮動作按鈕的遍歷順序

在這個範例中,traversalIndexisTraversalGroup 會控制 Material Design 懸浮動作按鈕 (FAB) 的遍歷順序。這個範例的基礎如下:

含有頂端應用程式列、範例文字、懸浮動作按鈕和底部應用程式列的版面配置。
圖 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 的預設值),表示浮動方塊位於所有其他螢幕元素之前。

接下來,您可以將浮動方塊和其他元素放入 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 → 底部應用程式列

其他資源