[読書会]動作(タップ、ドラッグ、等)

 ※https://docs.flutter.dev/ui/interactivity/gestures のリファレンスブログです。

 ここでは、Flutterにおけるジェスチャーの基本的な仕組みと使用方法について説明されています。ジェスチャーとは、ユーザーが画面をタップしたり、ドラッグしたり、ピンチインピンチアウトしたりする操作(動作)のことを指し、これらのジェスチャーはユーザーインタラクションの中心的な役割を担っています。

1.Flutter のジェスチャーシステム

(1)2つのレイヤー(生のポインタイベント、ジェスチャー)

 Flutterのジェスチャーシステムは2つの異なるレイヤーから成り立っています。

  1. 生のポインタイベント(Raw Pointer Events)
    • このレイヤーは、ユーザーのデバイス画面との物理的な接触(タッチ、マウス、スタイラスなど)を処理します。
    • 主なポインタイベントには以下の4種類があります。
      • PointerDownEvent:
         ポインタが画面の特定の位置に接触した時に発生。
      • PointerMoveEvent:
         ポインタが画面上のある位置から別の位置に移動した時に発生。
      • PointerUpEvent:
         ポインタが画面から離れた時に発生。
      • PointerCancelEvent:
         ポインタの入力がアプリに対してキャンセルされたに発生(例: ポップアップが表示されるなど)。
  2. ジェスチャー(Gestures)
    • このレイヤーは、タップドラッグといった意味のあるアクションを定義します。
      これらは、複数のポインタの動きを組み合わせ表現されます。
    • 例えば、TapGestureは1つまたは複数のPointerDownEventおよびPointerUpEventを組み合わせて、タップジェスチャーを認識します。

(2)ポインタイベントとジェスチャーイベントの違い

 ポインタイベントは低レベルの情報(位置、移動、接触の有無など)を直接扱いますが、ジェスチャーイベントはこれらのポインタイベントを抽象化し、タップ、ダブルタップ、ドラッグといったより意味のある動作として扱います。ジェスチャーイベントを使用することで、より簡単にユーザーインタラクションを管理することができます。

GestureDetectorウィジェットを使ってタップやドラッグを簡単に検出する(例):
GestureDetector(
  onTap: () {
    print("Tapped!");
  },
  onPanUpdate: (details) {
    print("Dragging with delta: ${details.delta}");
  },
  child: Container(
    width: 200,
    height: 200,
    color: Colors.blue,
  ),
);

//GestureDetectorを使って、
//ユーザーがコンテナをタップした時やドラッグした時の
//動作を処理しています。

 以下、上記の詳細です。

2.ポインター

(1)主なポインタイベント

 (上記の繰り返しですが)ポインターは、ユーザーとデバイスの画面とのインタラクションに関する生のデータを表し、そのポインター・イベントには4つの種類があります。

  • PointerDownEvent
     ポインタが画面の特定の位置に接触した時に発生。
  • PointerMoveEvent
     ポインタが画面上のある位置から別の位置に移動した時に発生。
  • PointerUpEvent
     ポインタが画面から離れた時に発生。
  • PointerCancelEvent
     ポインタの入力がアプリに対してキャンセルされた時に発生(例: ポップアップが表示されるなど)。

(2)ポインタイベントの詳細

 ポインタが画面に接触(PointerDownEvent)した際、Flutterフレームワークはアプリケーション内で「ヒットテスト(hit test)」を実行し、そのポインタがどのウィジェットに接触しているのかを判定します。ヒットテストで検出された最も内側のウィジェット(ポインタが最初に接触したウィジェット)にイベントが送信されます。

 イベントは、その最も内側のウィジェットから親ウィジェットに向かって順にバブリング(伝播)され、最終的にはルート(トップレベル)のウィジェットにまで到達します。このイベントの伝播は途中でキャンセルすることはできず、全てのウィジェットがそのイベントを受け取ることができます。

(3)Listenerウィジェット(ポインタイベントの伝播)

 Listenerウィジェットを使用することで、ポインタイベントを直接的にリッスン(監視)することができます。
これはウィジェットのレイヤーから生のポインタイベントを取得したい場合に便利ですが、通常はより高レベルなジェスチャーイベントを使用することを推奨しています。

Dart
Listener(
  onPointerDown: (PointerEvent event) {
    print("Pointer down at position: ${event.position}");
  },
  child: Container(
    width: 200,
    height: 200,
    color: Colors.blue,
  ),
);

