Flutterのタップイベントを同時に複数発火する

# 結論

FlutterのGestureDetectorは１回のタップで１つのイベントしか発火されないようになっているため、RawGestureDetectorを使う必要があります。

```dart
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';

class TitleScreen extends HookWidget {
  const TitleScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _GetAllScreenTapEventWidget(
        child: Center(
          child: SizedBox(
            width: 100,
            height: 100,
            child: GestureDetector(
              onTap: () => print("button tap event"),
              child: const Card(
                color: Colors.red,
                child: Text('Tap me'),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _GetAllScreenTapEventWidget extends HookWidget {
  final Widget child;
  const _GetAllScreenTapEventWidget({
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    void onTapDown(details) {
      print("all screen tap event");
    }

    return Stack(
      fit: StackFit.expand,
      children: [
        child,
        RawGestureDetector(
          behavior: HitTestBehavior.translucent,
          gestures: <Type, GestureRecognizerFactory>{
            _TapAnimationGestureRecognizer:
                GestureRecognizerFactoryWithHandlers<
                    _TapAnimationGestureRecognizer>(
              () {
                return _TapAnimationGestureRecognizer(onTapDown: onTapDown);
              },
              (_) {},
            ),
          },
        ),
      ],
    );
  }
}

class _TapAnimationGestureRecognizer extends OneSequenceGestureRecognizer {
  _TapAnimationGestureRecognizer({required this.onTapDown});
  final Function(PointerEvent) onTapDown;

  bool _isDown = false;

  @override
  void addAllowedPointer(PointerDownEvent event) {
    startTrackingPointer(event.pointer);
    // イベントを他のオブジェクトに伝搬させるためにrejectする
    resolve(GestureDisposition.rejected);
  }

  @override
  String get debugDescription => 'recognizer for duplicate tap down event';

  @override
  void didStopTrackingLastPointer(int pointer) {}

  @override
  void handleEvent(PointerEvent event) {
    if (event.down) {
      if (!_isDown) {
        onTapDown(event);
        _isDown = true;
      }
    } else {
      _isDown = false;
    }
  }
}

```

# 動作結果
上記のコードを実行すると以下のような画面が出ます。
![Screenshot_20230829-223335.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69504/583cc404-99b0-1c46-9e58-c5a41ed2db0e.png)

Tap meの領域を押すと、２つのタップイベントが発生して以下のようなログとなります。
![スクリーンショット 2023-08-29 223550.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69504/b1632461-db32-1c95-e092-e4ed2b8ff561.png)

Tap meの領域外を押すと、片方のタップイベントのみが発生して以下のようなログになります。
![スクリーンショット 2023-08-29 223622.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69504/a21eae6e-7a52-e332-24f8-b8a977625ab9.png)

# 用途
ゲームアプリ等で良くある、タップした場所が光るアニメーションを実装するために作成しました。
他のボタンがある場所を押した場合でも、タップアニメーションを表示するためにイベントを拾う必要があります。
サンプルコードのようなコードを活用して、以下のようなタップアニメーションを作りました。
![無題の動画 ‐ Clipchampで作成.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/69504/37bc257e-5ddd-ac99-d4eb-5870c28a938c.gif)

ホシヅクリというボードゲームのアプリ版で、以下にて配信中です。

iOS版
https://apps.apple.com/jp/app/%E3%83%9B%E3%82%B7%E3%83%85%E3%82%AF%E3%83%AA/id6449194423
Android版
https://play.google.com/store/apps/details?id=org.silverroom.tellus&pli=1

※執筆時点ではまだタップのアニメーションはリリースされていませんが、次回リリースに含まれる予定です。

# 参考記事
Flutterのタップイベントについてわかりやすく解説されているので、何故タップイベントが１つしか発火しないか気になる方はこちらの記事を見ることをおすすめします。
https://qiita.com/mjhd/items/8e300238e494d8755b44