import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; class ImmediateTapGestureRecognizer extends OneSequenceGestureRecognizer { ImmediateTapGestureRecognizer({ super.debugOwner, super.supportedDevices, super.allowedButtonsFilter, required this.onTapDown, required this.onTapUp, required this.onTapCancel, this.onTap, }); final GestureTapDownCallback onTapDown; final GestureTapUpCallback onTapUp; final GestureTapCancelCallback onTapCancel; final GestureTapCallback? onTap; PointerUpEvent? _up; int _activePointer = 0; bool _sentTapDown = false; bool _wonArena = false; @override bool isPointerPanZoomAllowed(PointerPanZoomStartEvent event) => false; @override bool isPointerAllowed(PointerDownEvent event) => _activePointer == 0 && super.isPointerAllowed(event); @override void addAllowedPointer(PointerDownEvent event) { super.addAllowedPointer(event); _activePointer = event.pointer; _sentTapDown = false; _wonArena = false; } @override void handleEvent(PointerEvent event) { if (event.pointer != _activePointer) { stopTrackingPointer(event.pointer); return; } if (event is PointerDownEvent) { _handleTapDown(event); } else if (event is PointerMoveEvent) { _handlePointerMove(event); } else if (event is PointerUpEvent) { _up = event; _handlePointerUp(event); } stopTrackingIfPointerNoLongerDown(event); } void _handleTapDown(PointerDownEvent event) { if (_sentTapDown) return; _sentTapDown = true; final details = TapDownDetails( globalPosition: event.position, localPosition: event.localPosition, kind: event.kind, ); invokeCallback('onTapDown', () => onTapDown(details)); } void _handlePointerMove(PointerMoveEvent event) { if (event.delta.distanceSquared > 2.0) { _cancelGesture('pointer moved'); stopTrackingPointer(event.pointer); } } void _handlePointerUp(PointerUpEvent event) { if (_wonArena && _sentTapDown) { _handleTapUp(event); } } void _handleTapUp(PointerUpEvent event) { if (_sentTapDown) { final details = TapUpDetails( globalPosition: event.position, localPosition: event.localPosition, kind: event.kind, ); invokeCallback('onTapUp', () => onTapUp(details)); if (onTap != null) { invokeCallback('onTap', onTap!); } } _reset(); } void _cancelGesture(String reason) { if (_sentTapDown) { invokeCallback('onTapCancel: $reason', onTapCancel); } _reset(); } void _reset() { _activePointer = 0; _up = null; _sentTapDown = false; _wonArena = false; } @override void acceptGesture(int pointer) { super.acceptGesture(pointer); if (pointer == _activePointer) { _wonArena = true; if (_up != null && _sentTapDown) { _handleTapUp(_up!); } } } @override void rejectGesture(int pointer) { super.rejectGesture(pointer); if (pointer == _activePointer) { _cancelGesture('gesture rejected by arena'); stopTrackingPointer(pointer); } } @override void didStopTrackingLastPointer(int pointer) { // wait for arena } @override void dispose() { if (_sentTapDown) { _cancelGesture('disposed'); } _reset(); super.dispose(); } @override String get debugDescription => 'immediate tap'; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(IntProperty('activePointer', _activePointer)) ..add( FlagProperty( 'sentTapDown', value: _sentTapDown, ifTrue: 'has sentTapDown', ), ) ..add(FlagProperty('wonArena', value: _wonArena, ifTrue: 'wonArena')) ..add( DiagnosticsProperty( 'pointerUpEvent', _up, defaultValue: null, ), ); } }