【Flutter】キー入力イベント処理の周辺知識とWidget選定
🎄 この記事は2021年 Flutterアドベントカレンダー 12月8日分の記事です 🎅
Photo by Wilhelm Gunkel on Unsplash
概要
Flutterにはキーボード入力イベントを処理するAPIが複数混在しています。Widget系だけでも KeyboardListener
EditableText
Shorcuts/Actions
FocusableActionDetector
などがあり、周辺APIの整理・更新も活発に行われているようです。
このような状況のためか、キー入力処理周りの知見をネット上で探そうにも情報自体が少なく、全体像をつかむのに苦労します(そもそもFlutterの利用がタッチデバイス向けアプリ中心という事情もあるかと思います)。
という個人的体験から、キー入力イベント周りの知識、とりわけ関連ウィジェットの使い分けに関して本記事でまとめてみることにしました。
周辺知識まとめ
本題に入る前に前提となる知識を軽くおさらいします。
◆キー入力イベントを処理するAPIの系統
現状では以下の2つのAPI系統が混在しているようです。
- RawKeyboard + RawKeyEvent 系統
- HardwareKeyboard + KeyEvent 系統
たとえば、Focus
ウィジェットのonKey
は 1. のRawKeyEvent
を受け取るコールバックですが、同onKeyEvent
は 2. のKeyEvent
を受け取るコールバックになっています。またウィジェットレベルでも 1. のRawKeyboardListener
と 2. のKeyboardListener
が併存しています。
ただ、RawKeyboard
系統は「古い方のAPI」であり将来的にdeprecatedとなる予定で、今後はHardwareKeyboard
系統のAPIに徐々に移行していくそうです。
まだFlutter内部でも部分的に古いAPIを使用しているようなのですが、以下のissueやドキュメントには利用し始めても問題ない点、積極使用を推奨する点などが書かれているため、本記事では同じウィジェット(もしくは同様の異なるウィジェット)で両API系統が混在している場合はHardwareKeyboard
系統のみを提示します。
※ RawKeyboard
系統とHardwareKeyboard
系統の細かい相違点については上記リンク先に詳細が書かれています。
◆キー入力イベントの種類
そもそもFlutterにおける「キー入力イベント」とは。
「キー入力イベント」を表す抽象クラスに KeyEvent (新系統)があります。それによると以下の3つがイベントのベースとなるとのこと。
-
KeyDownEvent
- ユーザーが●●のキーを押した -
KeyRepeatEvent
- ユーザーが●●のキーを押しっぱなしにしている -
KeyUpEvent
- ユーザーが●●のキーを解放した
このイベント情報は、KeyEventManagerがプラットフォーム側から受け取ったキー入力情報が元になっています。そこから情報の加工が進み、Flutterのより高レベルのAPIで「修飾キー(ctrl/opt/cmd/shiftなど)との組み合わせ」や「同時入力」が判定されるなどして情報がアプリで利用されていきます。
◆キー入力情報の用途
ユーザーによるキー入力をアプリで利用する用途としては、概ね以下の3つになるかと思います。
- テキスト入力(イベントを文字情報として出力)
- アプリの機能を呼び出すショートカット(細かい検知は不要、修飾キーとの組み合わせもあり)
- ゲーム等における繊細もしくは複雑な操作(細かいキーの押し上げ、特別な組み合わせなどの検知)
ただ、これらの境界は曖昧なところもあります。ゲームによっては 2. でも十分な場合もあるかと思うので参考程度です。
◆「キー」の種類
キーボードの「キー」を表すクラスにはLogicalKeyboardKeyとPhysicalKeyboardKeyの2種類があります。
LogicalKeyboardKey
が押されたキーの結果を表すのに対し、PhysicalKeyboardKey
はQWERTYキーボードにおけるキーを表します。
たとえば、フランスで使われるキーボードにはAZERTY配列のものもあり、QWERTYに対して「Q」と「A」の位置が逆になっています。
このようなケースでは、「capslockキーの右隣にあるキー(QWERTY配列ではAだけどAZERTY配列ではQ)でゲームキャラクターを左に移動させたい」という場合にLogicalKeyboardKey.keyA
で移動の判別をしてしまうと、Qを押すことになるAZERTY配列のキーボードでは対応できません。
なのでこのような場合はPhysicalKeyboardKey.keyA
を指定する必要があります。
◆キー入力とフォーカス(Focus)の関係
Flutterにおいて、あるウィジェットが「フォーカス状態にある」とは「キー入力を受け付けられる状態にある」という意味合いになります。
たとえば、enterキーをTextField
ウィジェットにフォーカスして入力したのか、ElevatedButton
だったのかでは同じキー入力イベントでも結果が異なってしまいます。
そのためキー入力情報を処理するということは同時に、フォーカスをコントロールするということでもあり、この2つは切っても切れない関係にあります。このことはFocusウィジェットがonKeyEvent
プロパティを持つことからもわかります。
タッチデバイス向けのアプリにおいてはあまり意識することはないですが、ユーザーからのキー入力情報を利用するアプリにおいてフォーカスは重要な要素です。
フォーカスを理解するにはこのリンク先の内容が一番だと思います。
中でも、用語解説のパートが理解にとても役立ちました。以下、その意訳です。
- Focus tree - ウィジェットツリーのうち、ボタンなどフォーカス可能なウィジェットだけを抜き出したツリー。
- Focus node - フォーカスツリーを構成するもの。フォーカスノードは 4. のフォーカスチェーンの中にあるとき、「フォーカスを持っている状態」であると言えることができる。そしてこの状態のときに限ってキー入力イベントを受け付けることができる。
- Primary focus - フォーカスを持っているノードのうち、フォカスツリーの一番末節にあるノードのフォーカスのこと。この末節から根っこに向かって(フォーカスチェーンに沿って)親ノードにキー入力イベントが伝わっていく。
- Focus chain - フォーカスツリーの「フォーカスを持つ末節(プライマリーフォーカス)」から根っこのノードにかけてのノード群。
- Focus scope - フォーカスノードのうち、他のノードを束ねてグループを作り、そのグループのみがフォーカスを持つことを許すことができる特別なノード(元締め的な)。グループのうちどのノードが直前にフォーカスを持っていたかを記憶している(「現フォーカスを持つノード」を含むウィジェットがツリーから削除された場合に、直前のノードにフォーカスを持たせられる)。
- Focus traversal - フォーカス可能なノード群の中でフォーカスが移動する順番のこと(FocusTraversalPolicyによって決まる)。tabキーを押してフォーカスを移動するのが一般的。
また、すべてのウィジェットがフォーカスを持てる(フォーカスノードを保持する)わけではなく、テキストフィールド系やボタン系のウィジェットなどあらかじめフレームワーク側で決められていますが、フォーカスノードを持てないウィジェットをFocus
ウィジェットなどでラップすることで擬似的に持たせることができます。
キー入力イベントを処理するウィジェットたち
この記事の本題?です。
前述の用途に沿って、使うべきウィジェットを挙げるとしたら以下のようになるかと思います。
-
テキスト入力(イベントを文字情報として出力)
👉EditableText
/TextField
/TextFormField
/CupertinoTextField
💬 これは当たり前ですね。。唯一ソフトウェアキーボードにも対応。
-
アプリの機能を呼び出すショートカット(細かい検知は不要、修飾キーとの組み合わせもあり)
👉Shortcuts
+Actions
+ フォーカス可能なウィジェット /FocusableActionDetector
💬 詳細は以前書いたこちらの記事を読んでください。細かい上げ下げには対応できませんが、単独キー・修飾キーを含むキーの組み合わせに対応できます。
-
ゲーム等における繊細もしくは複雑な操作(細かいキーの押し上げ、特別な組み合わせなどの検知)
👉KeyboardListener
/Focus
/(ウィジェットじゃないけど)HardwareKeyboard.instance
💬 2.を兼ねることもできますが、個人的にはこの用途に使うのがいいのかなと思います。キーを押したか・上げたか・押しっぱなしにしたかによって処理を変えられるため、たとえばタイピングゲームとか、バーチャルピアノとかでしょうか。
ちなみにKeyboardListener
はFocus
のラッパーウィジェットです。
最後に
続編の記事でデモアプリを作りながらFocusableActionDetector
やKeyboardListener
の具体的な使い方を紹介する予定です。
最後まで読んでいただきありがとうございました。
Discussion