🧏‍♂️

Flutterアクセシビリティガイド

2023/06/23に公開

はじめに

私がアクセシビリティに触れようと思ったきっかけは、前職が医療・福祉関係だったこともあり、何かしら身体に障害にある人をエンジニアとしてもサポートできないかなと思ったのが背景としてありました。
そして、Flutterにはアクセシビリティに関して便利なツールと機能が提供されています。この記事では、Flutterのアクセシビリティについての基本的な概念を紹介し、どのようにしてこれらの機能を使用してアクセシビリティを向上させることができるかについて説明していこうと思います。

1. セマンティクスとは何か

セマンティクス、つまり「意味論」は、情報が何を意味し、何を目指しているのかを示すものです。例えば、Webサイトやアプリでは、各部分が何をするものなのか(例:ボタン、リンク、見出し等)、どのような情報を提供しているのか(例:テキスト、画像の説明等)を明示するためにセマンティクスが利用されます。

このセマンティクスあることで、私たちは何が何だか理解でき、どこをクリックすれば次のページに進めるのか、どの部分が大切な情報を伝えているのかを把握できます。

Flutterの場合、Widget(アプリケーションの各部品)に対して、それが何を表しているのか、何のためにあるのかという「意味」を設定することができます。これにより、例えば視覚障害のある方がスクリーンリーダー(画面の内容を音声で読み上げるツール)を使って、アプリケーションの各部分が何であるかを理解し、使用することができます。

2. Semantics WidgetとSemanticsNode

FlutterのSemanticsWidgetを使用すると、任意のWidgetにカスタムセマンティクスを追加することができます。これは、特定のWidgetがボタンであること、特定のアクションをトリガーすること、特定の値を持つことなど、Widgetの目的や機能を明確にするために使用されます。

https://youtu.be/NvtMt_DtFrQ

一方、SemanticsNodeは、Flutterフレームワーク内部で使用されるデータ構造で、SemanticsWidgetから生成されます。これはアクセシビリティツールがアプリケーションを理解するために使用されます。

3. Semantics.container

Semantics.containerは、あるWidgetが「意味論的なコンテナ」であることを示します。ここで言う「コンテナ」とは、あるWidgetが他のWidgetを保持・管理している状態を指します。

このSemantics.containerプロパティがtrueに設定されている場合、そのWidgetは、自分自身の下にある(つまり子供の)Widget全ての「意味」(セマンティクス情報)をまとめて管理します。そしてそれら全てをまとめて一つの意味の塊(ノード)として扱います。

この性質は、複数の小さなWidgetが合わせて一つの機能を持っている場合や、多くのWidgetが一つのグループを形成する場合に特に便利です。例えば、一連のTextWidgetが連続した文章を形成している場合などです。この場合、それぞれのTextWidget(子供たち)は各々独自の意味を持っていますが、全体としては一つの文章(親)を形成しています。このような状況で、Semantics.containerを使うと、全体としての文章の意味をスクリーンリーダーなどのツールに適切に伝えることができます。

以下の例では、2つのTextWidgetはSemanticsWidgetによって一つのセマンティクスノードとしてまとめられます。

Semantics(
  container: true,
  child: Column(
    children: <Widget>[
      Text('こんにちは'),
      Text('こんばんは'),
    ],
  ),
) 

4. ExcludeSemantics

ExcludeSemanticsWidgetを使用すると、子孫Widgetのセマンティクス情報をセマンティクスツリーから除外することができます。これは、アクセシビリティツールによる読み上げを避けたい場合などに有用です。

ExcludeSemantics(
  child: Image.asset('images/flutter_logo.png'),
)

5. MergeSemantics

MergeSemanticsWidgetは、その子Widgetの「意味」、つまりセマンティクス情報を一つのノードにまとめ上げるWidgetです。これは、複数のWidgetが合わせて一つの概念やエンティティを形成している場合に特に役立ちます。

以下コード例です

MergeSemantics(
  child: Row(
    children: [
      Text('Lights'),
      Switch(
        value: _lights,
        onChanged: (bool value) { setState(() { _lights = value; }); },
      ),
    ],
  ),
)

