1. はじめに
この Codelab では、Jetpack Compose を使用してアプリのユーザー補助を改善する方法を学びます。一般的なユースケースを紹介してから、サンプルアプリを順を追って改善していきます。タップ ターゲットのサイズ、内容説明、クリックラベルなどについても説明します。
視覚、色覚、または聴覚に障がいのある方、細かい作業に支障のある方、認知障がいのある方など、障がいのある多くの方々が Android デバイスを使って、普段の生活でさまざまな操作を行っています。ユーザー補助を念頭に置いてアプリを開発すると、特に上記やその他の補助が必要なユーザーのエクスペリエンスを高めることができます。
この Codelab では、TalkBack を使用し、変更したコードを手動でテストします。TalkBack は、主に視覚に障がいのある方が使用するユーザー補助サービスです。スイッチ アクセスなど、他のユーザー補助サービスでも変更後のコードをテストしてください。
Jetnews アプリでの TalkBack の動作。
学習内容
この Codelab では、以下について学びます。
- タッチ ターゲットのサイズを大きくして、細かい作業が困難なユーザーに対応する方法。
- セマンティクス プロパティとその変更方法。
- コンポーザブルに情報を与えてユーザー補助を改善する方法。
必要なもの
- ラムダを含む Kotlin 構文の使用経験。
- Compose に関する基本的な経験。この Codelab の前に Jetpack Compose の基本の Codelab を受講することをおすすめします。
- TalkBack が有効になっている Android デバイスまたはエミュレータ。
作成するアプリの概要
この Codelab では、ニュース リーダー アプリのユーザー補助を改善します。まずは重要なユーザー補助機能がないアプリから始め、そこで学んだことを活かして、ユーザー補助を必要とするユーザーにとって、より使いやすくなるように改善します。
2. 設定方法
このステップでは、シンプルなニュース リーダー アプリを構成するコードをダウンロードします。
必要なもの
コードを取得する
この Codelab のコードは、codelab-android-compose GitHub リポジトリにあります。クローンを作成するには、次のコマンドを実行します。
$ git clone https://github.com/android/codelab-android-compose
または、次の 2 つの zip ファイルをダウンロードします。
サンプルアプリを確認する
ダウンロードしたコードには、利用可能なすべての Compose Codelab のコードが含まれています。この Codelab を完了するには、Android Studio 内で AccessibilityCodelab
プロジェクトを開きます。
main
ブランチのコードから始め、ご自身のペースで Codelab を進めることをおすすめします。
TalkBack のセットアップ
この Codelab では、TalkBack を使用して変更結果を確認します。実機を使用してテストする場合は、こちらの手順で TalkBack をオンにしてください。エミュレータにはデフォルトで TalkBack がインストールされていません。Play ストアを搭載したエミュレータを選択し、Android ユーザー補助設定ツールをダウンロードしてください。
3.タップ ターゲットのサイズ
クリック、タップなど、ユーザーが操作できる画面上の要素はすべて、確実に操作できるよう十分な大きさにする必要があります。これらの要素の幅と高さは 48 dp 以上にしてください。
これらのコントロールのサイズを動的に変更する場合や、コンテンツのサイズに基づいて変更する場合は、sizeIn
修飾子を使用して寸法の下限を設定することをおすすめします。
一部のマテリアル コンポーネントでは、これらのサイズが自動的に設定されます。たとえば、Button コンポーザブルは MinHeight
が 36 dp に設定されており、また、8 dp の垂直パディングを使用します。すると、合計で必要な高さである 48 dp を満たします。
サンプルアプリを開いて TalkBack を実行すると、投稿カードの × アイコンのタップ ターゲットが非常に小さいことがわかります。このタップ ターゲットが 48 dp 以上になるようにしましょう。
左が元のアプリのスクリーンショット、右が改善後のソリューションです。
実装を確認して、このコンポーザブルのサイズを確認しましょう。PostCards.kt
を開き、PostCardHistory
コンポーザブルを探します。ご覧のとおり、この実装ではオーバーフロー メニュー アイコンのサイズが 24 dp に設定されています。
@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
// ...
Row(
// ...
) {
// ...
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cd_show_fewer),
modifier = Modifier
.clickable { openDialog = true }
.size(24.dp)
)
}
}
// ...
}
この Icon
のタップ ターゲットのサイズを大きくするには、パディングを追加します。
@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
// ...
Row(
// ...
) {
// ...
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cd_show_fewer),
modifier = Modifier
.clickable { openDialog = true }
.padding(12.dp)
.size(24.dp)
)
}
}
// ...
}
このユースケースでは、より簡単な方法でタップ ターゲットを 48 dp 以上に設定できます。それは、マテリアル コンポーネントの IconButton
を利用する方法です。
@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
// ...
Row(
// ...
) {
// ...
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
IconButton(onClick = { openDialog = true }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cd_show_fewer)
)
}
}
}
// ...
}
TalkBack で画面を見てみると、48 dp のタップ ターゲット領域が正しく表示されるようになっています。さらに、IconButton
により、要素がクリック可能であることを示すリップル インジケーションも追加されています。
4. クリックラベル
デフォルトでは、アプリ内のクリック可能要素からクリック時の動作に関する情報を得ることはできません。そのため、TalkBack のようなユーザー補助サービスでは、非常に汎用的なデフォルトの説明が使用されます。
ユーザー補助を必要とするユーザーが快適に利用できるように、この要素のクリック時の動作に関する具体的な説明を設定できます。
Jetnews アプリでは、さまざまな投稿カードをクリックして、投稿の全文を読むことができます。デフォルトでは、クリック可能な要素の内容が読み上げられ、その後に「Double tap to activate」というテキストが読み上げられます。代わりに、より具体的な「Double tap to read article」を使用するようにしましょう。元のバージョンとこの理想的なソリューションを比較すると、次のようになります。
コンポーザブルのクリックラベルの変更。変更前(左)と変更後(右)。
clickable
修飾子には、このクリックラベルを直接設定するためのパラメータが含まれています。
PostCardHistory
の実装をもう一度見てみましょう。
@Composable
fun PostCardHistory(
// ...
) {
Row(
Modifier.clickable { navigateToArticle(post.id) }
) {
// ...
}
}
ご覧のとおり、この実装では clickable
修飾子が使用されています。クリックラベルを設定するには、onClickLabel
パラメータを設定します。
@Composable
fun PostCardHistory(
// ...
) {
Row(
Modifier.clickable(
// R.string.action_read_article = "read article"
onClickLabel = stringResource(R.string.action_read_article)
) {
navigateToArticle(post.id)
}
) {
// ...
}
}
TalkBack で「Double tap to read article」と読み上げられるようになりました。
ホーム画面の他の投稿カードにも同じ汎用的なクリックラベルが付いています。PostCardPopular
コンポーザブルの実装を確認し、そのクリックラベルを更新しましょう。
@Composable
fun PostCardPopular(
// ...
) {
Card(
shape = MaterialTheme.shapes.medium,
modifier = modifier.size(280.dp, 240.dp),
onClick = { navigateToArticle(post.id) }
) {
// ...
}
}
このコンポーザブルでは、内部で Card
コンポーザブルを使用しています。これでは、クリックラベルを直接設定できません。代わりに、次のように semantics
修飾子を使用することで、クリックラベルを設定できます。
@Composable
fun PostCardPopular(
post: Post,
navigateToArticle: (String) -> Unit,
modifier: Modifier = Modifier
) {
val readArticleLabel = stringResource(id = R.string.action_read_article)
Card(
shape = MaterialTheme.shapes.medium,
modifier = modifier
.size(280.dp, 240.dp)
.semantics { onClick(label = readArticleLabel, action = null) },
onClick = { navigateToArticle(post.id) }
) {
// ...
}
}
5. カスタム アクション
多くのアプリでは、なんらかのリストが表示され、リスト内の各アイテムには 1 つ以上のアクションが含まれています。スクリーン リーダーを使用する場合、同じアクションに何度もフォーカスがあたるため、そのようなリスト内移動が煩わしいものになります。
代わりに、コンポーザブルにカスタムのユーザー補助アクションを追加できます。こうすることで、同じリスト項目に関連するアクションをグループ化できます。
Jetnews アプリでは、読むことができる記事のリストを表示しています。各リスト項目には、ユーザーがこのトピックの表示を減らしたいことを示すアクションがあります。ここでは、このアクションをカスタム ユーザー補助アクションに移動することで、リスト内の移動を改善します。
左側のデフォルトの状況では、各クロスアイコンがフォーカス可能になっています。右側のソリューションでは、そのアクションが TalkBack のカスタム アクションにあります。
投稿アイテムにカスタム アクションを追加する。変更前(左)と変更後(右)。
PostCards.kt
を開き、PostCardHistory
コンポーザブルの実装を見てみましょう。Row
と IconButton
のクリック可能プロパティに注意してください。Modifier.clickable
と onClick
を使用しています。
@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
// ...
Row(
Modifier.clickable(
onClickLabel = stringResource(R.string.action_read_article)
) {
navigateToArticle(post.id)
}
) {
// ...
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
IconButton(onClick = { openDialog = true }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cd_show_fewer)
)
}
}
}
// ...
}
デフォルトでは、Row
と IconButton
のコンポーザブルはクリック可能であるため、TalkBack によってフォーカスがあてられます。この動作はリスト内の各アイテムに発生するため、リスト内の移動中に何度もスワイプすることになります。代わりに、IconButton
に関連するアクションがカスタム アクションとしてリストアイテムに含まれるようにします。clearAndSetSemantics
修飾子を使用することで、この Icon
とやり取りしないようユーザー補助サービスに伝えることができます。
@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
// ...
Row(
Modifier.clickable(
onClickLabel = stringResource(R.string.action_read_article)
) {
navigateToArticle(post.id)
}
) {
// ...
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
IconButton(
modifier = Modifier.clearAndSetSemantics { },
onClick = { openDialog = true }
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.cd_show_fewer)
)
}
}
}
// ...
}
しかし、IconButton
のセマンティクスを削除したことにより、アクションを実行する方法はなくなりました。代わりに semantics
修飾子にカスタム アクションを追加することで、リスト項目にアクションを追加できます。
@Composable
fun PostCardHistory(post: Post, navigateToArticle: (String) -> Unit) {
// ...
val showFewerLabel = stringResource(R.string.cd_show_fewer)
Row(
Modifier
.clickable(
onClickLabel = stringResource(R.string.action_read_article)
) {
navigateToArticle(post.id)
}
.semantics {
customActions = listOf(
CustomAccessibilityAction(
label = showFewerLabel,
// action returns boolean to indicate success
action = { openDialog = true; true }
)
)
}
) {
// ...
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
IconButton(
modifier = Modifier.clearAndSetSemantics { },
onClick = { openDialog = true }
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = showFewerLabel
)
}
}
}
// ...
}
これで、TalkBack でカスタム アクションのポップアップを使用してアクションを適用できるようになります。これはリスト項目のアクションが多い場合ほど役に立ちます。
6. 視覚的要素の説明
アプリのすべてのユーザーが、アプリに表示される視覚的要素(アイコンやイラストなど)を見たり解釈したりできるわけではありません。また、ユーザー補助サービスには、ピクセルのみに基づいて視覚的要素を認識する機能もありません。そのため、デベロッパーは、アプリの視覚的要素に関する詳細情報を、より多くユーザー補助サービスに渡す必要があります。
Image
や Icon
などの視覚的コンポーザブルには、パラメータ contentDescription
があります。ここでは、その視覚的要素のローカライズ済みの説明を渡します。純粋に装飾的な要素の場合は、null
を渡します。
このアプリの場合、内容説明の一部が記事画面に表示されません。アプリを実行し、トップ記事を選択して記事画面に移動しましょう。
視覚的な内容説明を追加する。変更前(左)と変更後(右)。
情報を提供しなかった場合、左上のナビゲーション アイコンでは「Button, double tap to activate」と読み上げられます。これでは、そのボタンを有効にしたときのアクションについて、ユーザーに何も伝わりません。ArticleScreen.kt
を開きましょう。
@Composable
fun ArticleScreen(
// ...
) {
// ...
Scaffold(
topBar = {
InsetAwareTopAppBar(
title = {
// ...
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = null
)
}
}
)
}
) {
// ...
}
}
意味のある内容説明を Icon に追加します。
@Composable
fun ArticleScreen(
// ...
) {
// ...
Scaffold(
topBar = {
InsetAwareTopAppBar(
title = {
// ...
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Filled.ArrowBack,
contentDescription = stringResource(
R.string.cd_navigate_up
)
)
}
}
)
}
) {
// ...
}
}
この記事のもう 1 つの視覚的要素はヘッダー画像です。今回は、この画像は純粋に装飾的なもので、ユーザーに伝える必要のあるものは表示されません。このため、内容説明は null
に設定され、ユーザー補助サービスを使用する際、この要素はスキップされます。
画面の最後の視覚的要素はプロフィール写真です。今回は一般的なアバターを使用しているため、ここに内容説明を追加する必要はありません。執筆者の実際のプロフィール写真を使用するときは、この写真の適切な内容説明を提供するように依頼してください。
7. 見出し
この記事画面のように画面に多数のテキストがある場合、視覚に障がいのあるユーザーが目的のセクションをすばやく見つけるのは非常に困難です。それを実現するために、テキストのどの部分が見出しなのかを示します。上または下にスワイプすると、見出し間をすばやく移動できます。
デフォルトでは、見出しとして指定されているコンポーザブルはないため、ナビゲーションは使用できません。記事画面で、見出しから見出しへのナビゲーションを提供しましょう。
見出しの追加。変更前(左)と変更後(右)。
今回は、記事の見出しは PostContent.kt
で定義されています。このファイルを開いて、Paragraph
コンポーザブルまでスクロールします。
@Composable
private fun Paragraph(paragraph: Paragraph) {
// ...
Box(modifier = Modifier.padding(bottom = trailingPadding)) {
when (paragraph.type) {
// ...
ParagraphType.Header -> {
Text(
modifier = Modifier.padding(4.dp),
text = annotatedString,
style = textStyle.merge(paragraphStyle)
)
}
// ...
}
}
}
ここでは、Header
は単純な Text
コンポーザブルとして定義されています。heading
セマンティクス プロパティを設定して、このコンポーザブルが見出しであることを示すことができます。
@Composable
private fun Paragraph(paragraph: Paragraph) {
// ...
Box(modifier = Modifier.padding(bottom = trailingPadding)) {
when (paragraph.type) {
// ...
ParagraphType.Header -> {
Text(
modifier = Modifier.padding(4.dp)
.semantics { heading() },
text = annotatedString,
style = textStyle.merge(paragraphStyle)
)
}
// ...
}
}
}
8. カスタムの統合
前のステップで説明したように、TalkBack などのユーザー補助サービスでは、画面要素を要素ごとに移動します。デフォルトでは、Jetpack Compose の下位レベルのコンポーザブルのうち、1 つ以上のセマンティクス プロパティを設定するものは、それぞれフォーカスを受け取ります。たとえば、Text
コンポーザブルは text
セマンティクス プロパティを設定するため、フォーカスを受け取ります。
しかし、フォーカス可能な要素が画面上に多すぎると、ユーザーが 1 つずつ移動することになり、混乱してしまう可能性があります。代わりに、mergeDescendants
プロパティを伴う semantics
修飾子を使用するとコンポーザブルをマージできます。
記事画面を確認しましょう。ほとんどの要素には、適切なレベルのフォーカスがあります。しかし、記事のメタデータは、現在のところ複数の個別の項目として読み上げられています。これは、フォーカス可能な 1 つのエンティティにマージすることで改善できます。
コンポーザブルのマージ。変更前(左)と変更後(右)。
PostContent.kt
を開き、PostMetadata
コンポーザブルを確認しましょう。
@Composable
private fun PostMetadata(metadata: Metadata) {
// ...
Row {
Image(
// ...
)
Spacer(Modifier.width(8.dp))
Column {
Text(
// ...
)
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
Text(
// ..
)
}
}
}
}
次のように、最上位の行に対して子をマージするよう指定すると、望みの動作を実現できます。
@Composable
private fun PostMetadata(metadata: Metadata) {
// ...
Row(Modifier.semantics(mergeDescendants = true) {}) {
Image(
// ...
)
Spacer(Modifier.width(8.dp))
Column {
Text(
// ...
)
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant) {
Text(
// ..
)
}
}
}
}
9. スイッチとチェックボックス
Switch
や Checkbox
などの切り替え可能な要素は、TalkBack によって選択されると、その切り替え状態が読み上げられます。コンテキストがないと、切り替え可能な要素が何を意味するのかを把握するのが難しくなります。切り替え可能な状態を引き上げることで、切り替え可能な要素のコンテキストを含めることができ、コンポーザブル自体かそれを説明するラベルをタップすることで Switch
または Checkbox
を切り替えられるようになります。
Interests カテゴリの画面例を見てみましょう。ホーム画面からナビゲーション ドロワーを開くと、そこに移動します。[Interests] 画面には、ユーザーが購読できるトピックのリストが表示されます。デフォルトでは、この画面上のチェックボックスはラベルとは別にフォーカスされるため、コンテキストの把握が難しくなります。そこで、Row
全体を切り替え可能にします。
チェックボックスの取り扱い。変更前(左)と変更後(右)。
InterestsScreen.kt
を開き、TopicItem
コンポーザブルの実装を見てみましょう。
@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
// ...
Row(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
// ...
Checkbox(
checked = selected,
onCheckedChange = { onToggle() },
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
このように、Checkbox
には要素の切り替えを処理する onCheckedChange
コールバックがあります。このコールバックは、次のようにして Row
全体のレベルに引き上げることができます。
@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
// ...
Row(
modifier = Modifier
.toggleable(
value = selected,
onValueChange = { _ -> onToggle() },
role = Role.Checkbox
)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
// ...
Checkbox(
checked = selected,
onCheckedChange = null,
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
10. 状態の説明
前のステップでは、切り替え動作を Checkbox
から親の Row
に引き上げました。コンポーザブルの状態に関するカスタムの説明を追加することで、この要素のユーザー補助をさらに改善できます。
デフォルトでは、Checkbox
のステータスは「Ticked」と「Not ticked」のいずれかで読み上げられます。この説明は、独自のカスタムの説明に置き換えることができます。
状態説明の追加。変更前(左)と変更後(右)。
前のステップで修正した TopicItem
コンポーザブルを続けて修正します。
@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
// ...
Row(
modifier = Modifier
.toggleable(
value = selected,
onValueChange = { _ -> onToggle() },
role = Role.Checkbox
)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
// ...
Checkbox(
checked = selected,
onCheckedChange = null,
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
semantics
修飾子内で stateDescription
プロパティを使用すると、カスタムの状態説明を追加できます。
@Composable
private fun TopicItem(itemTitle: String, selected: Boolean, onToggle: () -> Unit) {
// ...
val stateNotSubscribed = stringResource(R.string.state_not_subscribed)
val stateSubscribed = stringResource(R.string.state_subscribed)
Row(
modifier = Modifier
.semantics {
stateDescription = if (selected) {
stateSubscribed
} else {
stateNotSubscribed
}
}
.toggleable(
value = selected,
onValueChange = { _ -> onToggle() },
role = Role.Checkbox
)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
// ...
Checkbox(
checked = selected,
onCheckedChange = null,
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
11. 完了
これで、この Codelab は終了です。Compose のユーザー補助について詳しく学習しました。タップ ターゲット、視覚的要素の説明、状態の説明について学習しました。クリックラベル、見出し、カスタム アクションを追加しました。カスタムのマージを追加する方法、スイッチとチェックボックスの扱い方について学習しました。ここで学んだことをアプリに応用することで、ユーザー補助が大幅に改善されます。
Compose パスウェイに関する他の Codelab や、Jetnews を含む他のコードサンプルをご確認ください。
ドキュメント
これらのトピックに関する詳細とガイダンスについては、以下のドキュメントをご覧ください。