走査順序を変更する

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

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

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

トラバーサル用に要素をグループ化する

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 は文の断片を間違った順序で読み上げます。

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

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

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

traversalIndex は、TalkBack の移動順序をカスタマイズできる float プロパティです。要素をグループ化するだけでは TalkBack が正しく動作しない場合は、traversalIndex と isTraversalGroup を組み合わせて使用し、スクリーン リーダーの順序をさらにカスタマイズします。

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

  • traversalIndex 値が小さい要素ほど優先度が高くなります。
  • 正または負の値が可能。
  • デフォルト値は 0f です。
  • トラバーサル インデックスがトラバーサルの動作に影響を与えるようにするには、ユーザー補助サービスによって選択可能かつフォーカス可能になるコンポーネント(テキストやボタンなどの画面上の要素など)に設定する必要があります。
    • たとえば、Column で 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 = true に設定しなくても、traversalIndex の変更は適用されます。しかし、それらをバインドする CircularLayout がないと、時計の文字盤の 12 個の数字は、画面上の他のすべての要素が読み上げられた後に読み上げられます。これは、他のすべての要素のデフォルトの traversalIndex が 0f であり、時計のテキスト要素が他のすべての 0f 要素の後に読み取られるために発生します。

API に関する注意事項

トラバーサル API を使用する場合は、次の点を考慮してください。

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