[読書会]ユーザー入力とアクセシビリティ

https://docs.flutter.dev/ui/adaptive-responsive/input のリファレンスブログです。

1.カスタムウィジェットのスクロールホイール

ScrollViewやListViewのようなスクロールウィジェットはデフォルトでスクロールホイールをサポートしており、ほとんどすべてのスクロール可能なカスタムウィジェットはこれらのいずれかを使用して構築されているため、これらのウィジェットでも動作します。

カスタムスクロール動作実装する必要がある場合は、スクロールホイールに対するUIの反応をカスタマイズできるListenerウィジェットを使用できます。

ホイールやタッチパッドを使ったスクロール操作の監視:
return Listener(
  onPointerSignal: (event) {
    if (event is PointerScrollEvent) print(event.scrollDelta.dy);
  },
  child: ListView(),
);

上記のコードにおいて、onPointerSignal で監視しているのは、マウスホイールやタッチパッドのスクロールイベントに対応する PointerScrollEvent です。このため、実際にキャプチャされるのは ホイールやタッチパッドを使ったスクロール操作です。

具体的には・・・
マウスホイールを使ってスクロールした場合は、このイベントで dy の値を取得できます。
タッチパッドを使ってスクロールする場合も、PointerScrollEvent が発生し、dy の値が取得されます。
ただし、スクロールバーをドラッグしてスクロールした場合は、PointerScrollEvent ではなく、PointerMoveEvent やその他の別のイベントが発生します。したがって、このコードでは、マウスのスクロールバーを手動で操作して移動した場合は、dy の値を取得できません。

もし、スクロールバーの操作も含めてすべてのスクロール操作を監視したい場合、ScrollController や他のイベントリスナーを使って、手動スクロールや他のスクロール操作を別途監視する必要があります。

2.タブの移動とフォーカスの相互作用

物理的なキーボードを使うユーザーは、タブキーを使ってアプリケーションを素早くナビゲートできることを期待し、運動や視覚に障害を持つユーザーは、キーボードナビゲーションに完全に頼ることが多い。

タブ・インタラクションには、トラバーサルと呼ばれるウィジェットからウィジェットへのフォーカスの移動方法と、ウィジェットにフォーカスが当たったときに表示されるビジュアル・ハイライトの2つの考慮事項があります。

ボタンやテキスト・フィールドのようなほとんどの組み込みコンポーネントは、デフォルトでトラバーサルとハイライトをサポートしています。 トラバーサルに含めたい独自のウィジェットがある場合は、FocusableActionDetector ウィジェットを使って独自のコントロールを作成できます。

FocusableActionDetector は、Flutter でフォーカスやアクション、ホバー、キーボード入力などのユーザーインタラクションを検出するためのウィジェットです。特に、キーボード操作やマウス・タッチ入力に基づいて、ウィジェットの状態(フォーカス、ホバー、アクションなど)を制御したい場合に使用されます。

FocusableActionDetector は、フォーカスやアクション、ホバーといったユーザーの操作に応じたインタラクションを実現するための便利なウィジェットです。
これを利用することで、キーボード操作、マウス操作、タッチ入力に対して応答性の高いインターフェースを構築することが可能です。

主な機能・・・

1.フォーカスの検出

FocusableActionDetector は、フォーカスが当たったかどうかを検出し、その状態に応じてビジュアルや動作を変更できます。これは、ユーザーがタブキーを使ってウィジェットにフォーカスを移動させた場合などに役立ちます。

2.アクションの検出

ユーザーが特定のキーを押すなどのアクションに基づいて、カスタムアクションをトリガーすることができます。actions プロパティを使って、特定のショートカットキーにアクションを関連付けることができます。

3.ホバー状態の検出

マウスカーソルがウィジェットの上にあるとき(ホバー状態)の検出を行い、これに応じて UI を変更したり、他のアクションを実行できます。

4.アクセシビリティの強化

キーボード操作やフォーカス移動に対応しており、アクセシビリティを考慮したアプリケーションに役立ちます。例えば、タブキーで移動する際に視覚的なフィードバックを追加できます。

