FlutterのカスタマイズGestureDetectorについての備忘録（フリックとドラッグ）


赤いボックス（Container）が表示される
それをフリックすると、print() でメッセージが出ます（デバッグコンソールに）
```ruby:title.rb

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(
          child: MyGestureWrapper(
            flick: FlickAction(
              child: ColoredBoxWidget(),
              onFlick: handleFlick,
            ),
          ),
        ),
      ),
    );
  }

  static void handleFlick() {
    print('フリックされました！');
  }
}

//-------------------- FlickAction と Wrapper -------------------------

class FlickAction {
  final Widget child;
  final VoidCallback onFlick;

  const FlickAction({required this.child, required this.onFlick});
}

class MyGestureWrapper extends StatelessWidget {
  final FlickAction flick;

  const MyGestureWrapper({super.key, required this.flick});

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanEnd: (details) {
        final velocity = details.velocity.pixelsPerSecond;
        if (velocity.distance > 1000) {
          flick.onFlick();
        }
      },
      child: DraggableFlickableWidget(child: flick.child),
    );
  }
}

//-------------------- シンプルなドラッグ対応 Widget -------------------------

class DraggableFlickableWidget extends StatefulWidget {
  final Widget child;

  const DraggableFlickableWidget({super.key, required this.child});

  @override
  State<DraggableFlickableWidget> createState() => _DraggableFlickableWidgetState();
}

class _DraggableFlickableWidgetState extends State<DraggableFlickableWidget> {
  Offset _offset = Offset.zero;

  @override
  Widget build(BuildContext context) {
    return Transform.translate(
      offset: _offset,
      child: GestureDetector(
        onPanUpdate: (details) {
          setState(() {
            _offset += details.delta;
          });
        },
        child: widget.child,
      ),
    );
  }
}

//-------------------- テスト用の赤いボックス -------------------------

class ColoredBoxWidget extends StatelessWidget {
  const ColoredBoxWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 120,
      height: 120,
      color: Colors.red,
      alignment: Alignment.center,
      child: const Text('フリックしてね', style: TextStyle(color: Colors.white)),
    );
  }
}


```

速い操作 → フリック（onFlick を呼ぶ）
遅い操作 → ドラッグ（移動だけ）
```ruby:title.rb

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(
          child: MyGestureWrapper(
            flick: FlickAction(
              child: ColoredBoxWidget(),
              onFlick: handleFlick,
            ),
          ),
        ),
      ),
    );
  }

  static void handleFlick() {
    print('✨ フリックされました！');
  }
}

//-------------------- FlickAction と Wrapper -------------------------

class FlickAction {
  final Widget child;
  final VoidCallback onFlick;

  const FlickAction({required this.child, required this.onFlick});
}

class MyGestureWrapper extends StatefulWidget {
  final FlickAction flick;

  const MyGestureWrapper({super.key, required this.flick});

  @override
  State<MyGestureWrapper> createState() => _MyGestureWrapperState();
}

class _MyGestureWrapperState extends State<MyGestureWrapper> {
  Offset _offset = Offset.zero;
  final double flickThreshold = 1000.0; // px/sec 以上でフリックと判定

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (details) {
        // 移動中は常にオフセットを更新（ドラッグ）
        setState(() {
          _offset += details.delta;
        });
      },
      onPanEnd: (details) {
        final velocity = details.velocity.pixelsPerSecond;
        final speed = velocity.distance;

        print('速度: ${speed.toStringAsFixed(2)} px/s');

        if (speed > flickThreshold) {
          widget.flick.onFlick();
        } else {
          print('🔄 ドラッグでした');
        }
      },
      child: Transform.translate(
        offset: _offset,
        child: widget.flick.child,
      ),
    );
  }
}

//-------------------- テスト用の赤いボックス -------------------------

class ColoredBoxWidget extends StatelessWidget {
  const ColoredBoxWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 120,
      height: 120,
      color: Colors.red,
      alignment: Alignment.center,
      child: const Text('フリック or ドラッグ', style: TextStyle(color: Colors.white)),
    );
  }
}


```