この例では、TextWidget('Lights')とSwitchWidget(照明のスイッチ)が一緒に「照明の制御」という一つの概念を形成しています。しかし、これらは元々は別々のWidgetであり、それぞれが独自のセマンティクス情報を持っています。

ここでMergeSemanticsを使用すると、これら二つのWidgetのセマンティクス情報を一つにまとめることができます。結果として、スクリーンリーダーはこれを一つのエンティティ、「照明の制御」つまり、「ライトのスイッチ」であると解釈します。これにより、ユーザーはTextWidgetとSwitchWidgetが一緒に一つの概念を形成していることを理解できます。

6. アクセシビリティのテスト

Flutterでは、アクセシビリティのテストを行うために、事前に定義されたいくつかのガイドラインが提供されています。それらのガイドラインは以下の通りです:

  • androidTapTargetGuideline: タップ可能なノードが最低でも48x48ピクセルのサイズを持つことを確認します。
  • iOSTapTargetGuideline: タップ可能なノードが最低でも44x44ピクセルのサイズを持つことを確認します。
  • textContrastGuideline: WCAGにより指定されたテキストのコントラスト要件に対するガイドラインを提供します。
  • labeledTapTargetGuideline: タップまたは長押しのアクションを持つ全てのノードにラベルが存在することを強制します。

これらのガイドラインは、Widgetがユーザーインターフェースとして適切なアクセシビリティを提供しているかを検証するために使用されます。

例えば、以下のテストは、TextButtonがタップ可能なターゲットガイドライン(iOSおよびAndroid)を満たすことをテストします

成功例:

testWidgets('meetsGuideline: iOSTapTargetGuideline and androidTapTargetGuideline ✅', (tester) async {
  final handle = tester.ensureSemantics();
  final content = SizedBox.square(
    dimension: 50.0,
    child: TextButton(onPressed: () {}, child: Text('TapButton')),
  );
  await tester.pumpWidget(MaterialApp(home: Center(child: content)));
  await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));
  await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
  handle.dispose();
});

失敗例:

testWidgets('does not meet Guideline: iOSTapTargetGuideline and androidTapTargetGuideline ❌', (tester) async {
  final handle = tester.ensureSemantics();
  final content = SizedBox.square(
    dimension: 30.0, // このボタンのサイズは、iOSTapTargetGuidelineおよびandroidTapTargetGuidelineの要件を満たしていないため、テストは失敗します。
    child: TextButton(onPressed: () {}, child: Text('SmallButton')),
  );
  await tester.pumpWidget(MaterialApp(home: Center(child: content)));
  await expectLater(tester, isNot(meetsGuideline(iOSTapTargetGuideline)));
  await expectLater(tester, isNot(meetsGuideline(androidTapTargetGuideline)));
  handle.dispose();
});

7. showSemanticsDebugger

「セマンティクスツリー」を視覚化することができます。
このプロパティをtrueに設定すると、アプリをどのようにアクセシビリティツールを解釈しているのかを直感的に理解することが可能になります。
例えば、ボタンが正しいラベルを持っているか、ボタンが適切にグループ化されているか、またボタンが現在どのような状態(例えば「押された」、「無効化」など)を示しているかなどを確認することができます。

MaterialApp(
  home: MyApp(),
  showSemanticsDebugger: true, // このプロパティをtrueに設定
);

まとめ

いかがでしたでしょうか。
アクセシビリティはアプリケーションでも重要であり、すべてのユーザーがアプリケーションを最大限に活用できるようにするためには必要不可欠と私は感じます。
そして、意外とエンジニア経験が長い方でもアクセシビリティ領域にはあまり知見がなかったりと、意識しなければいけないという事実もあると思います。
Flutterでは、Widgetのセマンティクスを適切に設定することで、スクリーンリーダーやその他のアクセシビリティツールが正確にアプリケーションの情報をユーザーに伝えることができます。これにより、視覚障害や運動障害のあるユーザーでも、アプリケーションをスムーズに操作し、必要な情報を得ることができます。
また、アクセシビリティは障害者の方だけに提供するものと、私はずっと誤認していましたが、障害の有無に関わらず幅広い人が不自由なく使えることを目指すものだと今回、キャッチアップして認識を改めることができました。
ぜひ、皆さんもアクセシブルな開発を!

Discussion