Flutter Gesture

※記事内のコードは　Flutter 3.24.3　より抜粋

# 【概要編】

# ジェスチャー検出の仕組み

Flutter では、ジェスチャーは以下の順番で処理されている。

1. [Pointer Event の取得](#pointer-event-の取得)
1. [Hit Test](#hit-test)
1. [ジェスチャーの認識](#ジェスチャーの認識)
1. [ジェスチャーの競合](#ジェスチャーの競合)
1. [コールバックの実行](#コールバックの実行)

## Pointer Event の取得

Flutterは、画面上でユーザが行ったタップ、ドラッグ、スワイプなどの操作を [Pointer Event](#pointers--events) として収集している。

Pointer Event には下のような種類がある。

- **PointerDown**
- **PointerMove**
- **PointerUp**
- **PointerCancel**

この時点におけるユーザ操作は、単なるイベントであり、まだジェスチャーとしては認識されていない。

:::note info
「指が触れた」「指が離れた」などの画面上の操作は、Flutterで **Pointer Event** として発生する
:::


![pointer_event.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/40b8e998-7c3b-871a-805c-ecf2b57ac2ab.png)
>https://youtu.be/zEoASR7DTIw?si=jZWgkMEoNaJlpKHX

## Hit Test

画面上の座標に基づき、発生した Pointer Event をどのウィジェットが処理するかを決定する。

これにより Pointer Event が特定のウィジェットと紐付けられる。

この過程は [**Hit Test**](#hit-test-1) と呼ばれ、 **Render Tree** と呼ばれるツリー構造上で「親から子ウィジェットの方向」へ順次実行される。

:::note info
Pointer Event は **Hit Test** によって特定のウィジェットに紐づけられる
:::


![Screenshot 2024-11-24 at 17.02.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/611b3431-06e0-b085-221f-731e70997ff5.png)
>https://youtu.be/zEoASR7DTIw?si=jZWgkMEoNaJlpKHX

## ジェスチャーの認識

Flutter 上で発生した「指が画面に触れた」「指が画面から離れた」などの Pointer Event は、単なるイベントであり、これをアプリケーション上の操作、すなわち **ジェスチャー** として認識する必要がある。

例えば、`PointerDown`　→　`PointerMove`　→　`PointerUp`　などの一連の複数の Pointer Event は、スワイプとして認識されることで **単なるイベントからジェスチャーに昇格** する。

Flutterには既に定義されたジェスチャーが複数存在する。

![Gestures.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/e102b420-721c-000c-f180-9b8cf2b25dac.png)
>引用元：https://www.kodeco.com/29002200-creating-custom-gestures-in-flutter

ユーザ操作が特定のジェスチャーとして認識されることで、それぞれのジェスチャーに対して異なるコールバックを設定することができる。

## ジェスチャーの競合

複数のイベントが `PointerDown` → `PointerUp` の順に発生した際、一般的にこれは「タップ」として認識される。

一方で `PointerDown` → `PointerUp` が短時間の間に2回連続した場合「ダブルタップ」として認識する必要がある。

この時、発生する一連の Pointer Event は、単一のジェスチャー（操作）として認識される必要がある。

ここで登場するのがジェスチャー同士の競合という概念である。ジェスチャー は [**アリーナ**](#gesture-arena) と呼ばれる闘技場（arena=闘技場）においてそれぞれが競い合い、勝者と決定する。

このアリーナは単なる比喩ではなく、実際に [`_GestureArena`](https://github.com/flutter/flutter/blob/e7b7ebc066c1b2a5aa5c19f8961307427e0142a6/packages/flutter/lib/src/gestures/arena.dart#L57) というクラス名で実装されている。

アリーナでは競技参加者達（[Gesture Recognizer](#gesturerecognizer)）による **エントリー** （競技への参加申し込み手続き）が行われる。

Gesture Recognizer には

- [`TapGestureRecognizer`](#tapgesturerecognizer)
- [`DoubleTapGestureRecognizer`](#doubletapgesturerecognizer)
- [`LongPressGestureRecognizer`](#longpressgesturerecognizer)

などの種類がある。

![gesture_arena.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/14faafee-4f7b-0164-80f5-758f376a8067.png)
>https://youtu.be/Q85LBtBdi0U?si=WccdFgoGFDENAUli

競い合う Gesture Recognizer 達にとって、アリーナにおける「勝利」とはすなわち、Flutter 上で自身が **特定のジェスチャーとして認識される** ことを意味する。反対に「敗北」とはジェスチャーとして認識されないことを意味する。


## コールバックの実行

画面上のユーザ操作が特定のジェスチャーとして認識されると、[`GestureDetector`](#gesturedetector`) でそれぞれのジェスチャーに対して紐づけられたコールバックが実行される。

```java:GestureDetector
GestureDetector(
  onTap:  () {},
  onTapDown: (TapDownDetails details) {},
  onTapUp: (TapUpDetails details) {},
  onTapCancel: () {},
)
```

![Screenshot 2024-11-27 at 10.13.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/cc9dd408-a9a4-6bfd-2e05-5683e5030417.png)
> https://docs.flutter.dev/ui/interactivity/gestures

# 【詳細編】

# Pointers & Events

Flutterでは、ユーザーが画面上で行うタッチやマウスの操作を **Pointer** として扱う。

そして、これらの操作が内部で処理される際は **Pointer Event** が発生する。

実装では [`PointerEvent`](#pointerevent) クラスとして表現されている。

## `PointerEvent`

https://api.flutter.dev/flutter/gestures/PointerEvent-class.html

[`PointerEvent`](https://github.com/flutter/flutter/blob/e7b7ebc066c1b2a5aa5c19f8961307427e0142a6/packages/flutter/lib/src/gestures/events.dart#L237) クラスは`abstract`クラスで、下記のクラス群が拡張（`extends`）している。

```java:PointerEvent
abstract class PointerEvent {}
```

```java:PointerAddedEvent
// デバイスが Pointer の追跡を開始した瞬間に発生
// 指がまだ画面に触れていない可能性もある
class PointerAddedEvent extends PointerEvent {}
```

```java:PointerDownEvent
// 指が画面に触れた瞬間に発生
// タッチの開始を意味する
class PointerDownEvent extends PointerEvent {}
```

```java:PointerMoveEvent
// 指が画面上を移動している間、連続的に発生
// 移動の軌跡が追跡できる
class PointerMoveEvent extends PointerEvent {}
```

```java:PointerUpEvent
// 指が画面から離れた瞬間に発生
// タッチの終了を意味する
class PointerUpEvent extends PointerEvent {}
```

```java:PointerCancelEvent
// タッチイベントが中断された場合に発生
// 例: 電話の着信や他のアプリがフォーカスを奪ったとき
class PointerCancelEvent extends PointerEvent {}
```

```java:PointerHover
// マウスカーソルがウィジェットの上に乗ったとき
// タッチデバイス（`PointerDeviceKind.touch`）では発生しない
class PointerHover extends PointerEvent {}
```

```java:PointerSignal
// マウスホイールの回転によるスクロール操作などのポインター（マウスやタッチデバイスなど）から発生する離散的な信号を表すイベント
// ポインター自体の状態（位置やボタンの押下状態など）を変化させることなく発生し、連続的なイベントの文脈で解釈する必要がない単発のイベント
class PointerSignal extends PointerEvent {}
```

各イベントは連続的に発生するシーケンスである。

![pointer_event.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/40b8e998-7c3b-871a-805c-ecf2b57ac2ab.png)
>https://youtu.be/zEoASR7DTIw?si=jZWgkMEoNaJlpKHX

また `PointerEvent` は、 ID（識別子）、座標、デバイスなどの属性を持つ。

## pointer（ID）

PointerEvent を区別するための識別子。

```java:PointerEvent
abstract class PointerEvent {
  // 発生した PointerEvent が、どのポインタに紐づいているか
  // 2本指の操作では、それぞれのポインタを別々のIDで追跡する
  // または、一連の　PointerDown, PointerMove, PointerUp イベントを、同じIDで関連付ける
  int pointer = 0;
}
```

## 座標

操作が画面上のどの位置で発生したかを表す座標。

```java:PointerEvent
abstract class PointerEvent {
  // 現在のイベントが発生したグローバル座標（スクリーン全体での位置）
  Offset position = Offset.zero;

  // 前回の PointerEvent からの相対的な位置の変化
  // 操作開始座標を起点としてどの方向に Pointer が移動したかを判断する
  Offset delta = Offset.zero;

  // 座標系を変換するための行列
  // スクリーン座標変換とローカル座標変換に使用される
  Matrix4? transform;
}
```

## デバイス

```java:PointerEvent
abstract class PointerEvent {
  // デバイスの種類
  PointerDeviceKind kind = PointerDeviceKind.touch;

  // デバイスを識別するID
  int device = 0;
}
```
enum の [`PointerDeviceKind`](https://api.flutter.dev/flutter/dart-ui/PointerDeviceKind.html
) で表現される。

```java:PointerDeviceKind
enum PointerDeviceKind {
  // タッチ（指で画面に触れる操作）
  touch,

  // マウス（クリックやドラッグ操作）
  mouse,

  // スタイラス（タッチペン操作用のデバイス）
  stylus,

  // スタイラスの逆側（消しゴム機能）
  invertedStylus,

  // トラックパッド
  trackpad,
  
  // 不明
  unknown,
}
```

# Hit Test

ユーザーの操作をどのウィジェットが処理するべきかを決定するプロセス。

**Render Tree**（[`RenderObject`](https://api.flutter.dev/flutter/rendering/RenderObject-class.html
) のツリー）上で行われ、Pointer Event を適切なウィジェットに紐付ける。

具体的には、画面上でユーザによる Pointer の操作が行われると、Flutter は `PointerEvent` オブジェクトを生成する。この `PointerEvent` に座標情報（[`position`](#座標)）が含まれている。

```java:PointerEvent
abstract class PointerEvent {
  Offset position = Offset.zero;
}
```

Hit Test は親から子の方向へ向かって実行されるため、まず初めは、ルートに位置する `RenderObject` が対象となる。

![Screenshot 2024-11-24 at 17.00.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/759d33b1-7bdf-1fd2-1f0c-9a8711c707a4.png)
>https://youtu.be/zEoASR7DTIw?si=jZWgkMEoNaJlpKHX

`RenderObject` は `abstract` クラスの `HitTestable` で定義された [`hitTest()`](https://github.com/flutter/flutter/blob/166f1d76de879a1977dcce84aaa22edecc15918a/packages/flutter/lib/src/gestures/hit_test.dart#L12) を拡張（`extends`）している。

Hit Test 時にはこの `hitTest()` が呼ばれる。

```java:HitTestable
abstract class HitTestable {
  void hitTest(HitTestResult result, Offset position);
}
```

```java:MyRenderObject
class MyRenderObject extends RenderBox {
  @override
  void hitTest(BoxHitTestResult result, {required Offset position}) {
    // Pointer の座標が自身のウィジェット領域内かを検証
    if (position.dx >= 0 && 
        position.dx < size.width &&
        position.dy >= 0 &&
        position.dy < size.height) {
      // Hit Test 結果が true の場合、 HitTestResult に自身を追加する
      result.add(BoxHitTestEntry(this, position));
    }
  }
}
```


Hit Test の結果が`true` の場合、`HitTestResult` で定義された [`add()`](https://github.com/flutter/flutter/blob/166f1d76de879a1977dcce84aaa22edecc15918a/packages/flutter/lib/src/gestures/hit_test.dart#L175) が呼ばれ、テストをパスした `TestEntry` だけがリストとして保持される。

```java:HitTestTarget
abstract class HitTestTarget {
  void handleEvent(PointerEvent event, HitTestEntry entry);
}
```

```java:HitTestEntry
class HitTestEntry {
  HitTestEntry(this.target);

  final HitTestTarget target;
}
```

```java:HitTestResult
class HitTestResult {
  Iterable<HitTestEntry> get path => _path;
  final List<HitTestEntry> _path;

  void add(HitTestEntry entry) {
    _path.add(entry);
  }
}
```


![Screenshot 2024-11-24 at 17.01.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/ac231090-5ff8-2941-4e7a-41012a09190d.png)

![Screenshot 2024-11-24 at 17.01.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/3f42e584-467b-f4bd-617d-20331d45ccff.png)

![Screenshot 2024-11-24 at 17.01.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/3395094b-0c65-508f-7dad-de30b50d72fa.png)

![Screenshot 2024-11-24 at 17.02.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/611b3431-06e0-b085-221f-731e70997ff5.png)
>https://youtu.be/zEoASR7DTIw?si=jZWgkMEoNaJlpKHX

Hit Test が一通り完了すると、`HitTestResult` に格納された `HitTestEntry` では、Pointer Event が順次処理される。

具体的には、各 `HitTestEntry` に関連付けられた `HitTestTarget` の `handleEvent()` が呼び出される。

ここで重要なのが、リスト内の `HitTestEntry` はリストに追加された順番とは逆順で呼び出されることである。つまり、子ウィジェットから親ウィジェットへと遡る形で `handleEvent()` が実行される。

:::note info
Hit Test は親から子に向かって実行され、 `handleEvent()` は子から親に向かって実行される。
:::


```java:HitTestTarget
abstract class HitTestTarget {
  void handleEvent(PointerEvent event, HitTestEntry entry);
}
```

```java:MyRenderObject
class MyRenderObject extends RenderBox {
  @override
  bool hitTest(BoxHitTestResult result, {required Offset position}) {
  }
  
  @override
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    // Pointer Event の処理
    print('Pointer event received: $event');
  }
}
```

このメカニズムは `GestureDetector` を別の `GestureDetector` でラップするなどのネストが発生した場面における Pointer Event の伝播に重要な影響を与える。

ここの挙動を制御するには、`GestureDetector` の [`behavior`](#behavior) を設定する必要がある。


# Gesture Arena
ジェスチャーの競技場（アリーナ）

FLutter公式サイトでは [**Gesture disambiguation**](https://docs.flutter.dev/ui/interactivity/gestures#gesture-disambiguation) （ジェスチャーの曖昧さ回避）というセクションで説明されている。



また **Gesture Arena** は、YouTubeの公式Flutterチャンネルの [Decoding Flutter](https://www.youtube.com/watch?v=Q85LBtBdi0U
) というシリーズの一つとしても取り上げられている。

Flutterは、流れていく Pointer Event のストリームのすべてを監視（listen）していて、`TapGestureRecognizer`、`LongPressGestureRecognizer`、`PanGestureRecognizer` などの特定のジェスチャとして認識しようとする。

Arena はユーザ操作が複数のジェスチャー間で競合する際に、どのジェスチャーとしてハンドリングするかを決定する仕組みである。

![gesture_arena.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/14faafee-4f7b-0164-80f5-758f376a8067.png)
>引用元：https://youtu.be/Q85LBtBdi0U?si=WccdFgoGFDENAUli

## エントリ

Arena にエントリするのは `GestureArenaMember` である。

```java:GestureArenaMember
abstract class GestureArenaMember {}
```

ただし、`GestureArenaMember` の実体は `GestureRecognizer`。

```java:GestureRecognizer
abstract class GestureRecognizer extends GestureArenaMember {}
```

エントリは `_GestureArene` の `add()` によって行われる。

```java:_GestureArene
class _GestureArena {
  final List<GestureArenaMember> members = <GestureArenaMember>[];

  void add(GestureArenaMember member) {
    members.add(member);
  }
}
```


## 勝敗

Gesture Arena には [2つのルール](https://docs.flutter.dev/ui/interactivity/gestures#:~:text=At%20any%20time,recognizers%20to%20lose.) が存在する。

*At any time, a recognizer can eliminate itself and leave the arena. If there's only one recognizer left in the arena, that recognizer wins.*

***いつでも敗北を宣言してアリーナを退場できる。最後に残った一つの Recognizer が勝者になる。***

*At any time, a recognizer can declare itself the winner, causing all of the remaining recognizers to lose.*

***いつでも勝利を宣言することができる。それにより、他の Recognizer は敗者になる。***

これは `GestureArenaMember` に定義された2つのメソッドにより実現される。

```java:GestureArenaMember
abstract class GestureRecognizer extends GestureArenaMember {}

abstract class GestureArenaMember {
  void acceptGesture(int pointer);
  
  void rejectGesture(int pointer);
}
```

つまり Arena において `GestureRecognizer` は、勝利宣言を `acceptGesture()` によって行う。

勝利した `GestureRecognizer` は、 Pointer Event を自分のジェスチャーとしてハンドリング処理する権利を獲得する。

一方、敗北宣言は `rejectGesture()` によって行い、敗北した `GestureRecognizer` は受信した Pointer Event をハンドリング処理しない。

:::note info
`acceptGesture()`　： 勝利宣言時に呼ばれる
`rejectGesture()`　： 敗北宣言時に呼ばれる
:::

# `GestureRecognizer`

Flutter公式の説明では、エントリ後の Arena は以下のような状態であると説明されている（Preudocode=疑似コード）。

![Screenshot 2024-11-24 at 16.35.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/d2278777-4c42-7725-1976-c15803c7c053.png)
>引用元：https://youtu.be/Q85LBtBdi0U?si=WccdFgoGFDENAUli

ここでエントリしているの Recognizer は、下記のクラスから生成されたオブジェクトである。

- `TapGestureRecognizer`
- `DoubleTapGestureRecognizer`
- `LongPressGestureRecognizer`

`GestureRecognizer` を継承するクラスはとても多く、上記のクラスも含め、巨大なクラス群を形成している。

![GestureRecognizer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/67fd3d2e-377d-6644-8b7b-edcc5fc76aac.png)

前述の通り Arena における勝敗は、**参加者である Recognizer 自身が勝敗を宣言することによって決まる** ため、そのアルゴリズムは各 `GestureRecognizer` に委ねられている。

以下、動画の内容に沿って、各 `GestureRecognizer` が保有しているアルゴリズムについて記載する。

## `TapGestureRecognizer` のアルゴリズム

![GestureRecognizer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/6fd9e327-e163-b820-853b-44d45a9eabb5.png)

**勝利宣言をしない**。

`PointerDownEvent` と `PointerUpEvent` の間に、ポインタの座標が離れた場合には敗北宣言する。これはユーザの操作がタップ以外に該当するためである。

## `DoubleTapGestureRecognizer` のアルゴリズム

![GestureRecognizer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/0ec7d2e5-863c-2903-219b-1ffe02b687f4.png)

`PointerDownEvent` が発生すると 300ms のタイマーが設定され、タイマー内で２度目のタップが発生した場合に勝利宣言をする。

反対にタイマー内で２度目のタップが発生しない場合に敗北宣言する。

```java:DoubleTapGestureRecognizer
const Duration kDoubleTapTimeout = Duration(milliseconds: 300);

class DoubleTapGestureRecognizer extends GestureRecognizer {
  Timer? _doubleTapTimer;

  void _startDoubleTapTimer() {
    _doubleTapTimer ??= Timer(kDoubleTapTimeout, _reset);
  }
}
```

## `LongPressGestureRecognizer` のアルゴリズム

![GestureRecognizer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/abc78344-3703-c036-3493-4a5422e72441.png)

`PointerDownEvent` が発生すると、500msのタイマーが設定され、その間ユーザによる長押しが継続された（`PointerUpEvent` が発生しなかった）場合に勝利宣言する。

500ms内で `PointerUpEvent` が発生すると敗北宣言をする。

```java:LongPressGesuterRecognizer
const Duration kLongPressTimeout = Duration(milliseconds: 500);

class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
  LongPressGestureRecognizer({
    Duration? duration,
  }) : super(
         deadline: duration ?? kLongPressTimeout,
       );
}

abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer {
  final Duration? deadline;
  Timer? _timer;

  @override
  void addAllowedPointer(PointerDownEvent event) {
    if (deadline != null) {
      _timer = Timer(deadline!, () => didExceedDeadlineWithEvent(event));
    }
  }
}
```

## Gesture Arena 例

YoutubeのFltter公式チャンネルにある [GestureArena | Decoding Flutter](https://youtu.be/Q85LBtBdi0U?si=WccdFgoGFDENAUli) で説明されている内容のまとめ。

`PointerDownEvent` が発生した瞬間（0ms） Arena が開催し、3つの Recognizer がエントリした状態になっている。

![gesture_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/c8ed2068-3a62-6e6e-4481-6a2f54968787.png)
>引用元：https://youtu.be/Q85LBtBdi0U?si=WccdFgoGFDENAUli

`PointerDownEvent` から 50ms 後に `PointerUpEvent` が発生。

[`LongPressGestureRecognizer`](#longpressgesturerecognizer-のアルゴリズム) は `PointerDownEvent` の発生から500ms以内に `PointerUpEvent` が発生すると敗北宣言を行うため、ここで退場する。

![gesture_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/b053c8b6-8f8c-24ff-c0e4-f6f9c553757d.png)
>引用元：https://youtu.be/Q85LBtBdi0U?si=WccdFgoGFDENAUli

さらに、`PointerDownEvent` の発生から500ms以内に2度目の `PointerDownEvent` が発生しなかった場合、[`DoubleTapGestureRecognizer`](#doubletapgesturerecognizer-のアルゴリズム) が敗北宣言するため、 [`TapGestureRecognizer`](#tapgesturerecognizer-のアルゴリズム) が結果的に勝者となる。

![gesture_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/069233fb-21a4-915b-a550-26c4aaded44b.png)
>引用元：https://youtu.be/Q85LBtBdi0U?si=WccdFgoGFDENAUli


# 【解析編】

ここからはさらにFlutter（version 3.24.3）の内部の実装を解析する。

# `debugPrintGestureArenaDiagnostics`

Flutter には [`debugPrintGestureArenaDiagnostics`](https://github.com/flutter/flutter/blob/main/packages/flutter/lib/src/gestures/debug.dart#L40) というフラグによるデバッグ補助機能が用意されている。

```java:debugPrintGestureArenaDiagnostics
bool debugPrintGestureArenaDiagnostics = false;
```

これを `true` にすることで VSCode の DEBUG CONSOLE 上に Gesture 関連の詳細なログが出力されるようになる。

![Screenshot 2025-01-07 at 21.10.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/18106612-4c5a-490b-decd-8ed7182f1c91.png)

# `_GestureArena`

一部を抜粋（ソースは[こちら](https://github.com/flutter/flutter/blob/dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668/packages/flutter/lib/src/gestures/arena.dart#L59)）

```java:_GestureArena
class _GestureArena {
  final List<GestureArenaMember> members = <GestureArenaMember>[];
  bool isOpen = true;
  bool isHeld = false;
  bool hasPendingSweep = false;
  GestureArenaMember? eagerWinner;

  void add(GestureArenaMember member) {
    members.add(member);
  }
}
```

# `GestureArenaManager`

一部を抜粋（ソースは[こちら](https://github.com/flutter/flutter/blob/dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668/packages/flutter/lib/src/gestures/arena.dart#L113)）

```java:GestureArenaManager
class GestureArenaManager {
  final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};

  GestureArenaEntry add(int pointer, GestureArenaMember member) {
    final _GestureArena state = _arenas.putIfAbsent(pointer, () {
      return _GestureArena();
    });
    state.add(member);
    return GestureArenaEntry._(this, pointer, member);
  }

  void close(int pointer) {}

  void sweep(int pointer) {}

  void hold(int pointer) {}

  void release(int pointer) {}

  void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {}

  void _tryToResolveArena(int pointer, _GestureArena state) {}

  void _resolveByDefault(int pointer, _GestureArena state) {}

  void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {}

  bool _debugLogDiagnostic(int pointer, String message, [ _GestureArena? state ]) {}
}
```

# 解決 の概念

Arena において、Gestre Recognizer の勝敗に決着がつくことは **resolve** （解決）と表現されている。

```java:解決
class GestureArenaManager {
  void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {}
  void _tryToResolveArena(int pointer, _GestureArena state) {}
  void _resolveByDefault(int pointer, _GestureArena state) {}
  void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {}
}
```

解決の手段に関しては、後述する [`sweep()`](#gesturearenamanagersweep) が深く関わっている。

# `_GestureArene.add()` / `GestureArenaManager.add()`

https://api.flutter.dev/flutter/gestures/GestureArenaManager/add.html

Arena への  Recognizer のエントリは `_GestureArena` の `add()` を通じて行われる。

```java:add()
class GestureArenaManager {
  final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};

  GestureArenaEntry add(int pointer, GestureArenaMember member) {
    final _GestureArena state = _arenas.putIfAbsent(pointer, () {
      return _GestureArena();
    });
    state.add(member);
    return GestureArenaEntry._(this, pointer, member);
  }
}

class _GestureArena {
  final List<GestureArenaMember> members = <GestureArenaMember>[];

  void add(GestureArenaMember member) {
    members.add(member);
  }
}
```

![close.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/eb0768cc-1fbc-f9b0-1ee7-072cb25896cb.png)

# `pointer` / Gesture Arena の識別子

Arena 自体は `PointerDownEvent` が持つ `int` 型の [`pointer`](#pointerid) によって識別される。

そのため、Arena に対するメソッドは全て引数に Arena を識別するための `pointer` を受け取る。

```java:GestureArenaManager
class PointerDownEvent extends PointerEvent
// PointerEvent の メンバ（pointer）を継承
}

abstract class PointerEvent {
  // Arena の識別子
  final int pointer;
}

class GestureArenaManager {
  final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
  
  // 引数の pointer によって Arena を識別 
  void close(int pointer) {}
  void sweep(int pointer) {}
  void hold(int pointer) {}
  void release(int pointer) {}
}
```
この `pointer` は [`Listener`](#listener) から渡される `PointerDownEvent` の `pointer` である。

`Listener` は `RawGestureDetector` ウィジェットによってラップされていた。

![add.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/62445d47-0f96-f7e7-ea31-f972b78b80dc.png)

# `_GestureArena.isOpen` / `GestureArenaManager.close()`

https://api.flutter.dev/flutter/gestures/GestureArenaManager/close.html

`isOpen` は、 Arena に対する Recognizer の **新たなエントリ** が可能な状態かどうかを表す。

`close()` によって `isOpen = false` になっている場合、既に Arena は締め切られた状態になるため、新たな Recognizer をエントリさせることはできない。

```java:isOpen
class _GestureArena {
  // 初期値が true
  bool isOpen = true;
}

class _GestureArena {
  final List<GestureArenaMember> members = <GestureArenaMember>[];

  void add(GestureArenaMember member) {
    assert(isOpen); // add() 時に open している必要がある
    members.add(member);
  }
}
```

`close()` は [Hit Test](#hit-test) による `PointerDownEvent` のウィジェットに対する紐づけ後に呼び出される。

```java:close()
class _GestureArena {
  bool isOpen = true;
}

class GestureArenaManager {
  final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};

  void close(int pointer) {
    final _GestureArena? state = _arenas[pointer];
    // close() によって false になる
    state.isOpen = false;
  }
}

mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
  @override // from HitTestTarget
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    if (event is PointerDownEvent) {
      gestureArena.close(event.pointer);
    }
  }
}
```

![close.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/0f7bd143-6a58-78a2-a991-c4e9cd19d745.png)


# `_GestureArena.isHeld` / `GestureArenaManager.hold()`

https://api.flutter.dev/flutter/gestures/GestureArenaManager/hold.html

`isHeld` は Arena での解決が保留（hold）状態にあることを表す。

`hold()` の実行によって保留された Arena は、[`sweep()`](#gesturearenamanagersweep) によって解決されない。

```java:保留中の GestureArena は解決されない
class GestureArenaManager {
  final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};

  void sweep(int pointer) {
    final _GestureArena? state = _arenas[pointer];    
    if (state.isHeld) {
      // 保留中の Arena は sweep() が完遂されない
      return; // This arena is being held for a long-lived member.
    }
}
```

通常 `PointerUpEvent` が発生すると「画面から指が離れた」ことによって、ジェスチャーの決定、すなわち Arena における **解決** が行われる。

しかし、例えば「2回分」の `PointerUpEvent` を必要とする `DoubleTapGestureRecognizer` は、1回目の `PointerUpEvent` 時に、解決の実行を待って欲しい。

この時 `DoubleTapGestureRecognizer` によって `hold()` が呼ばれ、解決の保留が実施される。

```java:DoubleTapGestureRecognizer が hold() を要求している
class DoubleTapGestureRecognizer extends GestureRecognizer {
  void _registerFirstTap(_TapTracker tracker) {
    GestureBinding.instance.gestureArena.hold(tracker.pointer);
  }
}
```

また `isHeld` は [`release()`](#_gesturearenaisheld--gesturearenamanagerrelease) によって `false` になる。

```java:isHeld / hold()
class _GestureArena {
  bool isHeld = false;
}

class GestureArenaManager {
  final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};

  void hold(int pointer) {
    final _GestureArena? state = _arenas[pointer];
    // hold() によって true になる
    state.isHeld = true;
  }
  
  void release(int pointer) {
    final _GestureArena? state = _arenas[pointer];
    // release() によって false になる
    state.isHeld = false;
  }
}
```

![hold.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/fcc637c9-a62f-40a9-9821-06e42ea0d9c7.png)


# `_GestureArena.isHeld` / `GestureArenaManager.release()`

https://api.flutter.dev/flutter/gestures/GestureArenaManager/release.html

`release()` は保留状態にされた Arena に対して**解放**を行い、解決のプロセスを再開させる。

更に `release()` の内部では、勝者を決定するための `sweep()` が呼び出される。

```java:isHeld / release()
class _GestureArena {
  bool isHeld = false;
}

class GestureArenaManager {
  final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};
  
  void release(int pointer) {
    final _GestureArena? state = _arenas[pointer];
    // 保留状態が解除される
    state.isHeld = false;

    if (state.hasPendingSweep) {
      // release() 内部から sweep() が呼び出される
      sweep(pointer);
    }
  }
}
```

![release.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/62c63804-e7e6-3b06-1326-3b438ad661b8.png)

# `GestureArenaManager.sweep()`

https://api.flutter.dev/flutter/gestures/GestureArenaManager/sweep.html

Arena の解決を強制実行する。最後に残った Recoognizer は勝者となり、それ以外は敗者になる。その結果、エントリ済みの Recognizer は一掃（sweep）される。

また `release()` が呼ばれると内部で `sweep()` が実行され、保留中のアリーナが解決される。

個々の Recognizer の解決には、`acceptGesture()` もしくは `rejectGesture()` が使用される。


```java:hasPendingSweep / sweep()
class _GestureArena {
  bool hasPendingSweep = false;
}

class GestureArenaManager {
  final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};

  void release(int pointer) {
    final _GestureArena? state = _arenas[pointer];

    if (state.hasPendingSweep) {
      sweep(pointer);
    }
  }

  void sweep(int pointer) {
    final _GestureArena? state = _arenas[pointer];
    if (state.isHeld) {
      state.hasPendingSweep = true;
      return; // This arena is being held for a long-lived member.
    }
    _arenas.remove(pointer);
    if (state.members.isNotEmpty) {
      // First member wins.
      state.members.first.acceptGesture(pointer);
      // Give all the other members the bad news.
      for (int i = 1; i < state.members.length; i++) {
        state.members[i].rejectGesture(pointer);
      }
    }
  }
}
```

![sweep.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/3afb6e37-e951-14a3-6792-016f7a7449e3.png)

Arena における解決は、前述の[ルール](#勝敗)も加味すると以下の2パターンがあると理解することができる。

>*いつでも勝利を宣言することができる。それにより、他の Recognizer は敗者になる。*

① `GestureRecognizer` 自身が `GestureRecognizer` クラス**内部**からで `acceptGesture()` を呼ぶパターン。**能動的**に勝者が決まるパターンとも言える。この勝者は [eager winner](#eager-winner) と呼ばれる。

>*いつでも敗北を宣言してアリーナを退場できる。最後に残った一つの Recognizer が勝者になる。*

② `GestureArenaManager` の `sweep()` をきっかけにして `GestureArenaManager` クラス内で `acceptGesture()` が呼ばれるパターン。つまり `GestureRecognizer` クラス**外部**から `acceptGesture()` が呼ばれる。他のエントリが負けることにより、結果として残ったものが勝者となる**受動的**パターン。

# `_GestureArena.hasPendingSweep` 

Arena の `sweep()` が完了していないことを表す。

`hold()` によって保留された Arena に対して `sweep()` が実行された場合、`hasPendingSweep` フラグを立てて `return` によって処理を途中で抜けるようになっている。

フラグはその後、 `release()` の中で `sweep()` を実行すべきかの確認のために使用される。

```java:hasPendingSweep
class _GestureArena {
  bool hasPendingSweep = false;
}

class GestureArenaManager {
  final Map<int, _GestureArena> _arenas = <int, _GestureArena>{};

  void sweep(int pointer) {
    final _GestureArena? state = _arenas[pointer];
    if (state.isHeld) {
      // sweep() が未遂フラグが立つ
      state.hasPendingSweep = true;
      return; // This arena is being held for a long-lived member.
    }
  }

  void release(int pointer) {
    final _GestureArena? state = _arenas[pointer];
    // 保留状態が解除
    state.isHeld = false;
    if (state.hasPendingSweep) {
      // 未遂の sweep() を完遂
      sweep(pointer);
    }
  }
}
```

#  "eager winner"
eager=熱心な

周りが敗北宣言することによって勝ち残ったものではなく、積極的に自らが勝利宣言をすることによって勝利したものを **eager winner** として扱う。

`_GestureArena` が保持している。

```java:eagerWinner
class _GestureArena {
  GestureArenaMember? eagerWinner;
}
```

# 解決

ここまでの理解を踏まえて、更に解決の過程を深掘りする。

```java:解決系のメソッド
class GestureArenaManager {
  void _resolve(int pointer, GestureArenaMember member, GestureDisposition disposition) {}
  void _tryToResolveArena(int pointer, _GestureArena state) {}
  void _resolveByDefault(int pointer, _GestureArena state) {}
  void _resolveInFavorOf(int pointer, _GestureArena state, GestureArenaMember member) {}
}
```


## 処理フロー

`_resolve()` の内部処理をフロー化すると以下のようになっていた。条件分岐によって大きく3つの処理フローが存在していることがわかる。

また [解決系のメソッド](#解決) は `_resolve()` 内部で使用されていた。

- 勝利宣言が行われた場合
  - Arena がオープンしている場合：["eagerWinner"](#eager-winner) の決定
  - Arena がクローズされている場合：即座に [`_resolveInFavorOf()`](#_resolveinfavorof) を呼び出して解決

- 敗北宣言が行われた場合
  - `rejectGesture()` を呼び出し、Recognizer を Arena から削除
  - Arena がクローズされている場合： [`_tryToResolveArena()`](#_trytoresolvearena) を呼び出し、解決を図る


![_resolve.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/6faca3ef-ac77-6c1c-c082-8d8acbbf6b2e.png)

`GestureArenaManager` の `_resolve()` は `GestureArenaEntry` から呼ばれる。

```java:GestureArenaEntry
class GestureArenaEntry {
  /// Call this member to claim victory (with accepted) or admit defeat (with rejected).
  /// It's fine to attempt to resolve a gesture recognizer for an arena that is already resolved.
  void resolve(GestureDisposition disposition) {
    _arena._resolve(_pointer, _member, disposition);
  }
}
```

ここから `_resolve()` 内部をさらに詳しくみていく。

## 処理フロー1

![_resolve_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/53f79d9d-7951-00ac-ea6d-ee8f2ed6492b.png)

勝利宣言がされる中で [eagerWinner](#eagerwinner) が決定されるだけのシンプルな処理フロー。

## 処理フロー2

![_resolve_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/b3ae67e4-6473-e88c-ff77-87f52a4549d9.png)

Arena への[エントリが締め切られた](#_gesturearenaisopen--gesturearenamanagerclose)後に勝利宣言が行われるパターン。

[`_resolveInFavorOf()`](#_resolveinfavorof) が呼ばれている点が処理フロー1と異なる。 

## 処理フロー3

敗北宣言が行われるパターン。

勝利宣言と同時に Arena の解決が実行される処理フロー1、2 とは異なり、勝ち残ったものが勝利者となる[受動的な解決](#gesturearenamanagersweep)になるため、解決がやや消極的（try to resolve = 〜しようと試みる [`_tryToResolveArena()`](#_trytoresolvearena)）。

また `_resolve()` 内で `GestureArenaMember.rejectGesture()` が呼び出されている。

![_resolve_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/985b221b-4169-e288-ab88-2877c7d4734b.png)

## `_resolve()`

```java:_resolve()
void _resolve(
    int pointer,
    GestureArenaMember member,
    GestureDisposition disposition,
    ) {
  final _GestureArena? state = _arenas[pointer];
  if (state == null) {
    return; // This arena has already resolved.
  }
  switch (disposition) {
    case GestureDisposition.accepted:
      if (state.isOpen) {
        state.eagerWinner ??= member;
      } else {
        _resolveInFavorOf(pointer, state, member);
      }
    case GestureDisposition.rejected:
      state.members.remove(member);
      member.rejectGesture(pointer);
      if (!state.isOpen) {
        _tryToResolveArena(pointer, state);
      }
  }
}
```

## `_tryToResolveArena()`

```java:_tryToResolveArena()
void _tryToResolveArena(int pointer, _GestureArena state) {
  if (state.members.length == 1) {
    scheduleMicrotask(() => _resolveByDefault(pointer, state));
  } else if (state.members.isEmpty) {
    _arenas.remove(pointer);
  } else if (state.eagerWinner != null) {
    _resolveInFavorOf(pointer, state, state.eagerWinner!);
  }
}
```

`_tryToResolveArena()` 内の処理は以下の3つに分けることができる。これらは Arena の状況に応じて使い分けられている。

```java:Arena に存在する Recognizer が1つの場合　→　デフォルトの解決
if (state.members.length == 1) {
  scheduleMicrotask(() => _resolveByDefault(pointer, state));
}
```

この場合、Arena に残る Recognizer を勝者として解決を行う。

現在進行中の処理に影響を与えないように、次のイベントループサイクルに `_resolveByDefault()` の実行を後回ししている。

```java:Arena に Recognizer が存在しない場合　→　Arena を削除
else if (state.members.isEmpty) {
  _arenas.remove(pointer);
}
```

その [`pointer`](#pointerid) における Arena を削除する。

```java:eagerWinner が存在する場合　→　即ちに解決
else if (state.eagerWinner != null) {
  _resolveInFavorOf(pointer, state, state.eagerWinner!);
}
```

["eagerWinner"](#eager-winner) を勝者として `_resolveInFavorOf()` を呼び出す。

## `_resolveByDefault()`

```java:_resolveByDefault()
void _resolveByDefault(int pointer, _GestureArena state) {
  if (!_arenas.containsKey(pointer)) {
    return; // This arena has already resolved.
  }
  final List<GestureArenaMember> members = state.members;
  assert(members.length == 1);
  _arenas.remove(pointer);
  state.members.first.acceptGesture(pointer);
}
```

`assert` から Recognizer が1つの Arena に対して呼ばれることが前提であることがわかる。Arena が削除された後、`acceptGesture()` が実行される。


## `_resolveInFavorOf()`
*in favor of* = ～に有利な方向で、～を支持する

```java:_resolveInFavorOf()
void _resolveInFavorOf(
    int pointer,
    _GestureArena state,
    GestureArenaMember member,
    ) {
  _arenas.remove(pointer);
  for (final GestureArenaMember rejectedMember in state.members) {
    if (rejectedMember != member) {
      // 敗北宣言
      rejectedMember.rejectGesture(pointer);
    }
  }
  // 勝利宣言
  member.acceptGesture(pointer);
}
```

引数に取る `GestureArenaMember` を勝者として決定し、他のエントリを敗者として処理することで Arena を解決する。


# `Listener`

Pointer Event は、`Listener` ウィジェットを使用して監視することができる。

https://api.flutter.dev/flutter/widgets/Listener-class.html

```java:Listener
Listener(
  onPointerDown: (PointerDownEvent event) {},
  onPointerMove: (PointerMoveEvent event) {},
  onPointerUp: (PointerUpEvent event) {},
  onPointerCancel: (PointerCancelEvent event) {},
  child: ...,
)
```

`Listener` はジェスチャーとして認識される前の Pointer Event を監視することができるウィジェットで、 `GestureDetector` よりも低レベルのイベントを扱うことができる。つまり `GestureDetector` は `Listener` をラップしたウィジェットである。

```java:GestureDetector
GestureDetector(
  onTap:  () {},
  onTapDown: (TapDownDetails details) {},
  onTapUp: (TapUpDetails details) {},
  onTapCancel: () {},
)
```

:::note info
`Listener` は Pointer Event を監視できる。
`GestureDetector` はジェスチャーを検知できる。
:::

Pointer Event について調べようとした際に、`GestureDetector` のコールバックに `print()` などを仕込むと、生の Event を取得することはできない。未加工の情報を取得したければ、必ず `Listener` を使用するようにする。


# `MouseRegion`

`MouseRegion` によってボタンが押されていないときでもマウスの動きを追跡することができる。

https://api.flutter.dev/flutter/widgets/MouseRegion-class.html



# `GestureDetector`

https://api.flutter.dev/flutter/widgets/GestureDetector-class.html

`GestureDetector` に設定できる `onTapDown` は [Gesture Arena](#gesture-arena) で勝利する前に呼び出され、 `onTapCancel` は敗北した時に呼び出される。

## サイズ

子ウィジェットは指定しなくても良い。

子ウィジェットを指定した場合は子ウィジェットのサイズに制限され、指定しなかった場合は親ウィジェットのサイズを継承する。

```Java:子ウィジェットのサイズに制限される
GestureDetector(
  onTap: () {},
  child: Container(
    width: 100,
    height: 100,
  ),
)
```

```Java:親ウィジェットのサイズを継承する
Container(
  width: 200,
  height: 200,
  child: GestureDetector(
    onTap: () {},
  ),
)
```

:::note info
`GestureDetector` は
子ウィジェットを指定した場合は子ウィジェットのサイズに制限され、
指定しなかった場合は親ウィジェットのサイズを継承する。
:::


## Properties



### `onTap` 関連

単押し操作に関連するもの。

| 引数 | 内容 |
|:-|:-|
| `onTap` | シングルタップ |
| `onTapDown` | 指が触れた瞬間 |
| `onTapUp` | 指が離れた瞬間 |
| `onTapCancel` | タップ操作がキャンセルされたとき |
| `onDoubleTap` | ダブルタップ |

### `onLongPress` 関連

長押し操作に関連するもの。

| 引数 | 内容 |
|:-|:-|
| `onLongPress` | 長押し |
| `onLongPressStart` | 長押しが始まった瞬間<br>コールバックに `LongPressStartDetails` が引数として渡され、タッチ位置などの情報が取得可能 |
| `onLongPressMoveUpdate` | 長押し中に指が動いたとき<br>コールバックに `LongPressMoveUpdateDetails` が引数として渡される |
| `onLongPressUp` | 長押しをやめた（指を画面から離した）とき |
| `onLongPressEnd` | 長押しが終了したとき（指を離したりキャンセルされたりしたとき）<br>コールバックに `LongPressEndDetails` が引数として渡される |

### `onPan` 関連

ドラッグに関連するもの。

| 引数 | 内容 |
|:-|:-|
| `onPanStart` | ドラッグを開始したとき<br>コールバックに `DragStartDetails` が引数として渡され、タッチ開始位置が取得可能 |
| `onPanUpdate` | ドラッグ中に指が動いたとき<br>コールバックに `DragUpdateDetails` が引数として渡され、移動距離や現在位置が取得可能 |
| `onPanEnd` | ドラッグが終了したとき<br>コールバックに `DragEndDetails` が引数として渡され、速度や終了状態が取得可能 |
| `onPanCancel` | ドラッグ操作がキャンセルされたとき |
| `onHorizontalDragDown` | 水平方向のドラッグ操作を開始したときの指が画面に触れたとき<br>コールバックに `DragDownDetails` が引数として渡される |
| `onHorizontalDragStart` | 水平方向のドラッグ操作が開始されたとき |
| `onHorizontalDragUpdate` | 水平方向のドラッグ操作中に、位置が更新されたとき<br>コールバックに `DragUpdateDetails` が引数として渡される |
| `onHorizontalDragEnd` | 水平方向のドラッグ操作が終了したとき<br>コールバックに `DragEndDetails` が引数として渡される |
| `onHorizontalDragCancel` | 水平方向のドラッグ操作が途中でキャンセルされたとき |
| `onVerticalDragDown	` | 垂直方向のドラッグ操作を開始したときの指が画面に触れたとき<br>コールバックに `DragDownDetails` が引数として渡される |
| `onVerticalDragStart` | 垂直方向のドラッグ操作が開始されたとき |
| `onVerticalDragUpdate` | 垂直方向のドラッグ操作中に、位置が更新されたとき<br>コールバックに `DragUpdateDetails` が引数として渡される |
| `onVerticalDragEnd` | 垂直方向のドラッグ操作が終了したとき<br>コールバックに `DragEndDetails` が引数として渡される |
| `onVerticalDragCancel` | 垂直方向のドラッグ操作が途中でキャンセルされたとき |

### `dragStartBehavior`

### `onScale` 関連

ピンチインやピンチアウトなどのスケールに関連するもの。

| 引数 | 内容 |
|:-|:-|
| `onScaleStart` | スケール操作を開始したとき |
| `onScaleUpdate` | スケール操作中<br>コールバックに `ScaleUpdateDetails` が引数として渡され、拡大率や回転角度の変化が取得可能 |
| `onScaleEnd` | スケール操作が終了したとき<br>コールバックに `ScaleEndDetails` が引数として渡され、最終的な状態や速度が取得可能 |


### `onSecondaryTap` 関連

右クリックや二本指タップなどのセカンダリタップに関わるもの。

| 引数 | 内容 |
|:-|:-|
| `onSecondaryTap` | セカンダリタップ |
| `onSecondaryTapDown` | 指が画面に触れた瞬間<br>コールバックに `TapDownDetails` が引数として渡され、位置情報が取得可能 |
| `onSecondaryTapUp` | 指を離したとき<br>コールバックに `TapUpDetails` が引数として渡され、位置情報が取得可能 |
| `onSecondaryTapCancel` | セカンダリタップがキャンセルされたとき |


### `onTertiaryTap` 関連

三本指タップなどのサードタップに関わるもの。

| 引数 | 内容 |
|:-|:-|
| `onTertiaryTapDown` | 指が画面に触れた瞬間<br>コールバックに `TapDownDetails` が引数として渡され、位置情報が取得可能 |
| `onTertiaryTapUp` | 指を離したとき<br>コールバックに `TapUpDetails` が引数として渡され、位置情報が取得可能 |
| `onTertiaryTapCancel` | サードタップがキャンセルされたとき |

### `onDoubleTap` 関連

ダブルタップに関わるもの。

| 引数 | 内容 |
|:-|:-|
| `onDoubleTap` | ダブルタップ |
| `onDoubleTapDown` | 指が画面に触れた瞬間<br>コールバックに `TapDownDetails` が引数として渡される |
| `onDoubleTapCancel` | ダブルタップがキャンセルされたとき |

### `onForcePress` 関連

圧力感知に関わるもの（iOS専用）。

| 引数 | 内容 |
|:-|:-|
| `onForcePressStart` | 圧力検知（ForcePress）が開始されたとき |
| `onForcePressPeak` | 圧力がピークに達したとき<br>コールバックに `ForcePressDetails` が引数として渡される |
| `onForcePressUpdate` | 圧力が更新されたとき<br>コールバックに `ForcePressDetails` が引数として渡され、圧力レベルの変化を監視できる |
| `onForcePressEnd` | 圧力検知（ForcePress）が終了したとき<br>コールバックに `ForcePressDetails` が引数として渡される |

### `behavior` 関連


| 引数 | 内容 |
|:-|:-|
| [`behavior`](#behavior) | `GestureDetector` が Hit Test によって検出される際の挙動の指定<br>- `HitTestBehavior.deferToChild` <br>- `HitTestBehavior.opaque`<br>- `HitTestBehavior.translucent` |
| [`dragStartBehavior`](#dragStartBehavior) | ドラッグ操作が検出されるタイミング<br>-`DragStartBehavior.start`（デフォルト） <br>-`DragStartBehavior.down`  |

#### `behavior`

[Hit Test](#hit-test) の挙動を制御するプロパティ。

`GestureDetector` は Hit Test の結果を利用してジェスチャーを処理している。

詳細はFlutter公式の [Troubleshooting](https://api.flutter.dev/flutter/widgets/GestureDetector-class.html#:~:text=GestureDetector.2%20mysample-,Troubleshooting,-Why%20isn%27t%20my
) に説明があるものの、理解するまでに非常に時間のかかった。（理解できたのかすら怪しいが、なんとかまとめてみた）


#### `HitTestBehavior`

```java:HitTestBehavior
enum HitTestBehavior {
  deferToChild,
  opaque,
  translucent,
}
```

#### デフォルト値

*This defaults to `HitTestBehavior.deferToChild` if child is not null and `HitTestBehavior.translucent` if child is null.*

つまり、

`child != null` ---> `deferToChild`

`child == null` ---> `translucent`

ということ。[実装](https://github.com/flutter/flutter/blob/dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668/packages/flutter/lib/src/widgets/gesture_detector.dart#L1525) を確認してみても、その通りになっていた。

```java:デフォルト値
 HitTestBehavior get _defaultBehavior {
    return widget.child == null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild;
  }
```

#### `deferToChild`

*Targets that defer to their children receive events within their bounds only if one of their children is hit by the hit test.*

`GestureDetector` 自身は Hit Test の対象とされない（自身の領域に対する Hit Test 被検出を放棄する）。ただし被検出自体を放棄するわけではなく、Hit Test は `child` に指定したウィジェットへ委譲される。

結果的に `child` が Hit Test によって検出された場合にのみ、 `GestureDetector` も Hit Test によって検出されるという挙動をとる。

この挙動を検証してみた。

使用するサンプルはこちら。

```java:deferToChild の挙動を検証する
class MyGestureDetector extends StatelessWidget {
  const MyGestureDetector({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.red,
      height: 400,
      width: 400,
    );
  }
}
```

以下のような赤い正方形を使用する。

![defer_to_child.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/ce785200-93e2-ac94-fec0-c2dfd40c9336.png)

まずこの赤い四角形を `GestureDetector` でラップし、`GestureDetector`の Hit Test による被検出を、この赤い `Container` に委譲する設定を行う。

`behavior` は[デフォルト](#デフォルト値)で既に `deferToChild` になっているはずだが、あえて明示的に指定を行う。

```java:Hit Test を child に委譲
class MyGestureDetector extends StatelessWidget {
  const MyGestureDetector({super.key});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior.deferToChild,  // Hit Test を child に委譲
      onTapDown: (_) => debugPrint('tap down'),
      child: Container(  // Hit Testが委譲された child
        color: Colors.red,
        height: 400,
        width: 400,
      ),
    );
  }
}
```

すると、`Container` が Hit Test で検出された時だけ `GestureDetector` の `onTapDown` も連動して反応した。

![defer_to_child_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/c8e1e379-c19c-1e7a-ee9e-485552e45bc2.gif)

次に、以下のような構成に変更して、存在しない `child` へ Hit Test を委譲する形にしてみた。

```java:Hit Test を委譲する child が存在しない
class MyGestureDetector extends StatelessWidget {
  const MyGestureDetector({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.red,
      height: 400,
      width: 400,
      child: GestureDetector(
        behavior: HitTestBehavior.deferToChild,
        onTapDown: (_) => debugPrint('tap down'),
        // Hit Test が委譲される child が存在しない
      ),
    );
  }
}
```

すると、`GestureDetector` の `onTapDown` が反応しなくなった。Hit Test によって検出されるウィジェットが存在しなくなったためと考えられる。

![defer_to_child_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/c5ad5d37-75c5-226a-2104-c6eff9b9e5df.gif)

`child` を持たない `GestureDetector` が描画されていないのでは？という疑問が湧いたため、一応確認をしてみたところ、`GestureDetector` ウィジェット自体は、下記画像のように、確かに画面上に `400 x 400 px` の領域を取って存在していた。

![defer_to_child_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/87355650-368c-0cde-4a3f-6b17d5ae5c9b.png)

#### `opaque`

*Opaque targets can be hit by hit tests, causing them to both receive events within their bounds and prevent targets visually behind them from also receiving events.*

`GestureDetector` が `Padding` の余白部分などの透明な領域を含んでいても、その領域を**不透明**扱い（opaque）とすることにより Hit Test で検出できるようにする。

Flutter において、`Padding` の余白に代表される「何も描画されない状態、領域」は、「透明」と表現されることがある（特に [Flutter公式サイト](https://api.flutter.dev/flutter/rendering/HitTestBehavior.html)）。

この文脈における「透明」とは、単に `Colors.transparent` や `Color.fromARGB(0, 0, 0, 0)` などのカラーを指すものではない。そのため、挙動の違いを検証するために、これらの色を指定しても、「透明」の領域を作ったことにならない場合があるため注意。

`opaque` を使用した場合の効果を検証してみた。

使用するサンプルはこちら。

```java:opaque の挙動を検証する
class MyGestureDetector extends StatelessWidget {
  const MyGestureDetector({super.key});

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional.center,
      children: [
        Container(
          color: Colors.blue,
          height: 400,
          width: 400,
        ),
        Padding(
          padding: const EdgeInsets.all(50),
          child: Container(
            color: Colors.red,
            height: 300,
            width: 300,
          ),
        ),
      ],
    );
  }
}
```

青い正方形（`400 x 400 px`）の上に赤い正方形（`300 x 300 px`）を重ねている。赤い正方形は上下左右に余白をそれぞれ `50 px` ずつを持っていて、余白領域を合わせると青い正方形と同じ領域を占めていることになる。

![opaque.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/e19a1d0d-bc67-423a-9f44-dfdee141f331.png)

まず `deferToChild` を明示的に指定して、赤い領域やその余白領域の反応を確認してみる（`deferToChild` は [デフォルト値](#デフォルト値)）。

```java:opaque の場合
class MyGestureDetector extends StatelessWidget {
  const MyGestureDetector({super.key});

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional.center,
      children: [
        Container(
          color: Colors.blue,
          height: 400,
          width: 400,
        ),
        GestureDetector(
          behavior: HitTestBehavior.deferToChild,  // デフォルトの挙動を確認
          onTapDown: (_) => debugPrint('tap down'),
          child: Padding(
            padding: const EdgeInsets.all(50),
            child: Container(
              color: Colors.red,
              height: 300,
              width: 300,
            ),
          ),
        ),
      ],
    );
  }
}
```

`Padding` の余白領域の `onTapDown` は反応していなかった。

![opaque_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/7d235428-09bf-e371-be32-6759a064a9bf.gif)

ここで `deferToChild` を `opaque` に変更してみる。

```java:
class MyGestureDetector extends StatelessWidget {
  const MyGestureDetector({super.key});

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional.center,
      children: [
        Container(
          color: Colors.blue,
          height: 400,
          width: 400,
        ),
        GestureDetector(
          behavior: HitTestBehavior.opaque,  // opaque の時どうなる？
          onTapDown: (_) => debugPrint('tap down'),
          child: Padding(
            padding: const EdgeInsets.all(50),
            child: Container(
              color: Colors.red,
              height: 300,
              width: 300,
            ),
          ),
        ),
      ],
    );
  }
}
```

すると、`Padding` の余白領域がタップへ反応するようになった。

`opaque` を指定したことで「透明」の余白領域を持つ `GestureDetector` 全体が「不透明」扱いになったためと考えられる。

![opaque_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/9eafdc93-13f8-a25f-7902-88b41f7deec3.gif)

また`opaque` には、視覚的に背後に配置されたウィジェットの Hit Test による検出を妨げる効果がある。

これについても検証してみる。

今回は、赤い `GestureDetector` の「余白」が背後の青い `GestureDetector` の Hit Test を妨げるかどうかだけを見たいので、先程のサンプルの青い領域にのみ `onTapDown` を設定しておく。

まずはデフォルト値の `deferToChild` から。特に余白領域のタップに注目する。

```java:背後のウィジェットの検出を妨げる（deferToChild）
class MyGestureDetector extends StatelessWidget {
  const MyGestureDetector({super.key});

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional.center,
      children: [
        GestureDetector(
          // 背後にのみ設定しておく
          onTapDown: (_) => debugPrint('[background] tap down'),
          child: Container(
            color: Colors.blue,
            height: 400,
            width: 400,
          ),
        ),
        GestureDetector(
          behavior: HitTestBehavior.deferToChild, // デフォルト値
          child: Padding(
            padding: const EdgeInsets.all(50),
            child: Container(
              color: Colors.red,
              height: 300,
              width: 300,
            ),
          ),
        ),
      ],
    );
  }
}
```

赤い `GestureDetector` の余白領域は、背後の青い `GestureDetector` の検出を妨げなかった。

![opaque_3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/25bb87df-d455-ae6c-bd4f-5e9cedbbd22e.gif)

`deferToChild` を `opaque` に変更してみる。

```java:背後のウィジェットの検出を妨げる（opaque）
class MyGestureDetector extends StatelessWidget {
  const MyGestureDetector({super.key});

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional.center,
      children: [
        GestureDetector(
          onTapDown: (_) => debugPrint('[background] tap down'),
          child: Container(
            color: Colors.blue,
            height: 400,
            width: 400,
          ),
        ),
        GestureDetector(
          behavior: HitTestBehavior.opaque, // opaque にしてみる
          child: Padding(
            padding: const EdgeInsets.all(50),
            child: Container(
              color: Colors.red,
              height: 300,
              width: 300,
            ),
          ),
        ),
      ],
    );
  }
}
```

`Padding` の余白領域に対するタップには全く反応しなくなった。

前面の赤い `GestureDetector` （の余白領域）によって、後方の `GestureDetector` の Hit Test による検出が妨げられたためと考えられる。

![opaque_4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/9c3d065b-abaa-670a-9326-34b8c63bf606.gif)


#### `translucent`

*Translucent targets both receive events within their bounds and permit targets visually behind them to also receive events.*

[`opaque`](#opaque) 同様に、透明な余白領域を含む自身を Hit Test 対象に追加するが、`opaque`（不透明） とは異なり**半透明**（translucent）であるため、視覚上、背後に位置するウィジェットが Hit Test によって検出されることを阻まない。

特に `oqaque` との挙動の違いを検証してみた。

使用するサンプルはこちら（[`opaque`](#opaque) の時と同じ）。

```java:translucent の挙動を検証する
class MyGestureDetector extends StatelessWidget {
  const MyGestureDetector({super.key});

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional.center,
      children: [
        Container(
          color: Colors.blue,
          height: 400,
          width: 400,
        ),
        Padding(
          padding: const EdgeInsets.all(50),
          child: Container(
            color: Colors.red,
            height: 300,
            width: 300,
          ),
        ),
      ],
    );
  }
}
```

青い正方形（`400 x 400 px`）の上に赤い正方形（`300 x 300 px`）を重ねている。赤い正方形は上下左右に余白をそれぞれ `50 px` を持っていて、余白領域を合わせると青い正方形と同じ領域を占めていることになる。

![opaque.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/e19a1d0d-bc67-423a-9f44-dfdee141f331.png)

[`opaque`](#opaque) で検証した時の挙動をおさらいする。

赤い `GestureDetector` の余白領域の背後の青い `GestureDetector` は、 前方の赤い `GestureDetector` に設定した `opaque` によって Hit Test で検出されなくなるため、タップに反応しない。

```java:背後のウィジェットの検出を妨げる（opaque）
class MyGestureDetector extends StatelessWidget {
  const MyGestureDetector({super.key});

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional.center,
      children: [
        // 背後のウィジェット
        GestureDetector(
          onTapDown: (_) => debugPrint('[background] tap down'),
          child: Container(
            color: Colors.blue,
            height: 400,
            width: 400,
          ),
        ),
        // 前方のウィジェット
        GestureDetector(
          behavior: HitTestBehavior.opaque, // opaque
          child: Padding(
            padding: const EdgeInsets.all(50),
            child: Container(
              color: Colors.red,
              height: 300,
              width: 300,
            ),
          ),
        ),
      ],
    );
  }
}
```

![opaque_4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/9c3d065b-abaa-670a-9326-34b8c63bf606.gif)

これを `translucent` に変更してみる。

```java:translucent の挙動
class MyGestureDetector extends StatelessWidget {
  const MyGestureDetector({super.key});

  @override
  Widget build(BuildContext context) {
    return Stack(
      alignment: AlignmentDirectional.center,
      children: [
        // 背後のウィジェット
        GestureDetector(
          onTapDown: (_) => debugPrint('[background] tap down'),
          child: Container(
            color: Colors.blue,
            height: 400,
            width: 400,
          ),
        ),
        // 前方のウィジェット
        GestureDetector(
          behavior: HitTestBehavior.translucent, // translucent に変更
          child: Padding(
            padding: const EdgeInsets.all(50),
            child: Container(
              color: Colors.red,
              height: 300,
              width: 300,
            ),
          ),
        ),
      ],
    );
  }
}
```

すると余白領域をタップした際に、背後の青い `GestureDetector` が反応するようになった。

赤い `GestureDetector` の特に余白領域が、不透明（opaque）から半透明（translucent）扱いになったことで、背後の青い領域が Hit Test によって検出されるようになったためと考えられる。

![translucent.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1069520/d4a4dd01-173b-2e0d-0c84-4629347e094c.gif)


### その他

| 引数 | 内容 |
|:-|:-|
| `excludeFromSemantics` | セマンティクス（アクセシビリティツールでの操作情報）からこのウィジェットを除外するかどうかの指定<br>- `true`<br>- `false`（デフォルト） |
| `supportedDevices` | `GestureDetector` が反応するデバイスを制限するためのセット |
| `trackpadScrollCausesScale` | トラックパッドのスクロールジェスチャーをスケール操作として扱うかどうか<br>- `true`<br>- `false`（デフォルト） |
| `trackpadScrollToScaleFactor` | トラックパッドスクロールによるスケール操作の倍率<br>（デフォルト値）`kDefaultTrackpadScrollToScaleFactor =  Offset(0, -1/200)` |