onLongPressStart → ロングタップ開始時に isDragging = true
onPanUpdate → isDragging == true のときのみオフセット更新
onPanEnd → 速度が速ければフリック、そうでなければ何もしない（ドラッグ終了）
```ruby:title.rb

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(
          child: MyGestureWrapper(
            flick: FlickAction(
              child: ColoredBoxWidget(),
              onFlick: handleFlick,
            ),
          ),
        ),
      ),
    );
  }

  static void handleFlick() {
    print('✨ フリックされました！');
  }
}

//-------------------- FlickAction と Wrapper -------------------------

class FlickAction {
  final Widget child;
  final VoidCallback onFlick;

  const FlickAction({required this.child, required this.onFlick});
}

class MyGestureWrapper extends StatefulWidget {
  final FlickAction flick;

  const MyGestureWrapper({super.key, required this.flick});

  @override
  State<MyGestureWrapper> createState() => _MyGestureWrapperState();
}

class _MyGestureWrapperState extends State<MyGestureWrapper> {
  Offset _offset = Offset.zero;
  bool isDragging = false;
  final double flickThreshold = 1000.0; // px/sec 以上でフリックと判定

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onLongPressStart: (_) {
        // ロングタップされたらドラッグモードに入る
        isDragging = true;
        print('🟡 ロングタップ開始 → ドラッグ許可');
      },
      onPanUpdate: (details) {
        if (isDragging) {
          setState(() {
            _offset += details.delta;
          });
        }
      },
      onPanEnd: (details) {
        final velocity = details.velocity.pixelsPerSecond;
        final speed = velocity.distance;

        print('📈 終了時速度: ${speed.toStringAsFixed(2)} px/s');

        if (speed > flickThreshold) {
          widget.flick.onFlick(); // フリック
        } else if (isDragging) {
          print('🔄 ロングタップ＋ドラッグでした');
        }

        isDragging = false; // モード解除
      },
      child: Transform.translate(
        offset: _offset,
        child: widget.flick.child,
      ),
    );
  }
}

//-------------------- テスト用の赤いボックス -------------------------

class ColoredBoxWidget extends StatelessWidget {
  const ColoredBoxWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 140,
      height: 140,
      color: Colors.red,
      alignment: Alignment.center,
      child: const Text(
        'ロングタップしてドラッグ\nまたはフリック',
        style: TextStyle(color: Colors.white),
        textAlign: TextAlign.center,
      ),
    );
  }
}


```

赤いボックスを画像に差し替えたテストコード
```ruby:title.rb

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(
          child: MyGestureWrapper(
            flick: FlickAction(
              child: ImageBoxWidget(),
              onFlick: handleFlick,
            ),
          ),
        ),
      ),
    );
  }

  static void handleFlick() {
    print('✨ フリックされました！（画像）');
  }
}

//-------------------- FlickAction と Gesture Wrapper -------------------------

class FlickAction {
  final Widget child;
  final VoidCallback onFlick;

  const FlickAction({required this.child, required this.onFlick});
}

class MyGestureWrapper extends StatefulWidget {
  final FlickAction flick;

  const MyGestureWrapper({super.key, required this.flick});

  @override
  State<MyGestureWrapper> createState() => _MyGestureWrapperState();
}

class _MyGestureWrapperState extends State<MyGestureWrapper> {
  Offset _offset = Offset.zero;
  bool isDragging = false;
  final double flickThreshold = 1000.0; // px/sec 以上でフリックと判定

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onLongPressStart: (_) {
        isDragging = true;
        print('🟡 ロングタップ開始 → ドラッグ許可');
      },
      onPanUpdate: (details) {
        if (isDragging) {
          setState(() {
            _offset += details.delta;
          });
        }
      },
      onPanEnd: (details) {
        final velocity = details.velocity.pixelsPerSecond;
        final speed = velocity.distance;

        print('📈 終了時速度: ${speed.toStringAsFixed(2)} px/s');

        if (speed > flickThreshold) {
          widget.flick.onFlick();
        } else if (isDragging) {
          print('🔄 ロングタップ＋ドラッグでした');
        }

        isDragging = false;
      },
      child: Transform.translate(
        offset: _offset,
        child: widget.flick.child,
      ),
    );
  }
}

//-------------------- テスト用の画像 -------------------------

class ImageBoxWidget extends StatelessWidget {
  const ImageBoxWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Image.network(
      'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
      width: 200,
      height: 200,
      fit: BoxFit.cover,
    );
  }
}


```

