0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

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 を呼ぶ)
遅い操作 → ドラッグ(移動だけ)

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 → 速度が速ければフリック、そうでなければ何もしない(ドラッグ終了)

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,
      ),
    );
  }
}


赤いボックスを画像に差し替えたテストコード

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 にバインド

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の絶対値比較)

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,
    );
  }
}

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?