Jetpack Compose における TalkBack 対応

本記事は [Android Advent Calendar 2020](https://qiita.com/advent-calendar/2020/android) の 6 日目の記事です。

Jetpack Compose 以前はアクセシビリティ対応の一環として、`ImageView` に `contentDescription` を設定したり 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` では読み上げる文言の設定ができます。

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

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


## mergeDescendants

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

```kotlin
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` は要素の状態を設定することができます。

```kotlin
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` で読み上げに対応しています。

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

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


```kotlin
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` はフォーカスされている項目に対してアクションを設定することができます。

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126298/40ad881b-b7de-3737-beab-b4639bc371ed.png" width="400px" />


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

[Compose のサンプルプロジェクト](https://github.com/android/compose-samples)でも要所要所に `semantics` の実装がされているので、そちらをみるのも参考になると思います。


### ドキュメント

- [semantics](https://developer.android.com/reference/kotlin/androidx/compose/ui/semantics/package-summary#semantics) 
- [SemanticsPropertyReceiver](https://developer.android.com/reference/kotlin/androidx/compose/ui/semantics/SemanticsPropertyReceiver)