アクセシビリティ後回しにしてませんか?Flutterでできる読み上げ対応入門
はじめに
こんにちは!
株式会社アンドエーアイの荻野と申します!
今回は Flutterで「VoiceOver」や 「TalkBack」といった機能をFlutterで実現する際の知識として、「Semantics」をご紹介します!、
アクセシビリティとは
Accessibilty = 到達可能性
→ ユーザーがアプリケーションのコンテンツに到達できるか
似た概念としてユーザビリティがありますが、
ユーザビリティが「アプリが快適に使えるか」を表す概念であるのに対し、
アクセシビリティは、「そもそもユーザーがコンテンツに到達できるか」を表すという点で意味合いが異なります。
アクセシビリティの重要性
日本では「障害者差別解消法」が改正され、2024年から民間事業者にも合理的配慮が義務化されました。
国際的にもアクセシビリティ規格が多数あり、今日ではアクセシビリティは単なるオプションではなく「全てのユーザに情報を届ける責任」が開発者にも求められるようになりました。
今回は主なアクセシビリティのうち、スクリーンリーダーへの対応について、解説を行います。
Flutterにおけるスクリーンリーダー対応の概要
スクリーンリーダーとは
画面上の文字やUIを音声で読み上げてくれる支援技術です。
Android では 「TalkBack」、iOS では「VoiceOver」という機能がそれぞれ搭載されています。
Semanticsウィジェット
FlutterではSemantics
ウィジェットを使用し、UIに意味を付与することで、適切な画面の読上げを実現します。
Semantics
は子(child)の他にあらゆる属性のフラグを持ちます。
Semantics
でパラメータを指定することで、特定のウィジェットにUIパーツの種類や処理の説明を追加できます。
例:
enable
→ 有効か無効か
button
→ ボタンかどうか
Semanticsのパラメータ
Semantics({
Key? key,
Widget? child,
bool container = false,
bool explicitChildNodes = false,
bool excludeSemantics = false,
bool blockUserActions = false,
bool? enabled,
bool? checked,
bool? mixed,
bool? selected,
bool? toggled,
bool? button,
bool? slider,
bool? keyboardKey,
bool? link,
Uri? linkUrl,
bool? header,
int? headingLevel,
bool? textField,
bool? readOnly,
bool? focusable,
bool? focused,
bool? inMutuallyExclusiveGroup,
bool? obscured,
bool? multiline,
bool? scopesRoute,
bool? namesRoute,
bool? hidden,
bool? image,
bool? liveRegion,
bool? expanded,
int? maxValueLength,
int? currentValueLength,
String? identifier,
String? label,
AttributedString? attributedLabel,
String? value,
AttributedString? attributedValue,
String? increasedValue,
AttributedString? attributedIncreasedValue,
String? decreasedValue,
AttributedString? attributedDecreasedValue,
String? hint,
AttributedString? attributedHint,
String? tooltip,
String? onTapHint,
String? onLongPressHint,
TextDirection? textDirection,
SemanticsSortKey? sortKey,
SemanticsTag? tagForChildren,
VoidCallback? onTap,
VoidCallback? onLongPress,
VoidCallback? onScrollLeft,
VoidCallback? onScrollRight,
VoidCallback? onScrollUp,
VoidCallback? onScrollDown,
VoidCallback? onIncrease,
VoidCallback? onDecrease,
VoidCallback? onCopy,
VoidCallback? onCut,
VoidCallback? onPaste,
VoidCallback? onDismiss,
MoveCursorHandler? onMoveCursorForwardByCharacter,
MoveCursorHandler? onMoveCursorBackwardByCharacter,
SetSelectionHandler? onSetSelection,
SetTextHandler? onSetText,
VoidCallback? onDidGainAccessibilityFocus,
VoidCallback? onDidLoseAccessibilityFocus,
VoidCallback? onFocus,
Map<CustomSemanticsAction, VoidCallback>? customSemanticsActions,
ui.SemanticsRole? role,
})
アプリのSemanticsを可視化する
iOS(Voice Over)の場合
実機で確認、もしくはシミュレータの場合、Xcodeの Xcode > Open Developer Tool > Accesibility Inspector
を使用します。
Android(Talk Back)の場合
実機で確認、もしくはエミュレータの場合、Play StoreからAndroid ユーザー補助設定ツールをインストールします。
アプリ内でSemantics表示する場合
WidgetApp
, MaterialApp
, CupertinoApp
のshowSemanticsDebugger
を有効にすることでアプリ内のSemantics
を可視化出来ます。
実装方法
標準のコンポーネントを使用する場合
結論からいうと標準のコンポーネントは、アクセシビリティが厳格に要件があるなどの場合を除いて、そのままでも特に問題ありません。
基本的にパーツ毎にタイプや、内部のText
などを統合したSemantics
を提供する処理が組み込まれているため、特に対策せずともある程度のアクセシビリティが担保されます。
上の画面では、以下のウィジェットが使用されていますが、SemanticsDebugger
を有効にすると、問題なくSemantics
が設定されていることを確認出来ます。
TextFormField
IconButton
CheckBox
Text
ElevatedButton
独自のコンポーネントを使用する場合
標準コンポーネント使用時は特に意識せずとも、Semantics
を実装できましたが、独自にコンポーネントを実装する場合は少し工夫が必要です。
以下のような独自ボタンがあったとします。
Card(
child: InkWell(
onTap: _doSomething,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/images/logo.png',
width: 64,
height: 64,
),
const Text('Tap Me'),
],
),
),
),
),
これをAccessibility Inspectorで見てみると以下のようになりました。
このように、そのまま独自コンポーネントを実装すると、コンポーネントがボタンとして実装されていたとしても、スクリーンリーダ上では本質的でない情報が優先されてしまう可能性があります。
例示したコンポーネントを修正する
ここで必要なのは以下の三点です。
① 不要なSemantics
を削除する。
② ボタンであることを認識させる。
③ どのようなボタンかを説明するためにラベルもしくは名称を設定する。
Semantics
を削除する。
① 不要な不要なSematics
を削除するにはExcludeSemantics
を使用します。
これによりラップされたウィジェット内のSemantics
はスクリーンリーダから認識されなくなります。
今回は余計なものが入らないように、ボタン全体をラップします。
ExcludeSemantics(
child: ...,
),
② ボタンあることを認識させる。
Semantics
を使用し、button
を”true”に設定します。
これによりラップされたウィジェットはスクリーンリーダからボタンとして認識されます。
Semantics(
button: true,
child: ...
)
③ どのようなボタンかを説明するためにラベルもしくは名称を設定する。
おなじくSemantics
のlabel
を設定します。
これによりラップされたウィジェットを読み上げる際、label
に渡した文言が使用されます。
今回は、ラベルとしてより詳しい説明文を渡します。
Semantics(
label: '会社HPへ移動',
button: true,
child: ...
)
修正結果
上記修正を加えた結果が、こちらになります。
Semantics(
label: '会社HPへ移動',
button: true,
child: ExcludeSemantics(
child: Card(
child: InkWell(
onTap: _doSomething,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/images/logo.png',
width: 64,
height: 64,
),
const Text('Tap Me'),
],
),
),
),
),
),
),
),
説明とボタンであることを明示できました。
Semanticsの合成
たとえば、ラベル付きのチェックボックスなど、二つのコンポーネントを組み合わせたコンポーネントがあるとします。
このような場合に、それぞれのコンポーネントが別々に読み上げられてしまうと、ユーザーから二つのコンポーネントの関係性を理解してもらえない可能性があります。
そういった場合はMergeSemantics
を使用します。
MergeSemantics(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Checkbox(value: _value, onChanged: _onChanged),
const Text('Check something'),
],
),
),
修正結果
一つのパーツとして読み上げられるようになりました。
最後に
スクリーンリーダのような支援機能は、大多数の人からすれば必要のない機能であるため、特に要件として定められていないケースも多く、気づかないうち読上げが破綻していることも少なくありません。
しかし、公共性の高いサービスがどんどんデジタル化されていく現在では、可能な限り多くの人が必要な情報にアクセス・アプリを利用できるように、これら支援機能への対応がより求められるようになっていくと考えられます。
そうしたときに、この記事がそのような方々の一助になれば幸いです。
PR
アンドエーアイでは事業拡大のため、即戦力エンジニアを募集中!Flutterだけでなく、インフラ、Web、ネイティブ開発などの知識を持つ方も歓迎します。最新技術を追い、チームに積極的に貢献できる方をお待ちしています!
採用ページ
エンジニア採用ページ
Discussion