Compose のユーザー補助

Compose で作成するアプリは、さまざまなニーズを持つ利用者に対するユーザー補助をサポートする必要があります。ユーザー補助サービスは、画面に表示される内容を、特定のニーズを持つ利用者に適した形式に変換するために使用されます。ユーザー補助サービスをサポートするため、アプリは UI 要素に関するセマンティック情報を公開する Android フレームワークの API を使用します。Android フレームワークは、このセマンティック情報をユーザー補助サービスに伝えます。個々のユーザー補助サービスは、ユーザーに対してアプリを説明するのに最も適した方法を選択できます。Android には、TalkBackスイッチ アクセスなど、いくつかのユーザー補助サービスが用意されています。

セマンティクス

Compose は、セマンティクス プロパティを使用してユーザー補助サービスに情報を渡します。セマンティクス プロパティは、ユーザーに表示される UI 要素に関する情報を提供します。TextButton のような組み込みコンポーザブルのほとんどは、コンポーザブルとその子から推測される情報をセマンティクス プロパティに入力します。toggleableclickable のようないくつかの修飾子も、特定のセマンティクス プロパティを設定します。しかし、フレームワークは、ユーザーに対して UI 要素を説明する方法を把握するために、より多くの情報を必要とする場合があります。

このドキュメントでは、Android フレームワークに対して情報を正しく説明するために、コンポーザブルに明示的に情報を追加する必要があるさまざまな状況について説明します。また、特定のコンポーザブルのためにセマンティクス情報を完全に置き換える方法についても説明します。このドキュメントは、Android のユーザー補助の基本を理解していることを前提としています。

一般的なユースケース

ユーザー補助を必要とする人がアプリを正しく使用できるようにするには、このページに記載されているベスト プラクティスに沿ってアプリを作成する必要があります。

タップ ターゲットの最小サイズを検討する

クリック、タップなど、ユーザーが操作できる画面上の要素はすべて、確実に操作できるよう十分な大きさにする必要があります。これらの要素のサイズを調整する際は、マテリアル デザインのユーザー補助のガイドラインを適切に遵守するため、最小サイズを必ず 48 dp に設定してください。

マテリアル コンポーネント(CheckboxRadioButtonSwitchSliderSurface など)は、この最小サイズを内部で設定しますが、これはコンポーネントがユーザー アクションを受け取れる場合に限ります。たとえば、CheckboxonCheckedChange パラメータを非 null 値に設定すると、幅と高さが 48 dp 以上のパディングが含まれます。

@Composable
fun CheckableCheckbox() {
    Checkbox(checked = true, onCheckedChange = {})
}

onCheckedChange パラメータを null に設定すると、コンポーネントを直接操作できないため、パディングは含まれません。

@Composable
fun NonClickableCheckbox() {
    Checkbox(checked = true, onCheckedChange = null)
}

SwitchRadioButtonCheckbox などの選択コントロールを実装する場合、通常は、クリック可能な動作を親コンテナにリフトし、コンポーザブルのクリック コールバックを null に設定して、toggleable または selectable 修飾子を親コンポーザブルに追加します。

@Composable
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
fun DefaultPreview() {
   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
fun DefaultPreview() {
   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
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
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
fun ShareButton(onClick: () -> Unit) {
  IconButton(onClick = onClick) {
    Icon(
      imageVector = Icons.Filled.Share,
      contentDescription = stringResource(R.string.label_share)
    )
  }
}

一部の視覚要素は純粋に装飾的なものであるため、ユーザーに説明したくない場合があります。contentDescription パラメータを null に設定すると、この要素にはアクションまたは状態が関連付けられていないことを Android フレームワークに通知できます。

@Composable
fun PostImage(post: Post, modifier: Modifier = Modifier) {
  val image = post.imageThumb ?: imageResource(R.drawable.placeholder_1_1)

  Image(
    bitmap = 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 つの要素として認識するようにしたい場合があります。たとえば、ユーザーのアバター、名前、追加情報を表示するコンポーザブルを想像してください。

ユーザー名を含む UI 要素のグループ。名前が選択されています。

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")
    }
  }
}

ユーザー補助サービスは、コンテナ全体を一度にフォーカスして、それらのコンテンツを結合します。

ユーザー名を含む UI 要素のグループ。すべての要素がまとめて選択されています。

カスタム アクションを追加する

以下のリストアイテムをご覧ください。

記事タイトル、著者、ブックマーク アイコンを含む一般的なリストアイテム。

TalkBack のようなスクリーン リーダーを使用して画面の表示内容の読み上げを聴くときは、まずアイテム全体を選択し、次にブックマーク アイコンを選択します。

すべての要素がまとめて選択されたリストアイテム。

ブックマーク アイコンのみが選択されたリストアイテム。

長いリストは、繰り返しが多い場合があります。そのような場合は、ユーザーがアイテムをブックマークするためのカスタム アクションを定義する方が適切です。ブックマーク アイコン自体の動作を明示的に削除して、ユーザー補助サービスによって選択されないようにすることも忘れないでください。そのためには、clearAndSetSemantics 修飾子を使用します。

@Composable
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.h5,
    modifier = Modifier.semantics { heading() }
  )
}

低レベルのカスタム コンポーザブルを作成する

より高度なユースケースとして、アプリ内の特定のマテリアル コンポーネントをカスタム バージョンに置き換える場合があります。このシナリオでは、ユーザー補助の考慮事項を念頭に置くことが重要です。たとえば、マテリアル Checkbox を独自の実装で置き換える場合があります。その場合、このコンポーネントのユーザー補助プロパティを処理する triStateToggleable 修飾子を追加することを忘れがちです。

経験則上、マテリアル ライブラリのコンポーネントの実装を確認して、思いつく限りのユーザー補助動作を模倣する必要があります。さらに、UI レベルの修飾子ではなく、基盤修飾子を多用します。基盤修飾子は、すぐに使用できるユーザー補助の考慮事項に対応しているからです。カスタム コンポーネントの実装は、必ず複数のユーザー補助サービスでテストして、動作を検証してください。

詳細

Compose コードでのユーザー補助のサポートについて詳しくは、Jetpack Compose のユーザー補助の Codelab をご覧ください。