Compose で作成するアプリは、さまざまなニーズを持つ利用者に対するユーザー補助をサポートする必要があります。ユーザー補助サービスは、画面に表示される内容を、特定のニーズを持つ利用者に適した形式に変換するために使用されます。ユーザー補助サービスをサポートするため、アプリは UI 要素に関するセマンティック情報を公開する Android フレームワークの API を使用します。Android フレームワークは、このセマンティック情報をユーザー補助サービスに伝えます。個々のユーザー補助サービスは、ユーザーに対してアプリを説明するのに最も適した方法を選択できます。Android には、TalkBack やスイッチ アクセスなど、いくつかのユーザー補助サービスが用意されています。
セマンティクス
Compose は、セマンティクス プロパティを使用してユーザー補助サービスに情報を渡します。セマンティクス プロパティは、ユーザーに表示される UI 要素に関する情報を提供します。Text
や Button
のような組み込みコンポーザブルのほとんどは、コンポーザブルとその子から推測される情報をセマンティクス プロパティに入力します。toggleable
や clickable
のようないくつかの修飾子も、特定のセマンティクス プロパティを設定します。しかし、フレームワークは、ユーザーに対して UI 要素を説明する方法を把握するために、より多くの情報を必要とする場合があります。
このドキュメントでは、Android フレームワークに対して情報を正しく説明するために、コンポーザブルに明示的に情報を追加する必要があるさまざまな状況について説明します。また、特定のコンポーザブルのためにセマンティクス情報を完全に置き換える方法についても説明します。このドキュメントは、Android のユーザー補助の基本を理解していることを前提としています。
一般的なユースケース
ユーザー補助を必要とする人がアプリを正しく使用できるようにするには、このページに記載されているベスト プラクティスに沿ってアプリを作成する必要があります。
タップ ターゲットの最小サイズを検討する
クリック、タップなど、ユーザーが操作できる画面上の要素はすべて、確実に操作できるよう十分な大きさにする必要があります。これらの要素のサイズを調整する際は、マテリアル デザインのユーザー補助のガイドラインを適切に遵守するため、最小サイズを必ず 48 dp に設定してください。
マテリアル コンポーネント(Checkbox
、RadioButton
、Switch
、Slider
、Surface
など)は、この最小サイズを内部で設定しますが、これはコンポーネントがユーザー アクションを受け取れる場合に限ります。たとえば、Checkbox
の onCheckedChange
パラメータを非 null 値に設定すると、幅と高さが 48 dp 以上のパディングが含まれます。
@Composable private fun CheckableCheckbox() { Checkbox(checked = true, onCheckedChange = {}) }
onCheckedChange
パラメータを null に設定すると、コンポーネントを直接操作できないため、パディングは含まれません。
@Composable private fun NonClickableCheckbox() { Checkbox(checked = true, onCheckedChange = null) }
Switch
、RadioButton
、Checkbox
などの選択コントロールを実装する場合、通常は、クリック可能な動作を親コンテナにリフトし、コンポーザブルのクリック コールバックを null
に設定して、toggleable
または selectable
修飾子を親コンポーザブルに追加します。
@Composable private fun CheckableRow() { MaterialTheme { var checked by remember { mutableStateOf(false) } Row( Modifier .toggleable( value = checked, role = Role.Checkbox, onValueChange = { checked = !checked } ) .padding(16.dp) .fillMaxWidth() ) { Text("Option", Modifier.weight(1f)) Checkbox(checked = checked, onCheckedChange = null) } } }
クリック可能なコンポーザブルのサイズがタップ ターゲットの最小サイズより小さい場合、Compose はタップ ターゲットのサイズを大きくします。これは、コンポーザブルの境界の外側にタップ ターゲットのサイズを拡大することで実現されます。
以下の例では、非常に小さいクリック可能な Box
を作成します。タップ ターゲット領域は Box
の境界を越えて自動的に拡張されるため、Box
の横をタップしてもクリック イベントがトリガーされます。
@Composable private fun SmallBox() { var clicked by remember { mutableStateOf(false) } Box( Modifier .size(100.dp) .background(if (clicked) Color.DarkGray else Color.LightGray) ) { Box( Modifier .align(Alignment.Center) .clickable { clicked = !clicked } .background(Color.Black) .size(1.dp) ) } }
異なるコンポーザブルのタップ領域が重なり合うのを防ぐため、コンポーザブルには常に十分な大きさの最小サイズを使用する必要があります。この例では、sizeIn
修飾子を使用して内部のボックスの最小サイズを設定しています。
@Composable private fun LargeBox() { var clicked by remember { mutableStateOf(false) } Box( Modifier .size(100.dp) .background(if (clicked) Color.DarkGray else Color.LightGray) ) { Box( Modifier .align(Alignment.Center) .clickable { clicked = !clicked } .background(Color.Black) .sizeIn(minWidth = 48.dp, minHeight = 48.dp) ) } }
クリックラベルを追加する
クリックラベルを使用して、コンポーザブルのクリック動作に意味論的意味を追加できます。クリックラベルでは、ユーザーがコンポーザブルを操作したときの動作を説明します。ユーザー補助サービスでは、クリックラベルを使用して、特定のニーズがあるユーザーにアプリを説明します。
clickable
修飾子でパラメータを渡してクリックレベルを設定します。
@Composable private fun ArticleListItem(openArticle: () -> Unit) { Row( Modifier.clickable( // R.string.action_read_article = "read article" onClickLabel = stringResource(R.string.action_read_article), onClick = openArticle ) ) { // .. } }
また、clickable 修飾子にアクセスできない場合は、semantics 修飾子でクリックラベルを設定できます。
@Composable private fun LowLevelClickLabel(openArticle: () -> Boolean) { // R.string.action_read_article = "read article" val readArticleLabel = stringResource(R.string.action_read_article) Canvas( Modifier.semantics { onClick(label = readArticleLabel, action = openArticle) } ) { // .. } }
視覚要素を説明する
Image
または Icon
コンポーザブルを定義する際に、何が表示されているかを Android フレームワークが自動的に認識する方法はありません。視覚要素のテキスト説明を渡す必要があります。
ユーザーが現在のページを友だちと共有できる画面があるとします。この画面には、クリック可能な共有アイコンがあります。
Android フレームワークは、アイコンだけでは、視覚障がいのあるユーザーへの説明方法を認識できません。Android フレームワークは、アイコンに関する追加のテキスト説明を必要とします。
視覚要素を説明するには、contentDescription
パラメータを使用します。説明はユーザーに伝えられるものなので、ローカライズした文字列を使用する必要があります。
@Composable private fun ShareButton(onClick: () -> Unit) { IconButton(onClick = onClick) { Icon( imageVector = Icons.Filled.Share, contentDescription = stringResource(R.string.label_share) ) } }
一部の視覚要素は純粋に装飾的なものであるため、ユーザーに説明したくない場合があります。contentDescription
パラメータを null
に設定すると、この要素にはアクションまたは状態が関連付けられていないことを Android フレームワークに通知できます。
@Composable private fun PostImage(post: Post, modifier: Modifier = Modifier) { val image = post.imageThumb ?: painterResource(R.drawable.placeholder_1_1) Image( painter = image, // Specify that this image has no semantic meaning contentDescription = null, modifier = modifier .size(40.dp, 40.dp) .clip(MaterialTheme.shapes.small) ) }
特定の視覚要素に contentDescription
が必要かどうかは自由に決定できます。ユーザーがタスクを実行するために必要な情報が視覚要素に含まれているかどうかを検討してください。含まれていない場合は、説明を省略する方が適切です。
要素を結合する
TalkBack やスイッチ アクセスなどのユーザー補助サービスでは、ユーザーは画面上の要素間でフォーカスを移動できます。適切な粒度で要素がフォーカスされることが重要です。画面上で低レベルの単一コンポーザブルが個別にフォーカスされている場合、ユーザーは画面上を移動するために多くの操作を行う必要があります。要素を過剰に結合すると、ユーザーはどの要素が結合されているかを把握できなくなります。
clickable
修飾子をコンポーザブルに適用すると、それに含まれるすべての要素が Compose によって自動的に結合されます。これは ListItem
にも当てはまります。リストアイテム内の要素が結合され、ユーザー補助サービスからは 1 つの要素として認識されます。
論理グループを形成するコンポーザブルのセットを持つことはできますが、そのグループはクリック可能ではなく、リストアイテムの一部でもありません。それでもユーザー補助サービスがそれらを 1 つの要素として認識するようにしたい場合があります。たとえば、ユーザーのアバター、名前、追加情報を表示するコンポーザブルを想像してください。
semantics
修飾子の mergeDescendants
パラメータを使用して、これらの要素を結合するように Compose に指示できます。そうすると、ユーザー補助サービスは結合された要素のみを選択し、子孫のすべてのセマンティクス プロパティが結合されます。
@Composable private fun PostMetadata(metadata: Metadata) { // Merge elements below for accessibility purposes Row(modifier = Modifier.semantics(mergeDescendants = true) {}) { Image( imageVector = Icons.Filled.AccountCircle, contentDescription = null // decorative ) Column { Text(metadata.author.name) Text("${metadata.date} • ${metadata.readTimeMinutes} min read") } } }
ユーザー補助サービスは、コンテナ全体を一度にフォーカスして、それらのコンテンツを結合します。
カスタム アクションを追加する
以下のリストアイテムをご覧ください。
TalkBack のようなスクリーン リーダーを使用して画面の表示内容の読み上げを聴くときは、まずアイテム全体を選択し、次にブックマーク アイコンを選択します。
長いリストは、繰り返しが多い場合があります。そのような場合は、ユーザーがアイテムをブックマークするためのカスタム アクションを定義する方が適切です。ブックマーク アイコン自体の動作を明示的に削除して、ユーザー補助サービスによって選択されないようにすることも忘れないでください。そのためには、clearAndSetSemantics
修飾子を使用します。
@Composable private fun PostCardSimple( /* ... */ isFavorite: Boolean, onToggleFavorite: () -> Boolean ) { val actionLabel = stringResource( if (isFavorite) R.string.unfavorite else R.string.favorite ) Row( modifier = Modifier .clickable(onClick = { /* ... */ }) .semantics { // Set any explicit semantic properties customActions = listOf( CustomAccessibilityAction(actionLabel, onToggleFavorite) ) } ) { /* ... */ BookmarkButton( isBookmarked = isFavorite, onClick = onToggleFavorite, // Clear any semantics properties set on this node modifier = Modifier.clearAndSetSemantics { } ) } }
要素の状態を説明する
コンポーザブルでは、Android フレームワークによって使用されるセマンティクスの stateDescription
を定義して、コンポーザブルの状態を読み上げることができます。たとえば、切り替え可能なコンポーザブルの状態は、「オン」または「オフ」のいずれかです。Compose が使用するデフォルトの状態説明ラベルをオーバーライドしたい場合もあります。これを行うには、コンポーザブルを切り替え可能として定義する前に、状態説明ラベルを明示的に指定します。
@Composable private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) { val stateSubscribed = stringResource(R.string.subscribed) val stateNotSubscribed = stringResource(R.string.not_subscribed) Row( modifier = Modifier .semantics { // Set any explicit semantic properties stateDescription = if (selected) stateSubscribed else stateNotSubscribed } .toggleable( value = selected, onValueChange = { onToggle() } ) ) { /* ... */ } }
見出しを定義する
アプリは、スクロール可能なコンテナで 1 つの画面に多数のコンテンツを表示することがあります。たとえば、1 つの画面にユーザーが読んでいる記事のすべての内容を表示する場合などです。
ユーザー補助を必要とするユーザーにとって、そのような画面をナビゲートするのは困難です。ナビゲーションを補助するため、どの要素が見出しなのかを示すことができます。上記の例では、各サブセクションのタイトルをユーザー補助用の見出しとして定義できます。TalkBack のような一部のユーザー補助サービスでは、ユーザーは見出しから見出しに直接移動できます。
Compose で、あるコンポーザブルが見出しであることを示すには、セマンティクス プロパティを定義します。
@Composable private fun Subsection(text: String) { Text( text = text, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.semantics { heading() } ) }
ユーザー補助プロパティの自動テスト
アプリのセマンティック プロパティをカスタマイズする場合(たとえば、上記のユースケースに従う場合)は、自動 UI テストを使用して正確性を検証し、回帰を防ぐことができます。
たとえば、要素のクリックラベルが正しく設定されているかどうかをテストするには、次のコードを使用します。
@Test fun test() { composeTestRule .onNode(nodeMatcher) .assert( SemanticsMatcher("onClickLabel is set correctly") { it.config.getOrNull(SemanticsActions.OnClick)?.label == "My Click Label" } ) }
低レベルのカスタム コンポーザブルを作成する
より高度なユースケースとして、アプリ内の特定のマテリアル コンポーネントをカスタム バージョンに置き換える場合があります。このシナリオでは、ユーザー補助の考慮事項を念頭に置くことが重要です。たとえば、マテリアル Checkbox
を独自の実装で置き換える場合があります。その場合、このコンポーネントのユーザー補助プロパティを処理する triStateToggleable
修飾子を追加することを忘れがちです。
経験則上、マテリアル ライブラリのコンポーネントの実装を確認して、思いつく限りのユーザー補助動作を模倣する必要があります。さらに、UI レベルの修飾子ではなく、基盤修飾子を多用します。基盤修飾子は、すぐに使用できるユーザー補助の考慮事項に対応しているからです。カスタム コンポーネントの実装は、必ず複数のユーザー補助サービスでテストして、動作を検証してください。
isTraversalGroup
と traversalIndex
で移動順序を変更する
デフォルトでは、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 を使用して走査順序をさらにカスタマイズする
traversalIndex
は、TalkBack の移動順序をカスタマイズできる浮動小数点プロパティです。要素をグループ化するだけで TalkBack が正しく機能しない場合は、traversalIndex
を isTraversalGroup
と組み合わせて使用すると、スクリーン リーダーの順序をさらにカスタマイズできます。
traversalIndex
プロパティには次の特性があります。
traversalIndex
の値が低い要素が優先されます。- 正の値または負の値を指定できます。
- デフォルト値は
0f
です。 - テキストやボタンなどの画面上の要素など、スクリーン リーダーのフォーカス可能なノードにのみ影響します。たとえば、Column に
traversalIndex
のみを設定しても、Column に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 → ボトム アプリバー
詳細
Compose コードでのユーザー補助のサポートについて詳しくは、Jetpack Compose のユーザー補助の Codelab をご覧ください。
あなたへのおすすめ
- 注: JavaScript がオフになっている場合はリンクテキストが表示されます
- Compose のセマンティクス
- ジェスチャーについて
- Compose のマテリアル デザイン 2