Jetpack Compose における TalkBack 対応


本記事は Android Advent Calendar 2020 の 6 日目の記事です。

Jetpack Compose 以前はアクセシビリティ対応の一環として、ImageViewcontentDescription を設定したり View のグルーピングで TalkBack の対応をしていました。
大きな一つの View の中でレンダリングされる Jetpack Compose ではどのように TalkBack の対応をするのかについて書いていきます。

※ Jetpack Compose 1.0.0-alpha08 時点の記事で、今後大きな変更もあり得ます

Semantics

Jetpack Compose でアクセシビリティに対応するときは Semantics フレームワークを使用します。
これまでであれば読み上げツールが View のコンポーネントを識別していましたが、View のコンポーネントを操作できない Compose では Semantics を使用して構造化された UI に対して意味を与える必要があります。

また Semantics はアクセシビリティだけでなく、Jetpack Compose で UI テストをするときにも使用されます。

ref : https://developer.android.com/jetpack/compose/testing#semantics

accessibilityLabel

accessibilityLabel では読み上げる文言の設定ができます。

IconButton(
    onClick = {},
    modifier = Modifier.semantics {
        accessibilityLabel = "戻るボタン"
    }
) {
    Icon(imageVector = Icons.Default.ArrowBack)
}

これは ← のアイコンがついたボタンに Modifier#semantics でアクセシビリティに関することを設定します。
accessibilityLabel でボタンに対して読み上げて欲しい文言を設定します。
これまでの contentDescription に近いものがこれになります。

mergeDescendants

mergeDescendants は複数の要素をグループ化します。

Column(
    modifier = Modifier.semantics(mergeDescendants = true) {
    }
) {
    Text(
        text = "タイトル",
        style = MaterialTheme.typography.subtitle1
    )
    Providers(AmbientContentAlpha provides ContentAlpha.medium) {
        Text(
            text = "サブタイトル",
            style = MaterialTheme.typography.caption
        )
    }
}

mergeDescendants を true にすると要素がまとめて読み上げられます。
上記の例では 「タイトル サブタイトル」 と 2 つのテキストがまとめて読み上げられます。

accessibilityValue

accessibilityValue は要素の状態を設定することができます。

var checked by remember { mutableStateOf(false) }
Row(
    modifier = Modifier.semantics(mergeDescendants = true) {
        accessibilityValue = if (checked) {
            "チェック済み"
        } else {
            "未チェック"
        }
    }
) {
    Checkbox(checked = checked, onCheckedChange = {
        checked = it
    })
    Text("アイテム")
}

チェックボックスとテキストをグループ化し、チェックボックスの状態を accessibilityValue に設定しています。
上記の例では 「チェック済み アイテム」 と読み上げられます。

clickable

Compose でタップイベントを実装する Modifier#clickable も内部の実装では semantics で読み上げに対応しています。

Box(
    modifier = Modifier.fillMaxWidth()
        .clickable(
            onClick = {
                ...
            },
            onClickLabel = "詳細を開く"
        )
) {
    ...
}

Modifier#clickable には onClickLabelonLongClickLabel を設定でき、それぞれにタップ時に読み上げる文言を設定することができます。

val semanticModifier = Modifier.semantics(mergeDescendants = true) {
    if (enabled) {
        onClick(action = { onClick(); true }, label = onClickLabel)
        if (onLongClick != null) {
            onLongClick(action = { onLongClick(); true }, label = onLongClickLabel)
        }
    } else {
        disabled()
    }
}

Modifier#clickable の内部ではこのような実装になっており、 semantics が設定されています。

customActions

customActions はフォーカスされている項目に対してアクションを設定することができます。

Box(
    modifier = Modifier.semantics(mergeDescendants = true) {
        customActions = listOf(
            CustomAccessibilityAction("アクション") {
                // return a boolean result indicating whether the action is successfully handled
                true
            }
        )
    }
) {
    ...
}

TalkBack では選択された項目に対してジェスチャーでコンテキストメニューを呼び出して操作する場合もあります。
CustomAccessibilityAction でアクションを定義することで、コンテキストメニューでの項目を追加することができます。

Jetpack Compose における基本的な TalkBack の対応について書いてみました。
Jetpack Compose はまだ alpha とはいえアクセシビリティに対応する仕組みが用意されているので、置き換えるにしても新規で作るにしても意識しておきましょう。

Compose のサンプルプロジェクトでも要所要所に semantics の実装がされているので、そちらをみるのも参考になると思います。

ドキュメント