Dart
import 'package:flutter/material.dart';

class FocusableExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FocusableActionDetector(
      onShowFocusHighlight: (focus) {
        print("Focus: $focus");
      },
      onShowHoverHighlight: (hover) {
        print("Hover: $hover");
      },
      actions: <Type, Action<Intent>>{
        ActivateIntent: CallbackAction<Intent>(
          onInvoke: (Intent intent) => print("Action triggered!"),
        ),
      },
      child: Container(
        width: 100,
        height: 100,
        color: Colors.blue,
        child: Center(
          child: Text('Focus me'),
        ),
      ),
    );
  }
}

void main() {
  runApp(MaterialApp(home: Scaffold(body: FocusableExample())));
}

(プロパティの説明)
onFocusChange
 フォーカス状態が変わったときに呼び出されるコールバック。例えば、ウィジェットがフォーカスを取得したり失ったりするときに何か処理を行いたいときに使用します。

onShowFocusHighlight
 ウィジェットがフォーカスを持っているときに、ビジュアルフィードバック(ハイライトなど)を表示するためのプロパティです。

onShowHoverHighlight
 ウィジェットがマウスカーソルのホバー状態にあるときに、ビジュアルフィードバックを表示するためのプロパティです。

actions
 キーボードや他の入力デバイスで発生するアクションをカスタマイズするためのプロパティです。ShortcutManager と組み合わせることで、特定のキーにアクションを割り当てることができます。

focusNode
 フォーカスの制御に使う FocusNode を指定します。これにより、プログラム的にフォーカスの管理が可能です。

(主な用途)
・ボタンやカスタムウィジェットのフォーカス状態を強調

 キーボード操作に応じてフォーカスを受け取ったウィジェットを強調表示したり、ユーザーに現在の状態を視覚的にフィードバックできます。

・キーボードショートカットの実装

 FocusableActionDetector を使うことで、ショートカットキーにアクションを割り当て、効率的なユーザーインターフェースを実現できます。

・アクセシビリティ対応

 キーボード操作に適したフォーカス管理や、画面リーダーでの操作性を向上させるために使われることが多いです。

2-1.トラバーサル順序の制御

FocusTraversalGroup クラスは、Flutter でフォーカスの移動をカスタマイズするために使用されるウィジェットです。特に、キーボード操作やタブキーによるフォーカス移動を管理したい場合に利用されます。複数のフォーカス可能なウィジェットが含まれている場合に、フォーカスの移動順序やグループ化を指定することができます。

(主な用途と特徴)

1.フォーカス移動の制御

 FocusTraversalGroup は、グループ内でのフォーカスの移動を制御するために使われます。通常、タブキーを押すことでフォーカスは次のウィジェットに移動しますが、このクラスを使うことで、フォーカスが移動する順序をカスタマイズできます。

2.カスタムフォーカス順序の設定

 デフォルトでは、Flutter はウィジェットのツリー構造に基づいてフォーカスを移動させますが、FocusTraversalGroup を使うと、この順序を自由に指定できます。特定の順序でフォーカスを移動させたい場合、例えば、視覚的なレイアウトの順序に合わせるなど、順序をカスタマイズできます。

3.複数のフォーカスグループ

アプリケーションに複数のフォーカスグループがある場合、それぞれのグループに対して独立したフォーカス移動を設定できます。これにより、特定のエリア内でフォーカスが循環するように設定したり、異なるエリア間の移動を制御したりできます。