//このコードでは、Listenerウィジェットがポインタ(例えば、タッチやマウスクリック)
//がContainer上で発生した際に、その位置をログに出力します。

3.ジェスチャー

 Flutter では、ジェスチャー(Gestures)は、ユーザーの画面上での操作(タップ、ドラッグ、スワイプなど)を認識し、それに対応するアクションを定義する仕組みです。ジェスチャーは、複数のポインタイベント(PointerEvent)や、複数のポインタ(複数指タッチなど)を組み合わせて意味のあるアクションとして認識されます。

3.1 ジェスチャーとイベントの流れ

 Flutter でジェスチャーを使用する際、各ジェスチャーはライフサイクルに応じて複数のイベントを発生させます。これにより、ジェスチャーの開始start)、更新update)、終了end)などを個別に処理できます。

以下、主なジェスチャーとそれに関連するイベントの流れについて説明します。

(1)Tap(タップ)

 タップジェスチャーは、ユーザーが画面を軽くタップする操作を表します。
 GestureDetectorなどを使って簡単に処理できます。

  • onTapDown
     ポインタが画面の特定の位置に接触し、タップの可能性があることを示す。
  • onTapUp
     ポインタが接触をやめ、タップを引き起こす可能性があることを示す。
  • onTap:
     onTapDownとonTapUpの両方が発生し、結果としてタップが完了したことを示す。
  • onTapCancel:
     onTapDownが発生したが、結果としてタップが完了しなかったことを示す。

 (例)ユーザがボタンをタップして、コンテンツを開いたり、クリックイベントを発火させる操作。

(2)Double Tap(ダブルタップ)

 ダブルタップジェスチャーは、ユーザーが同じ場所を短い間隔で2回タップする操作を表します。

  • onDoubleTap
     ユーザーが素早く同じ位置を2回タップしたことを示す。

 (例)画像をダブルタップして拡大する操作。

(3)Long Press(長押し)

 長押しジェスチャーは、ユーザーが画面の特定の位置を長時間押し続ける操作を表します。

  • onLongPress:
     ポインタが同じ位置に長時間接触し続けていることを示す。

 (例)テキストを選択する際の長押し操作や、コンテキストメニューを表示する操作。

(4)Vertical Drag(垂直方向のドラッグ)

 ユーザーが画面を上下にドラッグする操作を表します。

  • onVerticalDragStart
     ポインタが接触し、垂直方向に移動を始める可能性があることを示す。
  • onVerticalDragUpdate
     ポインタが垂直方向に移動していることを示す。
  • onVerticalDragEnd
     ポインタが画面から離れ、特定の速度で垂直方向の移動を終了したことを示す。

 (例)リストビューの上下スクロールや、ページの引き伸ばし操作。

(5)Horizontal Drag(水平方向のドラッグ)

 ユーザーが画面を左右にドラッグする操作を表します。

  • onHorizontalDragStart
     ポインタが接触し、水平方向に移動を始める可能性があることを示す。
  • onHorizontalDragUpdate
     ポインタが水平方向に移動していることを示す。
  • onHorizontalDragEnd
     ポインタが画面から離れ、特定の速度で水平方向の移動を終了したことを示す。

 (例)スワイプによるアイテムの削除、またはページめくりの操作。

(6)Pan(垂直方向・水平方向の移動)

 Pan ジェスチャーは、垂直方向および水平方向の両方の移動を扱います。
 特定の方向(垂直/水平)だけのドラッグとは異なり、全方向の動きを追跡できます。

  • onPanStart
     ポインタが接触し、垂直または水平方向に移動を始める可能性があることを示す。
  • onPanUpdate
     ポインタが接触したまま垂直または水平方向に移動していることを示す。
  • onPanEnd
     ポインタが接触をやめ、特定の速度で移動を終了したことを示す。

 (例)キャンバス上の要素を自由にドラッグして移動する操作。

 注意:onPan ジェスチャーは、
    ・onHorizontalDrag onVerticalDrag のイベントが同時に設定されていると、
    競合が発生し、エラーになる可能性があります。

(7)まとめ

  • Flutter のジェスチャーシステムでは、
     ポインタイベント(生のデータ)とジェスチャー(意味のある動作)区別して扱います。
  • タップやドラッグなどのジェスチャーは、複数のポインタイベントを組み合わせて認識され、個別のライフサイクル(開始、更新、終了)に応じてイベントを処理できます。
  • GestureDetector を使って、これらのジェスチャーを簡単に監視・処理することができ、また生のポインタイベントを扱いたい場合には Listener を使います。

