デフォルトでは、Compose アプリのユーザー補助スクリーン リーダーの動作は、想定される読み取り順序(通常は左から右、上から下)で実装されます。ただし、追加のヒントがなければ、アルゴリズムが実際の読み取り順序を決定できないタイプのアプリのレイアウトもあります。ビューベースのアプリでは、traversalBefore
プロパティと traversalAfter
プロパティを使用して、このような問題を修正できます。Compose 1.5 以降、Compose は同等の柔軟な API を提供しますが、新しいコンセプト モデルが追加されています。
isTraversalGroup
と traversalIndex
は、デフォルトの並べ替えアルゴリズムが適切でない場合に、ユーザー補助と TalkBack のフォーカス順序を制御できるセマンティック プロパティです。isTraversalGroup
は意味的に重要なグループを識別し、traversalIndex
はそれらのグループ内の個々の要素の順序を調整します。isTraversalGroup
を単独で使用することも、traversalIndex
と一緒に使用してさらにカスタマイズすることもできます。
スクリーン リーダーの移動順序を制御するには、アプリで isTraversalGroup
と traversalIndex
を使用します。
isTraversalGroup
で要素をグループ化する
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 ) } }
このコードにより、次のような出力が生成されます。
セマンティクスが設定されていないため、スクリーン リーダーのデフォルトの動作では、要素を左から右、上から下に走査します。このデフォルト設定により、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 が文の断片を正しい順序で読み上げるようになりました。
「この文は」→「左の列」にあります。→「この文は」→「右側」です。
移動順序をさらにカスタマイズする
traversalIndex
は、TalkBack の移動順序をカスタマイズできる float プロパティです。要素をグループ化するだけで 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's
セマンティクスを isTraversalGroup =
true
に設定しなくても、traversalIndex
の変更は引き続き適用されます。ただし、それらをバインドする CircularLayout
がないと、画面上の他のすべての要素がアクセスされた後に、文字盤の 12 桁の数字が最後に読み取られます。これは、他のすべての要素のデフォルトの traversalIndex
が 0f
であり、時計のテキスト要素が他のすべての 0f
要素の後に読み取られるためです。
例: フローティング アクション ボタンの移動順序をカスタマイズする
この例では、traversalIndex
と isTraversalGroup
は、マテリアル デザインのフローティング アクション ボタン(FAB)の移動順序を制御します。この例の基礎となるレイアウトは次のとおりです。
この例のレイアウトでは、デフォルトでは TalkBack が次の順番になっています。
トップ アプリバー → サンプル テキスト 0 ~ 6 → フローティング アクション ボタン(FAB) → 下部のアプリバー
スクリーン リーダーでまず FAB にフォーカスすることをおすすめします。FAB などのマテリアル要素に 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
より低い)、フローティング ボックスが画面上の他のすべての要素よりも先に表示されます。
次に、フローティング ボックスやその他の要素をスキャフォールドに配置して、マテリアル デザイン レイアウトを実装できます。
@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 のユーザー補助機能に関するテストの原則とツール