Creating Custom Gesture Recognizers for Specific Needs in Flutter

Flutter provides a rich set of built-in gesture recognizers like TapGestureRecognizer, LongPressGestureRecognizer, and PanGestureRecognizer. However, there are situations where you need to recognize custom or compound gestures unique to your app’s interactions. Flutter allows you to create custom gesture recognizers to meet these specific needs.

Understanding Gesture Recognizers

Gesture recognizers are classes that analyze touch events and determine if a particular gesture has occurred. When a gesture is recognized, the recognizer triggers an associated callback, allowing your app to respond to the user’s input. Flutter’s gesture recognition system is designed to be flexible and extensible.

Why Create Custom Gesture Recognizers?

  • Unique Interactions: Implement interactions beyond standard tap, drag, or scale gestures.
  • Compound Gestures: Recognize combinations of gestures, such as a tap followed by a swipe.
  • Custom Logic: Incorporate application-specific logic into gesture recognition.

How to Create Custom Gesture Recognizers in Flutter

Creating a custom gesture recognizer involves extending GestureRecognizer or one of its subclasses and overriding key methods to analyze touch events.

Step 1: Create a Custom Gesture Recognizer Class

Define a new class that extends GestureRecognizer. Typically, you might extend PrimaryPointerGestureRecognizer for simpler recognizers or MultiDragGestureRecognizer for more complex multi-pointer interactions.


import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';

class CustomSwipeGestureRecognizer extends OneSequenceGestureRecognizer {
  /// Callback when a swipe gesture is recognized.
  final GestureLongPressCallback? onLongPress;

  CustomSwipeGestureRecognizer({
    Object? debugOwner,
    this.onLongPress,
    PointerDeviceKind? kind,
  }) : super(
         debugOwner: debugOwner,
         kind: kind,
       );

  bool _hasSufficientDistance = false;
  Offset? _initialPosition;
  
  @override
  void handleEvent(PointerEvent event) {
      if (event is PointerMoveEvent) {
          final delta = event.position - _initialPosition!;
          if (delta.distance > 50) {
              _hasSufficientDistance = true;
              acceptGesture(event.pointer);
          }
      } else if (event is PointerUpEvent) {
          if (_hasSufficientDistance) {
              // Gesture was accepted, so it already called the callback
              resolve(GestureDisposition.accepted);
          } else {
              rejectGesture(event.pointer);
          }
      }
  }

  @override
  void didReject(PointerEvent event) {
      stopTrackingPointer(event.pointer);
  }

  @override
  void didStartTracking(PointerEvent event) {
      _initialPosition = event.position;
      _hasSufficientDistance = false;
  }

  @override
  void dispose() {
    super.dispose();
  }

  @override
  String get debugDescription => 'custom_swipe';
}

Explanation:

  • The custom gesture recognizer, `CustomSwipeGestureRecognizer`, extends `OneSequenceGestureRecognizer`, useful for recognizing single-pointer gestures.
  • `onLongPress` callback: This field is used to provide a callback function when the long press gesture is recognized.
  • The `handleEvent` method is the core logic that analyzes the events.
  • We define the threshold, initiate value.
  • `acceptGesture()` is called if acceptable.

Step 2: Implement Gesture Recognition Logic

Override methods like handleEvent, didStartTracking, and acceptGesture to implement the recognition logic.


  @override
  void handleEvent(PointerEvent event) {
    if (event is PointerMoveEvent) {
      // Calculate distance from the starting point
      final delta = event.position - _initialPosition!;
      if (delta.distance > kMinTravelDistance) {
        _hasSufficientDistance = true;
        acceptGesture(event.pointer);
      }
    } else if (event is PointerUpEvent) {
      if (_hasSufficientDistance) {
        // Gesture was accepted, so it already called the callback
        resolve(GestureDisposition.accepted);
      } else {
        rejectGesture(event.pointer);
      }
    }
  }

  @override
  void didReject(PointerEvent event) {
    stopTrackingPointer(event.pointer);
  }

  @override
  void didStartTracking(PointerEvent event) {
    _initialPosition = event.position;
    _hasSufficientDistance = false;
  }
  • `handleEvent(PointerEvent event)`: This is the main method to override. Inside it, analyze `PointerEvent` objects (such as `PointerDownEvent`, `PointerMoveEvent`, `PointerUpEvent`, etc.) to determine if the gesture is progressing as expected. Update the internal state (e.g., checking for sufficient distance moved, duration of touch, number of fingers) and potentially call `acceptGesture()` when the gesture is recognized.
  • `didStartTracking(PointerEvent event)`: Called when a pointer is added for consideration. Initialize any state here, like starting timestamps, or recording initial pointer positions. Calling `stopTrackingPointer(event.pointer)` will cause this recognizer to ignore this pointer event.
  • `acceptGesture(int pointer)`: Called when the gesture is fully recognized (for a `OneSequenceGestureRecognizer`) and should trigger its callback. After this method is called, further pointer events should typically be ignored. You’d usually call your gesture callback (e.g., `onLongPress()`) in this method.
  • `didReject(PointerEvent event)`: Clean up and release all of the resources.

