【Flutter】VoiceOverでタップできない問題への対処【アクセシビリティ】
起きたこと
Flutterで開発中のモバイルアプリについて、アクセシビリティ対応を進めていたところ、VoiceOverでタップできないボタンがあることが判明しました。
一例として、Dialog
(material.dart
)の子要素にボタンがある場合、読み上げフォーカスできない状態でした。
Widget Tree
簡略化していますが、以下のような構造のWidgetです。
Semantics Tree
iOSのVoiceOverや、AndroidのTalkBackのような読み上げ機能はWidget Tree
ではなくSemantics Tree
を参照しています。
Widget Tree
とSemantics Tree
はイコールではなく、これが今回のタップできない問題の原因となっています。なぜイコールにならないかというと、Widgetに付与されているSemantics
によって、複数のノードがマージされ簡略化されることがあるためです。
先ほどのWidget Tree
は、全てのノードがマージされ、以下のようになっていました。
Semantics.container: true
よく紹介されている対応方法として、ノードとして独立させたいWidetをSemantics
Widgetの子要素にして、container
プロパティの値をtrue
にする方法があります。
Semantics(
container:true,
child: InkWell(...),
)
うまくいかない場合も
ただし、これだけではうまくいかない場合もあります。
今回の場合、Dialog
自体の読み上げ領域が InkWell
を内包しており、InkWell
を読み上げしたくても上に重なっているDialog
が優先されてしまいます。
したがって、InkWell
をタップしたくてもDialog
の読み上げが勝ってしまうので、Text1+Text2
の読み上げが優先され、InkWell
のタップは不可能でした。
Semantics.explicitChildNodes: true
ここでの問題は、本来読み上げ領域ではないDialog
がNode
として大きすぎることで、他のWidgetの読み上げを阻害していることでした。
この場合、Semantics
WidgetのexplicitChildNodes
プロパティをtrue
にすることで解決が可能です。
Dialog(
child: Semantics(
explicitChildNodes: true,
child: Column(...),
)
)
explicitChildNodes
がtrue
の場合、子要素のノードが親にマージされることがなくなります。
つまり、以下のようなSemanticTreeになります。
Dialog
やColumn
自体には読み上げるラベルもボタンもないため、読み上げフォーカス領域ではなくなります。結果として、それぞれのテキストやボタン(InkWell)が読み上げ、タップ可能になります。
excludeFromSemantics
単にSemanticsNodeとして不要、いらない場合はexcludeFromSemantics
プロパティが利用できる場合があります。以下のようなWedgetに設定できます。
- GestureDetector
- SvgPicture, Image など画像系
- ...など
GestureDetectorは画面全体にオーバーレイするケースなど、Semanticsとして不要な場合で利用できます。
画像系は何もしないと『画像』と読み上げされてしまうので、単に装飾的な意味合いの場合は利用しておくことで不要な読み上げを減らすことができます。
まとめ
VoiceOverでタップできない要素がある場合、以下の方法で解決できる可能性があります。
Semantics.container: true
Semantics.explicitChildNodes: true
excludeFromSemantics
Discussion