3.2 ウィジェットにジェスチャー検出機能を追加する

 Flutterでは、ジェスチャー(タップ、ドラッグ、スワイプなど)を検出するために、GestureDetector ウィジェットを使用することが一般的です。以下、GestureDetector の使用方法やその他のジェスチャー対応のウィジェットについての話題です。

(1)GestureDetector の使用

 GestureDetector は、Flutter のウィジェットのレイヤーでジェスチャーを検出するためのウィジェットです。これは、通常のウィジェットにジェスチャー機能を追加したい場合に使われます。GestureDetector には、以下のようなイベントを検出するプロパティが用意されています。

  • onTap
     ユーザーが画面をタップした時に呼ばれる。
  • onDoubleTap
     ユーザーが素早く同じ場所を2回タップした時に呼ばれる。
  • onLongPress
     ユーザーが画面を長押しした時に呼ばれる。
  • onPanStart
     ユーザーがドラッグ(Pan)操作を始めた時に呼ばれる。
  • onPanUpdate
     ドラッグ操作が更新された時に呼ばれる。
  • onPanEnd
     ドラッグ操作が終了した時に呼ばれる。
GestureDetector を使用したタップ検出する(例):
GestureDetector(
  onTap: () {
    print('画面がタップされました');
  },
  child: Container(
    width: 100,
    height: 100,
    color: Colors.blue,
  ),
)

//青い Container をタップすると、コンソールに
//「画面がタップされました」と表示されます。