Step 3: Using the Custom Gesture Recognizer in a Widget

Attach your custom gesture recognizer to a widget using RawGestureDetector. Here’s how to use the custom swipe gesture recognizer in a Flutter widget:


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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Custom Swipe Gesture'),
        ),
        body: Center(
          child: RawGestureDetector(
            gestures: &ltType, GestureRecognizer&gt{
              CustomSwipeGestureRecognizer:
                  GestureRecognizerFactoryWithHandlers&ltCustomSwipeGestureRecognizer&gt(
                () => CustomSwipeGestureRecognizer(
                  debugOwner: this,
                  onLongPress: () {
                    print('Custom Swipe Gesture Recognized!');
                  },
                ),
                (CustomSwipeGestureRecognizer instance) {},
              ),
            },
            child: Container(
              width: 200,
              height: 200,
              color: Colors.blue,
              child: Center(
                child: Text(
                  'Swipe Here',
                  style: TextStyle(color: Colors.white),
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}
  • **`RawGestureDetector`**: Allows the usage of gesture recognizers directly, and accepts a `Map` to define which recognizers to use.
  • **`GestureRecognizerFactoryWithHandlers`**: Used to create gesture recognizer instances and provide handlers.
    Create an instance of `CustomSwipeGestureRecognizer` with a debug owner and the callback `onLongPress` for when the gesture is detected.

Step 4: Create a Custom Recognizer for more advance multi-touch

You can extends `MultiDragGestureRecognizer`, the complete Example of implementation as below :


import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';

/// Recognizes when the user has pressed multiple pointers (fingers, mouse
/// pointers) on the screen for a particular duration.
///
/// The [onMultipleTap] callback is called when the gesture is detected.
///
/// This example shows how to use the [MultipleTapGestureRecognizer]:
///
/// ```dart
/// RawGestureDetector(
///   gestures: {
///     MultipleTapGestureRecognizer:
///         GestureRecognizerFactoryWithHandlers(
///       () => MultipleTapGestureRecognizer(maxPointers: 3),
///       (MultipleTapGestureRecognizer instance) {
///         instance
///           ..onMultipleTap = (int count) {
///             setState(() {
///               _event = 'Multi-Tap Count: $count';
///             });
///           }
///           ..onStart = () => setState(() {
///                 _event = 'onStart';
///               });
///       },
///     ),
///   },
///   child: Container(
///     alignment: Alignment.center,
///     child: Text('$_event', textScaleFactor: 5),
///   ),
/// )
/// ```
class MultipleTapGestureRecognizer extends MultiDragGestureRecognizer {
  /// Initialize the object.
  ///
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
  MultipleTapGestureRecognizer({
    super.debugOwner,
    this.maxPointers = 2,
    super.kind,
    this.onStart,
  });

  /// Called when multiple tap
  ValueChanged? onMultipleTap;

  ///
  VoidCallback? onStart;

  ///  Count how many pointers
  int currentPointerCount = 0;

  ///
  int maxPointers;

  @override
  void handleEvent(PointerEvent event) {
    super.handleEvent(event);
    if (!event.synthesized) {
      if (event is PointerDownEvent) {
        currentPointerCount++;
      } else if (event is PointerUpEvent) {
        currentPointerCount--;
      }

      if (currentPointerCount == maxPointers) {
        // if ($currentPointerCount == 1) {
        onStart?.call();
        // }

        // final LongPressStartDetails details = LongPressStartDetails(
        //   globalPosition: details.globalPosition,
        //   localPosition: details.localPosition,
        // );
        // invokeCallback('onLongPressStart', () => onMultipleTap?.call(event.localPosition));

        onMultipleTap?.call(currentPointerCount);
      }
    }
  }

  @override
  void addAllowedPointer(PointerDownEvent event) {
    acceptGesture(event.pointer);
  }

  @override
  String get debugDescription => 'multiple_tap';

  @override
  void dispose() {
    currentPointerCount = 0;
    super.dispose();
  }
}
  • **MultiDragGestureRecognizer**: allows creating a gesture to detect multi touch events .

Conclusion

Creating custom gesture recognizers in Flutter provides a powerful way to implement application-specific interactions and gestures beyond the standard set. By extending GestureRecognizer, overriding relevant methods, and integrating with RawGestureDetector, you can tailor gesture recognition to meet your application’s unique needs, providing a more intuitive and engaging user experience. Proper handling and implementation are critical for effective and reliable gesture detection.