FocusTraversalGroup を使ってフォーカスの移動順序をカスタマイズする例:
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('FocusTraversalGroup Example')),
        body: Center(
          child: FocusTraversalGroup(
            policy: OrderedTraversalPolicy(), // フォーカス移動の順序をカスタマイズ
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                TextField(
                  decoration: InputDecoration(labelText: 'First Field'),
                ),
                TextField(
                  decoration: InputDecoration(labelText: 'Second Field'),
                ),
                TextField(
                  decoration: InputDecoration(labelText: 'Third Field'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

(上記の主なプロパティ)
・policy
  FocusTraversalGroup の policy プロパティを使用して、フォーカス移動のポリシーを指定します。
  いくつかの標準的なポリシーがあります。
 ・ OrderedTraversalPolicy
    フォーカスがウィジェットツリーの順序に従って移動します。
 ・ WidgetOrderTraversalPolicy
    ウィジェットが画面上に描画された順序に従ってフォーカスが移動します。
 ・ ReadingOrderTraversalPolicy
    文章を読む順序に従ってフォーカスが移動します(左から右、上から下など)。

・ descendantsAreFocusable
   FocusTraversalGroup 内の子ウィジェットがフォーカス可能かどうかを制御します。
   false に設定すると、グループ内のウィジェットはフォーカスできなくなります。

(シナリオ例)
 複雑なフォームのフォーカス管理: フォーム内でフォーカスの移動順序をカスタマイズしたい場合に、FocusTraversalGroup を使用して、ユーザーが意図した順序で入力フィールドに移動できるようにします。

 ダイアログやポップアップのフォーカス制御: ダイアログやモーダルウィンドウの中で、フォーカスが適切に管理されるように設定する際に利用します。例えば、閉じるボタンやキャンセルボタンにスムーズに移動させることが可能です。

 複数のフォーカスグループの制御: 画面上に複数の独立したエリアがある場合、それぞれのエリア内でフォーカスの移動を制御するために使用します。

(まとめ)

FocusTraversalGroup は、Flutter アプリ内でフォーカスの移動順序をカスタマイズするための便利なウィジェットです。
標準のフォーカス移動順序をオーバーライドし、特定の順序でフォーカスが移動するように設定できます。
複数のフォーカスグループを作成して、それぞれに異なるフォーカス移動ロジックを設定することも可能です。

3.キーボードアクセラレータ

タブトラバーサルに加えて、デスクトップやウェブユーザーは、特定のアクションにバインドされた様々なキーボードショートカットを持つことに慣れています。 素早く削除するためのDeleteキーであれ、新しい文書を作成するためのControl+Nであれ、ユーザーが期待するさまざまなアクセラレータを考慮するようにしてください。 キーボードは強力な入力ツールなので、できる限り効率的に入力できるようにしましょう。

キーボードアクセラレータをFlutterで実現するには、目的に応じていくつかの方法があります。

TextFieldやButtonのような単一のウィジェットで、すでにフォーカスノードを持っている場合、それをKeyboardListenerやFocusウィジェットでラップして、キーボードイベントをリッスンすることができます。

Focusウィジェットで(フォーカス保有単一ウィジェットを)ラップして、キーボードイベントを監視する例:
  @override
  Widget build(BuildContext context) {
    return Focus(
      onKeyEvent: (node, event) {
        if (event is KeyDownEvent) {
          print(event.logicalKey);
        }
        return KeyEventResult.ignored;
      },
      child: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 400),
        child: const TextField(
          decoration: InputDecoration(
            border: OutlineInputBorder(),
          ),
        ),
      ),
    );
  }
}

ツリーの大部分にキーボードショートカットのセットを適用するには、Shortcutsウィジェットを使用します。

Shortcutsウィジェットでツリー全体にキーボードショートカットのセットを適用する例:
// Define a class for each type of shortcut action you want
class CreateNewItemIntent extends Intent {
  const CreateNewItemIntent();
}

Widget build(BuildContext context) {
  return Shortcuts(
    // Bind intents to key combinations
    shortcuts: const <ShortcutActivator, Intent>{
      SingleActivator(LogicalKeyboardKey.keyN, control: true):
          CreateNewItemIntent(),
    },
    child: Actions(
      // Bind intents to an actual method in your code
      actions: <Type, Action<Intent>>{
        CreateNewItemIntent: CallbackAction<CreateNewItemIntent>(
          onInvoke: (intent) => _createNewItem(),
        ),
      },
      // Your sub-tree must be wrapped in a focusNode, so it can take focus.
      child: Focus(
        autofocus: true,
        child: Container(),
      ),
    ),
  );
}

Shortcutsウィジェットは、このウィジェットツリーまたはその子ウィジェットの1つがフォーカスを持ち、表示されているときにのみショートカットを実行できるので便利です。
最後のオプションは、グローバルリスナーです。 このリスナーは、常時オンアプリ全体ショートカットや、(フォーカスの状態に関係なく表示されているときにいつでもショートカットを受け付けるパネルに使用できます。
グローバルリスナーを追加するのは、HardwareKeyboardを使えば簡単です。

HardwareKeybord を使用してグローバルリスナーを有効にする例:
@override
void initState() {
  super.initState();
  HardwareKeyboard.instance.addHandler(_handleKey);
}

@override
void dispose() {
  HardwareKeyboard.instance.removeHandler(_handleKey);
  super.dispose();
}

グローバルリスナーでキーの組み合わせをチェックするには、HardwareKeyboard.instance.logicalKeysPressedセットを使用します。
例えば、以下のようなメソッドで、指定されたキーのどれかが押されているかどうかをチェックできます。

指定されたキーのどれかが押されているかどうか、をチェックするメソッド例:
static bool isKeyDown(Set<LogicalKeyboardKey> keys) {
  return keys
      .intersection(HardwareKeyboard.instance.logicalKeysPressed)
      .isNotEmpty;
}

この2つを組み合わせると、<Shift+N>が押されたときにアクションを実行できる:

<Shift+N>が押されたときにアクションを実行する例:
bool _handleKey(KeyEvent event) {
  bool isShiftDown = isKeyDown({
    LogicalKeyboardKey.shiftLeft,
    LogicalKeyboardKey.shiftRight,
  });

  if (isShiftDown && event.logicalKey == LogicalKeyboardKey.keyN) {
    _createNewItem();
    return true;
  }

  return false;
}

静的リスナーを使う時の注意点
 ユーザーがフィールドに入力している時や、関連付けられているウィジェットが非表示になっている時に、リスナーを無効にする必要があります。
 ShortcutsやKeyboardListenerと違って、開発者が管理する必要があります。
 DeleteのためにDelete/Backspaceアクセラレータをバインドしているときに、ユーザーが入力するかもしれない子TextFieldがあると、これは特に重要になります。

4.カスタムウィジェットのマウス入力、終了、ホバー

デスクトップでは、マウスカーソルを変えて、マウスがホバーしているコンテンツに関する機能を示すのが一般的です。 例えば、ボタンにマウスカーソルを合わせるとハンドカーソルが表示され、テキストにマウスカーソルを合わせるとIカーソルが表示されるのが一般的です。

FlutterのMaterialボタンは、標準的なボタンカーソルとテキストカーソルの基本的なフォーカス状態を処理します。 (特筆すべき例外は、Materialボタンのデフォルトのスタイルを変更してoverlayColorをtransparentに設定した場合です。)

アプリ内のカスタムボタンやジェスチャーディテクタにフォーカス状態を実装します。 デフォルトの Material ボタンのスタイルを変更する場合は、キーボードのフォーカス状態をテストし、必要であれば独自に実装してください。

カスタムウィジェットの中からカーソルを変更するには、MouseRegionを使用します。

MouseRegion を使用して、カスタムウィジェットの中からカーソルを変更する例:
// Show hand cursor
return MouseRegion(
  cursor: SystemMouseCursors.click,
  // Request focus when clicked
  child: GestureDetector(
    onTap: () {
      Focus.of(context).requestFocus();
      _submit();
    },
    child: Logo(showBorder: hasFocus),
  ),
);

MouseRegionは、カスタムのロールオーバーホバー効果を作成するのにも便利です。

MouseRegionを使用して、マウスオーバー効果を作成する例:
return MouseRegion(
  onEnter: (_) => setState(() => _isMouseOver = true),
  onExit: (_) => setState(() => _isMouseOver = false),
  onHover: (e) => print(e.localPosition),
  child: Container(
    height: 500,
    color: _isMouseOver ? Colors.blue : Colors.black,
  ),
);

ボタンにフォーカスがあるときにボタンのスタイルを変更してアウトライン化する例として、Wonderous アプリのボタン・コードをご覧ください。 このアプリは FocusNode.hasFocus プロパティを変更して、ボタンにフォーカスがあるかどうかをチェックし、フォーカスがあればアウトラインを追加します。

5.視覚的な密度

例えば、タッチスクリーンに対応するために、ウィジェットの「ヒットエリア」を拡大することを検討してもよい。

入力デバイスによって精度が異なるため、ヒットする部分の大きさも異なります。 FlutterのVisualDensityクラスを使えば、アプリケーション全体のビューの密度を簡単に調整できます。例えば、タッチデバイスではボタンを大きくする(つまりタップしやすくする)ことができます。

MaterialApp の VisualDensity を変更すると、それをサポートする MaterialComponents もそれに合わせて密度をアニメーション化します。 デフォルトでは、水平方向と垂直方向の密度はどちらも 0.0 に設定されていますが、密度を任意の負または正の値に設定できます。 異なる密度を切り替えることで、UIを簡単に調整できます。

カスタムのビジュアル密度を設定するには、MaterialAppテーマに密度を注入します。

MaterialAppテーマに密度を注入して、カスタムのビジュアル密度を設定する例:
double densityAmt = touchMode ? 0.0 : -1.0;
VisualDensity density =
    VisualDensity(horizontal: densityAmt, vertical: densityAmt);
return MaterialApp(
  theme: ThemeData(visualDensity: density),
  home: MainAppScaffold(),
  debugShowCheckedModeBanner: false,
);

VisualDensity は、Flutter においてウィジェットの「密度」を制御するためのクラスです。具体的には、ウィジェットのコンテンツとその周囲のスペース(パディングやマージン)を、どの程度詰めて表示するかを指定することができます。visualDensity プロパティを使用することで、ユーザーインターフェースの密度を簡単に調整し、コンパクトなレイアウトや余裕のあるレイアウトを作成することが可能です。

Dart
VisualDensity density = Theme.of(context).visualDensity;

上記のコードでは、現在のテーマからウィジェットの visualDensity を取得しています。Theme.of(context) で現在適用されているテーマを取得し、そのテーマの密度設定を visualDensity から取得しています。

(VisualDensity の概要)
 VisualDensity は、画面に表示されるウィジェットの密度を高めたり低くしたりするためのパラメータです。画面サイズやデバイスの解像度に応じて、ウィジェットの見た目や使い勝手を調整するために役立ちます。

デフォルト値
・visualDensity のデフォルトは VisualDensity.standard で、これはデバイスの標準的な表示密度を使用します。
・例えば、タブレットやデスクトップのように広い画面では、デフォルトの密度が低く設定され、モバイルデバイスのように画面が狭い場合には高密度に設定されることがあります。

調整の例
・高密度(VisualDensity.compact)
  ウィジェット間の余白を減らし、表示領域を効率的に使いたいときに使います。
・低密度(VisualDensity.comfortable)
 ウィジェットに余裕を持たせて、よりリラックスした見た目にする場合に使用します。

コンテナは密度の変化に自動的に反応するだけでなく、密度の変化に合わせてアニメーションも行います。 これにより、カスタムコンポーネントとビルトインコンポーネントが連携し、アプリ全体のスムーズなトランジション効果が得られます。 このように、VisualDensityは単位を持たないため、ビューによって異なる意味を持つことがあります。 以下の例では、1密度単位は6ピクセルに相当しますが、これは完全にあなた次第です。 単位がない分、非常に汎用性が高く、ほとんどの文脈で機能するはずです。 マテリアルは一般的に、各ビジュアル密度単位に約4論理ピクセルの値を使用することは注目に値します。 サポートされているコンポーネントの詳細については、VisualDensity APIを参照してください。 一般的な密度の原則については、マテリアルデザインガイドを参照してください。

コメントを残す