(2)InkWell と InkResponse

 GestureDetector を使用するだけでは、Material Design のインクスプラッシュ効果」(タップしたときに広がるエフェクトを表示することはできません。Material デザインの一貫性を保ちたい場合、InkWell InkResponse を使用することをお勧めします

  • InkWell
    • タップされたときに、インクスプラッシュ効果を表示するウィジェット。
    • 通常、ボタンやアイコンなどにタップ感を追加するために使われる。
InkWell(例)
InkWell(
  onTap: () {
    print('InkWellがタップされました');
  },
  child: Container(
    width: 100,
    height: 100,
    color: Colors.green,
    child: Center(child: Text('Tap me')),
  ),
)
  • InkResponse
    • InkWell に似ているが、radius containedInkWell などのプロパティを使ってインクスプラッシュの表示方法をより詳細に制御できる

(3)GestureDetector と InkWell の使い分け

  • GestureDetector
     主にジェスチャーを検出するためのウィジェット。
     カスタムなジェスチャーイベント(ドラッグやスワイプなど)を追加したいときに便利です。
  • InkWell
     ジェスチャー検出とともに、Material デザインの一貫したビジュアル表現を追加したいときに使います。
     通常はボタンやアイコンに使われることが多いです。

(4)標準的なジェスチャー対応ウィジェット

 Flutter の Material コンポーネント(例:IconButton、TextButton、ListView)は、すでにタップやドラッグ、スワイプなどのジェスチャーに対応しています。これらのコンポーネントを使用することで、ジェスチャー対応の実装を簡単に行うことができます。

  • IconButton
     タップジェスチャーに対応し、アイコンがクリックされたときにイベントを発生させる。
  • TextButton
     テキスト形式のボタンとして、タップイベントを発生させる。
  • ListView
     スクロールやスワイプの操作に対応。

 このような標準ウィジェットを使うことで、ジェスチャー対応のインタラクションを簡単に実装できるほか、GestureDetector 組み合わせてカスタマイズすることも可能です。

(5)まとめ

  1. GestureDetector を使用して、ジェスチャーを検出する方法:
    • GestureDetector を使って、タップ、ダブルタップ、長押し、ドラッグなどのジェスチャーを検出できます。
  2. InkWell を使用して、Material デザインの効果を追加する方法:
    • インクスプラッシュ効果を表示したい場合は、InkWell を使います。
  3. 標準の Material コンポーネントならジェスチャーに対応している:
    • IconButton や TextButton などは、ジェスチャーに対応しているため、特別な設定なしでタップなどを検出できます。
  4. ジェスチャー対応のウィジェット選び:
    • ジェスチャーを検出するだけなら GestureDetector を、インクスプラッシュ効果を使いたいなら InkWell を使用することが推奨されます。

3.3 ジェスチャーの曖昧さ解消

 Flutter では、画面上の同じ領域に複数のジェスチャー認識機構が重なって存在する場合や、同じジェスチャー認識機構が異なるジェスチャーを処理するよう設定されている場合、フレームワークは 「ジェスチャーの曖昧性を解消(Gesture Disambiguation)」 する仕組みを持っています。これにより、どのジェスチャー認識機構がユーザーの操作を処理するべきかが判断され、競合が解決されます。

(1)ジェスチャーの競合が発生する例

 以下のようなシチュエーションでは、ジェスチャーの競合発生しやすくなります。

  • ListTile アイコンボタンの競合:
     例えば、ListTile ウィジェットにはリスト全体に対する onTap コールバックが設定されており、その中にある trailing プロパティのアイコンボタン(例:IconButton)も独自に onPressed コールバックを持っていることがあります。この場合、アイコンボタンの領域は ListTile の領域と重なり、どちらのジェスチャー認識機構がタップを処理するかの競合が発生します。
  • 複数のジェスチャー認識機構が同じ領域をカバーしている場合:
     GestureDetector がタップとロングプレスの両方のジェスチャーを処理するよう設定されていると、ユーザーがその領域をタップしたときに、その動作がタップとして認識されるのか、それともロングプレスとして認識されるのかが不明瞭になる場合があります。

(2)ジェスチャーアリーナの仕組み

 Flutter では、こういった競合を解決するために 「ジェスチャーアリーナ(Gesture Arena)」 というメカニズムを使用します。ジェスチャーアリーナでは、複数のジェスチャー認識機構が競合した場合、どれが勝者(winner)となり、どれが敗者(loser)となるかを決定します。

ジェスチャーアリーナルールは以下の通りです。

  1. ジェスチャー認識機構の競技開始:
     各ジェスチャー認識機構は、ユーザーが画面をタップしたり、指を置いた時点でアリーナに参加します(例:PointerDownEvent の発生時)。
  2. ジェスチャー認識機構の自己除外:
     認識機構が自己除外を宣言し、アリーナから退場することができます。これは例えば、ユーザーが画面をタップしても、その操作がロングプレスに該当しないと判断した場合に発生します。この場合、ロングプレス認識機構は自己除外を行います。
  3. 勝者の決定:
     一つの認識機構が勝者を宣言した時点で、その認識機構がユーザーの操作を処理することになります。他の認識機構はすべて敗者となり、ジェスチャーの処理が中止されます。
  4. 残り1つの認識機構のみが残った場合:
     他のすべての認識機構が自己除外を行い、1つだけ残った場合、その認識機構が自動的に勝者となり、ジェスチャーを処理します。

(3)具体的な例: 水平方向と垂直方向のドラッグの競合

 水平方向のドラッグ(HorizontalDrag)と垂直方向のドラッグ(VerticalDrag)の競合を考えてみましょう。

  • ユーザーが画面に指を置く(PointerDownEvent)と、両方の認識機構(HorizontalDrag と VerticalDrag)がアリーナに参加します。
  • ユーザーが指を動かし始めると(PointerMoveEvent)、両方の認識機構はその動きを観察します。
  • 指が一定のピクセル数以上、水平または垂直方向に動いた場合、その動きに対応する認識機構(HorizontalDrag もしくは VerticalDrag)が勝者を宣言し、そのジェスチャーがドラッグとして確定します。
  • 勝者が決まった時点で、もう一方の認識機構は敗者として、ジェスチャーの処理が中断されます。

(4)実装におけるヒント

  • GestureDetector を使った基本的な実装
GestureDetector を使って Tap と Long Press の競合を処理する(例):
GestureDetector(
  onTap: () => print('タップが認識されました'),
  onLongPress: () => print('ロングプレスが認識されました'),
  child: Container(
    width: 100,
    height: 100,
    color: Colors.blue,
    child: Center(child: Text('Tap or Long Press')),
  ),
);

//ユーザーが短くタップした場合と、
//長く押し続けた場合で異なるイベントが発生しますが、
//これらの競合は GestureDetector が自動的にアリーナを
//使用して解決します。

(5)GestureArena の利点と考慮事項

 ジェスチャーアリーナは、競合が発生した際の処理を簡潔にする利点がありますが、ジェスチャーの感度や認識のタイミングを適切に設定しないと、誤認識やレスポンスの遅延が発生する可能性もあります。

コメントを残す