社内でアクセシビリティを考慮したアプリに関わる可能性があるので先行で調査することにしました。iOSでの記事も執筆しているのでOS間での違いも併せてみていただければと思います。
アクセシビリティ機能(TalkBack)とは
視覚、色覚、または聴覚に障がいのある方、細かい作業に支障のある方、認知障がいのある方など、障がいのある多くの方々が Android デバイスを使って、普段の生活でさまざまな操作を行っています。ユーザー補助を念頭に置いてアプリを開発するには、特に上記やその他の補助が必要なユーザー向けに、アプリの便宜性を高めてください。
https://developer.android.com/guide/topics/ui/accessibility/apps
AndroidでもiOSと同様に障がいのある方でも便利に使えるようにアクセシビリティ機能に力を入れているようで、2022年5月にはPlayStore上にあるAccessible(アクセシビリティ機能を備えた)アプリについてAllyタグをつけることを発表しています。タグの詳細についてはリンクを参照ください。
様々なアクセシビリティ機能がある中で、視覚に障がいのある方に有効なTalkbackの実装について今回は調べました。
TalkBackを有効/無効にする方法
- 音量ボタンの大小両方を3秒ほど長押し(押すたびにオンオフが切り替わる)
- Googleアシスタントに「TalkBack をオン(オフ)にして」と言う
- デバイスの設定アプリで
設定 > ユーザー補助 > Talkback
を選択し、「TalkBackの使用」をオン(オフ)にする
この3パターンがあります。
注意点としてTalkBackはデフォルトでパスワードを読み上げるようです。対策として TalkBackの設定>詳細設定>パスワードの読み上げ をオフ
にすることでイヤフォンを利用している時のみ読み上げられます。
また、Androidの中でも独自にカスタマイズされているOSの場合、上の設定項目の名称や順番などは変わっている時があるので、お手元にあるAndroidで一度項目を探してみると良いかもしれません。この際に使ってみてください。
私の手元にあるXperiaⅢではGoogleアシスタントに言っても反応はするが機能はしませんでした。原因はわからず、音量ボタン大小同時押しで切り替えています。
基本的な操作方法について
使う際の基本的な操作方法は以下のようになっています。
動作 | 実際の操作 |
---|---|
フォーカスの移動 | 1本指で左右スワイプ動作 |
要素の選択 | 1本指でタップ動作 |
要素の決定 | 1本指でダブルタップ動作 |

読み上げ
読み上げの対象
OSから提供されているUI要素はImageView
やImageButton
などを除いてデフォルトで読み上げられます。
デフォルトで読み上げられないものは情報が画像として伝えられており、コンテンツ ラベルが設定しない限り存在していないことから読み上げられないので、Viewのandroid:contentDescription
属性を利用して、設定する必要があります。
ちなみにAndroidStudioでImageViewに対してcontentDescription
属性を設定していない場合、このような警告が表示されます。
Suppress:Add tools:ignore="ContentDescription"attribute
を実行すれば警告は消えますができる限り対応した方がAccessible
なアプリと言えるでしょう。

検証用に作成したアプリが以下の動画になります。画面中段のswitchBottomと言うラベルの左にあるアイコン画像だけcontentDescription
の設定をしていません。
画面録画の技術的な問題でTalkbackの音声入りの動画が撮れなかったので、画面下部のトースト表示で話している内容を表示しています。
これはTalkBackの設定 > 詳細設定 > デベロッパー向けの設定 > 音声出力の表示
をオンに設定すると表示できます。
description
を設定していないImageViewがTalkBackの対象に選ばれずに無視されていることがわかりますでしょうか。このようにdescription設定のないImageViewはデフォルトでは読み上げる内容がないので選ばれません。
コンテンツラベルを設定する属性
android:contentDescription
先に説明したように情報を画像で伝えるviewはこのandroid:contentDescription
属性を使用してコンテンツラベルを設定できます。後で紹介するLinearLayout
などの親クラスであるViewGroup
を使う場合このViewGroup
に対してこの属性を設定することもあります。
Label等にcontentDescription
を設定しなかった場合Labelが持つtext属性を読みますが、設定した場合はcontentDescription
の内容を反映します。
例として以下の画像の緑の枠を読み上げることを考えます。レイアウトXMLファイルの当該箇所では、textが読み上げ
、contentDescription
が読み上げサンプル
に設定されています。実際にTalkBackが話す内容は読み上げサンプル
になっています。
このようにcontentDescription
を優先して読み上げます。
<Switch
android:id="@+id/linearFocusableSwitch"
style="@style/defaultSwitchStyle"
android:contentDescription="読み上げサンプル"
android:text="読み上げ"
app:layout_constraintBottom_toTopOf="@id/LinearLayoutTop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/seekBar" />

