走査順序を変更する

移動順序は、ユーザー補助サービスが UI 要素を移動する順序です。Compose アプリでは、要素は想定される読み取り順序で配置されます。通常は左から右、上から下です。ただし、正しい読み順を決定するために Compose に追加のヒントが必要な場合があります。

isTraversalGrouptraversalIndex は、Compose のデフォルトの並べ替えアルゴリズムが不十分なシナリオで、ユーザー補助サービスの走査順序に影響を与えることができるセマンティック プロパティです。isTraversalGroup は、カスタマイズが必要な意味的に重要なグループを特定します。一方、traversalIndex は、これらのグループ内の個々の要素の順序を調整します。isTraversalGroup のみを使用して、グループ内のすべての要素を一緒に選択する必要があることを示すことができます。traversalIndex と組み合わせて使用すると、さらにカスタマイズできます。

アプリで isTraversalGrouptraversalIndex を使用して、スクリーン リーダーの移動順序を制御します。

要素をグループ化して走査する

isTraversalGroup は、セマンティクス ノードが走査グループであるかどうかを定義するブール値プロパティです。このタイプのノードは、ノードの子を整理する境界または境界線として機能します。

ノードに isTraversalGroup = true を設定すると、他の要素に移動する前に、そのノードのすべての子が訪問されます。isTraversalGroup は、スクリーン リーダーがフォーカスできないノード(列、行、ボックスなど)に設定できます。

次の例では、isTraversalGroup を使用しています。4 つのテキスト要素を出力します。左の 2 つの要素は 1 つの CardBox 要素に属し、右の 2 つの要素は別の 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
        )
    }
}

このコードにより、次のような出力が生成されます。

2 列のテキスト レイアウト。左の列には「This sentence is in the left column」とあり、右の列には「This sentence is on the right」とあります。
図 1. 2 つの文(左の列に 1 つ、右の列に 1 つ)を含むレイアウト。

セマンティクスが設定されていないため、スクリーン リーダーのデフォルトの動作は、要素を左から右、上から下に移動することです。このデフォルト設定により、TalkBack は文の断片を間違った順序で読み上げます。

「この文は」→「この文は」→「左の列にあります」→ 「右側」

フラグメントを正しく並べ替えるには、元のスニペットを変更して 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 }
        )
    }
}

isTraversalGroup は各 CardBox に個別に設定されるため、要素の並べ替え時に CardBox 境界が適用されます。この場合、左側の CardBox が最初に読み取られ、次に右側の CardBox が読み取られます。

これで、TalkBack は文章のフラグメントを正しい順序で読み上げます。

「この文は」→「左の列にあります」→ 「この文は」→「右側にあります。」

移動順序をカスタマイズする

traversalIndex は、TalkBack の走査順序をカスタマイズできる浮動小数点プロパティです。要素をグループ化しても TalkBack が正しく動作しない場合は、traversalIndexisTraversalGroup を組み合わせて、スクリーン リーダーの順序をさらにカスタマイズします。

traversalIndex プロパティには次の特性があります。

  • traversalIndex 値が小さい要素が優先されます。
  • 正または負の値にできます。
  • デフォルト値は 0f です。
  • トラバーサル インデックスがトラバーサル動作に影響を与えるには、ユーザー補助サービスによって選択可能でフォーカス可能になるコンポーネント(テキストやボタンなどの画面上要素など)に設定する必要があります。
    • たとえば、ColumntraversalIndex のみを設定しても、列に isTraversalGroup も設定されていない限り、効果はありません。

次の例は、traversalIndexisTraversalGroup を併用する方法を示しています。

標準の走査順序が機能しない一般的なシナリオとして、文字盤があります。このセクションの例は時刻選択ツールです。ユーザーは文字盤上の数字を移動して、時刻と分刻みの数字を選択できます。

時刻選択ツールが上に表示された文字盤。
図 2. 文字盤の画像。

次の簡素化されたスニペットでは、12 個の数字が 12 から始まり、時計回りに円の周囲に描画される CircularLayout があります。

@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 桁が最後に読み上げられます。これは、他のすべての要素のデフォルトの traversalIndex0f であり、時計のテキスト要素が他のすべての 0f 要素の後に読み取られるためです。

API に関する考慮事項

走査 API を使用する場合は、次の点を考慮してください。

  • isTraversalGroup = true は、グループ化された要素を含む親に設定する必要があります。
  • traversalIndex は、セマンティクスを含む子コンポーネントに設定し、ユーザー補助サービスによって選択されるようにする必要があります。
  • 調査対象の要素がすべて同じ zIndex レベルにあることを確認します。これは、セマンティクスと走査順序にも影響します。
  • 不要なセマンティクスが統合されていないことを確認してください。これは、どのコンポーネントに走査インデックスが適用されるかに影響する可能性があります。