MyGestureWrapper に VoidCallback? onTap を追加
GestureDetector の onTap にバインド
```ruby:title.rb

class MyGestureWrapper extends StatefulWidget {
  final FlickAction flick;
  final VoidCallback? onTap; // 👈 追加

  const MyGestureWrapper({
    super.key,
    required this.flick,
    this.onTap, // 👈 追加
  });

  @override
  State<MyGestureWrapper> createState() => _MyGestureWrapperState();
}

class _MyGestureWrapperState extends State<MyGestureWrapper> {
  Offset _offset = Offset.zero;
  bool isDragging = false;
  final double flickThreshold = 1000.0;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: widget.onTap, // 👈 追加された onTap を GestureDetector に渡す
      onLongPressStart: (_) {
        isDragging = true;
        print('🟡 ロングタップ → ドラッグ許可');
      },
      onPanUpdate: (details) {
        if (isDragging) {
          setState(() {
            _offset += details.delta;
          });
        }
      },
      onPanEnd: (details) {
        final speed = details.velocity.pixelsPerSecond.distance;
        print('📈 終了時速度: ${speed.toStringAsFixed(2)} px/s');

        if (speed > flickThreshold) {
          widget.flick.onFlick();
        } else if (isDragging) {
          print('🔄 ロングタップ＋ドラッグでした');
        }

        isDragging = false;
      },
      child: Transform.translate(
        offset: _offset,
        child: widget.flick.child,
      ),
    );
  }
}


// 利用側
MyGestureWrapper(
  flick: FlickAction(
    child: ImageBoxWidget(),
    onFlick: () => print("🌀 フリック！"),
  ),
  onTap: () => print("👆 タップされました！"),
)

```

Direction enum を定義
FlickAction に Function(Direction) 型の onFlick を渡せるよう変更
フリック方向をベクトルから判定（XとYの絶対値比較）
```ruby:title.rb

import 'package:flutter/material.dart';
import 'dart:math' as math;

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: Scaffold(
        body: Center(
          child: MyGestureWrapper(
            flick: FlickAction(
              child: ImageBoxWidget(),
              onFlick: _handleFlick,
            ),
            onTap: _handleTap,
          ),
        ),
      ),
    );
  }

  static void _handleFlick(Direction direction) {
    print('🌀 フリックされました → $direction');
  }

  static void _handleTap() {
    print('👆 タップされました！');
  }
}

//-------------------- Direction Enum -------------------------

enum Direction { up, down, left, right }

//-------------------- FlickAction -------------------------

class FlickAction {
  final Widget child;
  final void Function(Direction direction) onFlick;

  const FlickAction({
    required this.child,
    required this.onFlick,
  });
}

//-------------------- MyGestureWrapper -------------------------

class MyGestureWrapper extends StatefulWidget {
  final FlickAction flick;
  final VoidCallback? onTap;

  const MyGestureWrapper({
    super.key,
    required this.flick,
    this.onTap,
  });

  @override
  State<MyGestureWrapper> createState() => _MyGestureWrapperState();
}

class _MyGestureWrapperState extends State<MyGestureWrapper> {
  Offset _offset = Offset.zero;
  bool isDragging = false;
  final double flickThreshold = 1000.0;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: widget.onTap,
      onLongPressStart: (_) {
        isDragging = true;
        print('🟡 ロングタップ開始 → ドラッグ許可');
      },
      onPanUpdate: (details) {
        if (isDragging) {
          setState(() {
            _offset += details.delta;
          });
          final direction = _getDirectionFromOffset(details.delta);
          print('🔄 ドラッグ方向: $direction');
        }
      },
      onPanEnd: (details) {
        final velocity = details.velocity.pixelsPerSecond;
        final speed = velocity.distance;

        print('📈 フリック速度: ${speed.toStringAsFixed(2)} px/s');

        if (speed > flickThreshold) {
          final direction = _getDirectionFromOffset(velocity);
          widget.flick.onFlick(direction);
        } else if (isDragging) {
          print('🛑 ロングタップ＋スロー移動（ドラッグ）終了');
        }

        isDragging = false;
      },
      child: Transform.translate(
        offset: _offset,
        child: widget.flick.child,
      ),
    );
  }

  Direction _getDirectionFromOffset(Offset offset) {
    final dx = offset.dx;
    final dy = offset.dy;

    if (dx.abs() > dy.abs()) {
      return dx > 0 ? Direction.right : Direction.left;
    } else {
      return dy > 0 ? Direction.down : Direction.up;
    }
  }
}

//-------------------- テスト用画像 -------------------------

class ImageBoxWidget extends StatelessWidget {
  const ImageBoxWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Image.network(
      'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg',
      width: 200,
      height: 200,
      fit: BoxFit.cover,
    );
  }
}

```