android:hint
EditText
またはTextView
ではandroid:contentDescription
をコンテンツラベルとして使用できません。代わりにandroid:hint
を利用して目的を設定します。
例として以下の画像上部の緑の枠のテキストボックスを読み上げます。レイアウトXMLファイル内で指定した通り今日の天気を入力
と発声していることがわかります。
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
style="@style/defaultSwitchStyle"
app:layout_constraintBottom_toTopOf="@id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="今日の天気を入力" />
</com.google.android.material.textfield.TextInputLayout>

どのように読み上げられるか
iOSでは規定の順番がありましたが、Androidではユーザーが選択できるようになっています。
要素は状態
、名前
、種類
の三種類あります。順序を並べ替えるにはTalkBackの設定 > 詳細設定 > 説明する要素の順序
で並び替えることができます。


基本的にこのデフォルトである状態
、名前
、種類
で問題ないと思われます。先のcontentDescription
の項でも紹介した右写真の緑の四角で囲まれた要素を読み上げてみようと思います。
各設定で読み上げてみた結果を下に示します。
状態、名前、種類
OFF、読み上げサンプル、スイッチ
種類、名前、状態
スイッチ、読み上げサンプル、OFF
名前、種類、状態
読み上げサンプル、スイッチ、OFF
デフォルトでは先に状態が分かり、次に名前、最後にスイッチとわかりますが他の2つでは状態を最後まで聞かないと先に行けません。いくつも要素を辿っていくユーザーからすると端的に物の名前、状態が分かれば良いのでデフォルトが望ましいかと思われます。
フォーカス遷移の順番
基本は左から右、上から下に遷移します。画面の設計上縦に並んでいる2要素をまとめて読んでほしい場合があるかと思います。その場合は、LinearLayout
などViewGroup
で要素を包んであげることでまとめて1要素と見做すことができます。以下に例を示します。
<LinearLayout
android:id="@+id/LinearLayoutLeft"
style="@style/verticalLinearStyle"
android:contentDescription="@string/start_Description"
app:layout_constraintEnd_toStartOf="@+id/LinearLayoutRight"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/LinearLayoutBottom">
<ImageView
android:id="@+id/imageViewLeft"
style="@style/defaultImageViewStyle"
android:src="@android:drawable/sym_def_app_icon"
tools:srcCompat="@android:mipmap/sym_def_app_icon" />
<Switch
android:id="@+id/switchLeft"
style="@style/linearSwitchStyle"
android:text="switchLeft" />
</LinearLayout>
このような構成のviewを用意した際にLinear Layoutにフォーカスすることを示すには
val layout = findViewById<LinearLayout>(R.id.LinearLayoutLeft)
val image = findViewById<ImageView>(R.id.imageViewLeft)
val switch = findViewById<Switch>(R.id.switchLeft)
switch.isFocusable = false
image.isFocusable = false
layout.isFocusable = true
と書きます。同様に、レイアウトを構成しているxml上でもfocasable
が指定可能なので、対象Viewのフォーカス可否が確定している場合はxml上で指定してしまう方が良いでしょう。
今回の例のようにSwitchのようなクリック動作がある場合は
val switch = findViewById<Switch>(R.id.switchLeft)
val layout = findViewById<LinearLayout>(R.id.LinearLayoutLeft)
layout.setOnClickListener { switch.toggle() }
このようにsetOnClickListener
を設定すると、同様にフォーカス可能と認識されます。今回はSwitchにLinearLayout
のクリックイベントを流すように書いているので実質タップ領域が拡張されていますがTalkBackが有効かどうかをチェックして必要な場合にのみ拡張する、など必要に応じた対応をすると良いでしょう。
ところでこのLinearLayoutにフォーカスした状態から次の要素に移動してみると以下の動画のように、switchのisFocusable
をfalse
にしたはずなのにフォーカスされます。
これをフォーカスさせないためにSwitchのandroid:importantForAccessibility
をno
に設定します。こうすることでswitch側のアクセシビリティ情報は無視されますがLinearLayout側で説明することで問題なく利用できます。
val switch = findViewById<Switch>(R.id.switchLeft)
switch.isFocusable = false
switch.importantForAccessibility = 2
//ref. https://developer.android.com/reference/android/view/View.html#attr_android:importantForAccessibility
まとめ
事前に検討が必要な点
- Talkbackが読み上げるべきviewと必要のない装飾viewを切り分ける
- 要素が読み上げる適切なタイトルを検討する
- UI設計上でまとめて読みたいviewを洗い出す
感想
iOSを先行して調査していましたが、同じ音声案内での補助機能とあってできる機能としてはそう変わらないと感じました。気になった点としては読み上げる順序がiOSとAndroidで少し異なっていて、OS間の差が共通規格などが制定されて吸収されると、実装側としても利用者側としても違和感なくどのデバイスでも使えるだろうなと思いました。
補足
読み上げる文章に漢字を含むワードを設定することについて
contentDescription
に記事内では読み上げサンプル
などと設定していましたが、漢字を含むワードを読み上げるよう設定することの是非については賛否両論あるようです。
- スクリーンリーダーなどの音声合成エンジンが漢字を誤読しないようにあえてひらがな表記にする
- ひらがな表記にされると声の抑揚がなく聞き取りづらく、誤読は慣れているとそこまで問題にならない
アプリのユーザー層、用途など様々な要因で正確性を優先すべき場合もあるため、事前にしっかり誤読する可能性を含めた検討が行われる必要があるかと思います。
検証用アプリのレイアウトXML
展開
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
style="@style/defaultTextViewStyle"
android:text="以下のtext boxに今日の天気を入力"
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
style="@style/defaultSwitchStyle"
app:layout_constraintBottom_toTopOf="@id/button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="今日の天気を入力" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/button"
style="@style/defaultSwitchStyle"
android:text="Button"
android:contentDescription="決定"
app:layout_constraintBottom_toTopOf="@id/seekBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout" />
<SeekBar
android:id="@+id/seekBar"
style="@style/defaultSwitchStyle"
android:contentDescription="画面輝度"
app:layout_constraintBottom_toTopOf="@id/linearFocusableSwitch"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/button" />
<Switch
android:id="@+id/linearFocusableSwitch"
style="@style/defaultSwitchStyle"
android:contentDescription="読み上げサンプル"
android:text="読み上げ"
app:layout_constraintBottom_toTopOf="@id/LinearLayoutTop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/seekBar" />
<LinearLayout
android:id="@+id/LinearLayoutTop"
style="@style/horizontalLinearStyle"
android:clickable="true"
android:contentDescription="@string/top_description"
android:focusable="true"
app:layout_constraintBottom_toTopOf="@+id/LinearLayoutBottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearFocusableSwitch">
<ImageView
android:id="@+id/imageViewTop"
style="@style/defaultImageViewStyle"
android:src="@android:drawable/sym_def_app_icon"
tools:srcCompat="@android:mipmap/sym_def_app_icon" />
<Switch
android:id="@+id/switchTop"
style="@style/linearSwitchStyle"
android:text="switchTop" />
</LinearLayout>
<LinearLayout
android:id="@+id/LinearLayoutBottom"
style="@style/horizontalLinearStyle"
android:contentDescription="@string/bottom_Description"
app:layout_constraintBottom_toTopOf="@id/LinearLayoutLeft"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearFocusableSwitch">
<ImageView
android:id="@+id/imageViewBottom"
style="@style/defaultImageViewStyle"
android:src="@android:drawable/sym_def_app_icon"
tools:srcCompat="@android:mipmap/sym_def_app_icon" />
<Switch
android:id="@+id/switchBottom"
style="@style/linearSwitchStyle"
android:importantForAccessibility="no"
android:text="switchBottom" />
</LinearLayout>
<LinearLayout
android:id="@+id/LinearLayoutLeft"
style="@style/verticalLinearStyle"
android:contentDescription="@string/start_Description"
app:layout_constraintEnd_toStartOf="@+id/LinearLayoutRight"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/LinearLayoutBottom">
<ImageView
android:id="@+id/imageViewLeft"
style="@style/defaultImageViewStyle"
android:focusable="false"
android:src="@android:drawable/sym_def_app_icon"
tools:srcCompat="@android:mipmap/sym_def_app_icon" />
<Switch
android:id="@+id/switchLeft"
style="@style/linearSwitchStyle"
android:text="switchLeft" />
</LinearLayout>
<LinearLayout
android:id="@+id/LinearLayoutRight"
style="@style/verticalLinearStyle"
android:contentDescription="@string/end_Description"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/LinearLayoutLeft"
app:layout_constraintTop_toBottomOf="@+id/LinearLayoutBottom">
<ImageView
android:id="@+id/imageViewRight"
style="@style/defaultImageViewStyle"
android:src="@android:drawable/sym_def_app_icon"
tools:srcCompat="@android:mipmap/sym_def_app_icon" />
<Switch
android:id="@+id/switchRight"
style="@style/linearSwitchStyle"
android:text="switchRight" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
参考文献
https://developer.android.com/guide/topics/ui/accessibility/principles?hl=ja
https://chuff-chuff.hatenablog.com/entry/2019/12/06/003051