Compose のユーザー補助機能を改善する主な手順

ユーザー補助を必要とする人々がアプリを適切に使用できるようにするには、主要なユーザー補助の要件に対応するようにアプリを設計します。

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

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

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

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

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

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

図 1. パディングなしのチェックボックス

SwitchRadioButtonCheckbox などの選択コントロールを実装する場合、通常は、クリック可能な動作を親コンテナにリフトし、コンポーザブルのクリック コールバックを 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
        )
    ) {
        // ..
    }
}

クリック可能な修飾子にアクセスできない場合は、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 つの要素として表示する必要があります。たとえば、ユーザーのアバター、名前、追加情報を表示するコンポーザブルを考えてみます。

ユーザー名を含む 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
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 で、コンポーザブルが見出しであることを示すには、その semantics プロパティを定義します。

@Composable
private fun Subsection(text: String) {
    Text(
        text = text,
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier.semantics { heading() }
    )
}

カスタム コンポーザブルを処理する

アプリの特定のマテリアル コンポーネントをカスタム バージョンに置き換える場合は、ユーザー補助に関する考慮事項を念頭に置く必要があります。

マテリアルの Checkbox を独自の実装に置き換えるとします。このコンポーネントのユーザー補助プロパティを処理する triStateToggleable 修飾子を追加し忘れても構いません。

経験上、マテリアル ライブラリのコンポーネントの実装を確認し、任意のユーザー補助動作を再現します。さらに、UI レベルの修飾子ではなく、Foundation 修飾子を多用します。これらの修飾子には、すぐに利用できるユーザー補助機能の考慮事項が含まれています。

複数のユーザー補助サービスを使用してカスタム コンポーネントの実装をテストし、その動作を検証します。

参考情報