import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; class TerminalGestureDetector extends StatefulWidget { const TerminalGestureDetector({ super.key, this.child, this.onSingleTapUp, this.onTapUp, this.onTapDown, this.onSecondaryTapDown, this.onSecondaryTapUp, this.onTertiaryTapDown, this.onTertiaryTapUp, this.onLongPressStart, this.onLongPressMoveUpdate, this.onLongPressUp, this.onDragStart, this.onDragUpdate, this.onDoubleTapDown, }); final Widget? child; final GestureTapUpCallback? onTapUp; final GestureTapUpCallback? onSingleTapUp; final GestureTapDownCallback? onTapDown; final GestureTapDownCallback? onSecondaryTapDown; final GestureTapUpCallback? onSecondaryTapUp; final GestureTapDownCallback? onDoubleTapDown; final GestureTapDownCallback? onTertiaryTapDown; final GestureTapUpCallback? onTertiaryTapUp; final GestureLongPressStartCallback? onLongPressStart; final GestureLongPressMoveUpdateCallback? onLongPressMoveUpdate; final GestureLongPressUpCallback? onLongPressUp; final GestureDragStartCallback? onDragStart; final GestureDragUpdateCallback? onDragUpdate; @override State createState() => _TerminalGestureDetectorState(); } class _TerminalGestureDetectorState extends State { Timer? _doubleTapTimer; Offset? _lastTapOffset; // True if a second tap down of a double tap is detected. Used to discard // subsequent tap up / tap hold of the same tap. bool _isDoubleTap = false; // The down handler is force-run on success of a single tap and optimistically // run before a long press success. void _handleTapDown(TapDownDetails details) { widget.onTapDown?.call(details); if (_doubleTapTimer != null && _isWithinDoubleTapTolerance(details.globalPosition)) { // If there was already a previous tap, the second down hold/tap is a // double tap down. widget.onDoubleTapDown?.call(details); _doubleTapTimer!.cancel(); _doubleTapTimeout(); _isDoubleTap = true; } } void _handleTapUp(TapUpDetails details) { if (!_isDoubleTap) { widget.onSingleTapUp?.call(details); _lastTapOffset = details.globalPosition; _doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout); } _isDoubleTap = false; } void _doubleTapTimeout() { _doubleTapTimer = null; _lastTapOffset = null; } bool _isWithinDoubleTapTolerance(Offset secondTapOffset) { if (_lastTapOffset == null) { return false; } final Offset difference = secondTapOffset - _lastTapOffset!; return difference.distance <= kDoubleTapSlop; } @override Widget build(BuildContext context) { final gestures = {}; gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(debugOwner: this), (TapGestureRecognizer instance) { instance ..onTapDown = _handleTapDown ..onTapUp = _handleTapUp ..onSecondaryTapDown = widget.onSecondaryTapDown ..onSecondaryTapUp = widget.onSecondaryTapUp ..onTertiaryTapDown = widget.onTertiaryTapDown ..onTertiaryTapUp = widget.onTertiaryTapUp; }, ); gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => LongPressGestureRecognizer( debugOwner: this, supportedDevices: { PointerDeviceKind.touch, // PointerDeviceKind.mouse, // for debugging purposes only }, ), (LongPressGestureRecognizer instance) { instance ..onLongPressStart = widget.onLongPressStart ..onLongPressMoveUpdate = widget.onLongPressMoveUpdate ..onLongPressUp = widget.onLongPressUp; }, ); gestures[PanGestureRecognizer] = GestureRecognizerFactoryWithHandlers( () => PanGestureRecognizer( debugOwner: this, supportedDevices: {PointerDeviceKind.mouse}, ), (PanGestureRecognizer instance) { instance ..dragStartBehavior = DragStartBehavior.down ..onStart = widget.onDragStart ..onUpdate = widget.onDragUpdate; }, ); return RawGestureDetector( gestures: gestures, excludeFromSemantics: true, child: widget.child, ); } }