【Flutter】VoiceOverでタップできない問題への対処【アクセシビリティ】

2024/03/27に公開

起きたこと

Flutterで開発中のモバイルアプリについて、アクセシビリティ対応を進めていたところ、VoiceOverでタップできないボタンがあることが判明しました。

一例として、Dialogmaterial.dart)の子要素にボタンがある場合、読み上げフォーカスできない状態でした。

Widget Tree

簡略化していますが、以下のような構造のWidgetです。

Semantics Tree

iOSのVoiceOverや、AndroidのTalkBackのような読み上げ機能はWidget TreeではなくSemantics Treeを参照しています。

Widget TreeSemantics Treeはイコールではなく、これが今回のタップできない問題の原因となっています。なぜイコールにならないかというと、Widgetに付与されているSemanticsによって、複数のノードがマージされ簡略化されることがあるためです。

先ほどのWidget Treeは、全てのノードがマージされ、以下のようになっていました。

Semantics.container: true

よく紹介されている対応方法として、ノードとして独立させたいWidetをSemanticsWidgetの子要素にして、containerプロパティの値をtrueにする方法があります。

Semantics(
    container:true,
    child: InkWell(...),
)

うまくいかない場合も

ただし、これだけではうまくいかない場合もあります。
今回の場合、Dialog自体の読み上げ領域が InkWellを内包しており、InkWellを読み上げしたくても上に重なっているDialogが優先されてしまいます。

したがって、InkWellをタップしたくてもDialogの読み上げが勝ってしまうので、Text1+Text2の読み上げが優先され、InkWellのタップは不可能でした。

Semantics.explicitChildNodes: true

ここでの問題は、本来読み上げ領域ではないDialogNodeとして大きすぎることで、他のWidgetの読み上げを阻害していることでした。

この場合、SemanticsWidgetのexplicitChildNodesプロパティをtrueにすることで解決が可能です。

Dialog(
    child: Semantics(
        explicitChildNodes: true,
        child: Column(...),
    )
)

explicitChildNodestrueの場合、子要素のノードが親にマージされることがなくなります。
つまり、以下のようなSemanticTreeになります。

DialogColumn自体には読み上げるラベルもボタンもないため、読み上げフォーカス領域ではなくなります。結果として、それぞれのテキストやボタン(InkWell)が読み上げ、タップ可能になります。

excludeFromSemantics

単にSemanticsNodeとして不要、いらない場合はexcludeFromSemanticsプロパティが利用できる場合があります。以下のようなWedgetに設定できます。

  • GestureDetector
  • SvgPicture, Image など画像系
  • ...など

GestureDetectorは画面全体にオーバーレイするケースなど、Semanticsとして不要な場合で利用できます。
画像系は何もしないと『画像』と読み上げされてしまうので、単に装飾的な意味合いの場合は利用しておくことで不要な読み上げを減らすことができます。

まとめ

VoiceOverでタップできない要素がある場合、以下の方法で解決できる可能性があります。

  • Semantics.container: true
  • Semantics.explicitChildNodes: true
  • excludeFromSemantics

Discussion