import 'dart:async'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; enum CustomTouchGestureState { none, oneFingerPan, twoFingerScale, twoFingerVerticalDrag, twoFingerHorizontalDrag, twoFingerPan } const kScaleSlop = kPrecisePointerPanSlop / 10; class CustomTouchGestureRecognizer extends ScaleGestureRecognizer { CustomTouchGestureRecognizer({ Object? debugOwner, Set? supportedDevices, }) : super( debugOwner: debugOwner, supportedDevices: supportedDevices, ) { _init(); } // oneFingerPan GestureDragStartCallback? onOneFingerPanStart; GestureDragUpdateCallback? onOneFingerPanUpdate; GestureDragEndCallback? onOneFingerPanEnd; // twoFingerScale GestureScaleStartCallback? onTwoFingerScaleStart; GestureScaleUpdateCallback? onTwoFingerScaleUpdate; GestureScaleEndCallback? onTwoFingerScaleEnd; // twoFingerVerticalDrag GestureDragStartCallback? onTwoFingerVerticalDragStart; GestureDragUpdateCallback? onTwoFingerVerticalDragUpdate; GestureDragEndCallback? onTwoFingerVerticalDragEnd; // twoFingerHorizontalDrag GestureDragStartCallback? onTwoFingerHorizontalDragStart; GestureDragUpdateCallback? onTwoFingerHorizontalDragUpdate; GestureDragEndCallback? onTwoFingerHorizontalDragEnd; // twoFingerPan GestureDragStartCallback? onTwoFingerPanStart; GestureDragUpdateCallback? onTwoFingerPanUpdate; GestureDragEndCallback? onTwoFingerPanEnd; void _init() { debugPrint("CustomTouchGestureRecognizer init"); onStart = (d) { if (d.pointerCount == 1) { _currentState = CustomTouchGestureState.oneFingerPan; if (onOneFingerPanStart != null) { onOneFingerPanStart!(DragStartDetails( localPosition: d.localFocalPoint, globalPosition: d.focalPoint)); } debugPrint("start pan"); } else if (d.pointerCount == 2) { _currentState = CustomTouchGestureState.none; startWatchTimer(); } else { _currentState = CustomTouchGestureState.none; _reset(); } }; onUpdate = (d) { if (_isWatch) { _updateCompute(d); return; } if (_currentState != CustomTouchGestureState.none) { switch (_currentState) { case CustomTouchGestureState.oneFingerPan: if (onOneFingerPanUpdate != null) { onOneFingerPanUpdate!(_getDragUpdateDetails(d)); } break; case CustomTouchGestureState.twoFingerScale: if (onTwoFingerScaleUpdate != null) { onTwoFingerScaleUpdate!(d); } break; case CustomTouchGestureState.twoFingerHorizontalDrag: if (onTwoFingerHorizontalDragUpdate != null) { onTwoFingerHorizontalDragUpdate!(_getDragUpdateDetails(d)); } break; case CustomTouchGestureState.twoFingerVerticalDrag: if (onTwoFingerVerticalDragUpdate != null) { onTwoFingerVerticalDragUpdate!(_getDragUpdateDetails(d)); } break; case CustomTouchGestureState.twoFingerPan: if (onTwoFingerPanUpdate != null) { onTwoFingerPanUpdate!(_getDragUpdateDetails(d)); } break; default: break; } return; } }; onEnd = (d) { debugPrint("ScaleGestureRecognizer onEnd"); // end switch (_currentState) { case CustomTouchGestureState.oneFingerPan: debugPrint("TwoFingerState.pan onEnd"); if (onOneFingerPanEnd != null) { onOneFingerPanEnd!(_getDragEndDetails(d)); } break; case CustomTouchGestureState.twoFingerScale: debugPrint("TwoFingerState.scale onEnd"); if (onTwoFingerScaleEnd != null) { onTwoFingerScaleEnd!(d); } break; case CustomTouchGestureState.twoFingerHorizontalDrag: debugPrint("TwoFingerState.horizontal onEnd"); if (onTwoFingerHorizontalDragEnd != null) { onTwoFingerHorizontalDragEnd!(_getDragEndDetails(d)); } break; case CustomTouchGestureState.twoFingerVerticalDrag: debugPrint("TwoFingerState.vertical onEnd"); if (onTwoFingerVerticalDragEnd != null) { onTwoFingerVerticalDragEnd!(_getDragEndDetails(d)); } break; case CustomTouchGestureState.twoFingerPan: debugPrint("TwoFingerState.twoFingerPan onEnd"); if (onTwoFingerPanEnd != null) { onTwoFingerPanEnd!(_getDragEndDetails(d)); } break; default: break; } _currentState = CustomTouchGestureState.none; _reset(); }; } var _currentState = CustomTouchGestureState.none; var _isWatch = false; Timer? _timer; double _sumScale = 0; double _sumVertical = 0; double _sumHorizontal = 0; void _clearSum() { _sumScale = 0; _sumVertical = 0; _sumHorizontal = 0; } void _reset() { _isWatch = false; _clearSum(); if (_timer != null) _timer!.cancel(); } void _updateCompute(ScaleUpdateDetails d) { _sumScale += d.scale - 1; _sumHorizontal += d.focalPointDelta.dx; _sumVertical += d.focalPointDelta.dy; // start if (_sumScale.abs() > kScaleSlop) { debugPrint("start Scale"); _currentState = CustomTouchGestureState.twoFingerScale; if (onTwoFingerScaleStart != null) { onTwoFingerScaleStart!(ScaleStartDetails( localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint)); } _reset(); } else if (_sumVertical.abs() > kPrecisePointerPanSlop && _sumHorizontal.abs() < kPrecisePointerHitSlop) { debugPrint("start Vertical"); if (onTwoFingerVerticalDragStart != null) { _getDragStartDetails(d); } _currentState = CustomTouchGestureState.twoFingerVerticalDrag; _reset(); } else if ((_sumHorizontal.abs() + _sumVertical.abs()) > kPrecisePointerPanSlop) { debugPrint("start TwoFingerPan"); _currentState = CustomTouchGestureState.twoFingerPan; if (onTwoFingerPanStart != null) { onTwoFingerPanStart!(_getDragStartDetails(d)); } _reset(); } } void startWatchTimer() { debugPrint("startWatchTimer"); _isWatch = true; _clearSum(); if (_timer != null) _timer!.cancel(); _timer = Timer(const Duration(milliseconds: 200), _reset); } DragStartDetails _getDragStartDetails(ScaleUpdateDetails d) => DragStartDetails( globalPosition: d.focalPoint, localPosition: d.localFocalPoint, ); DragUpdateDetails _getDragUpdateDetails(ScaleUpdateDetails d) => DragUpdateDetails( globalPosition: d.focalPoint, localPosition: d.localFocalPoint, delta: d.focalPointDelta); DragEndDetails _getDragEndDetails(ScaleEndDetails d) => DragEndDetails(velocity: d.velocity); } class HoldTapMoveGestureRecognizer extends GestureRecognizer { HoldTapMoveGestureRecognizer({ Object? debugOwner, Set? supportedDevices, }) : super( debugOwner: debugOwner, supportedDevices: supportedDevices, ); GestureDragStartCallback? onHoldDragStart; GestureDragUpdateCallback? onHoldDragUpdate; GestureDragDownCallback? onHoldDragDown; GestureDragCancelCallback? onHoldDragCancel; bool _isStart = false; Timer? _firstTapUpTimer; // 第一次点击后的计时 超时未等到第二次操作则reject Timer? _secondTapDownTimer; // 第二次点击后的计时 期间内有其他的操作则reject 超时则判定成功 drag update _TapTracker? _firstTap; _TapTracker? _secondTap; final Map _trackers = {}; @override bool isPointerAllowed(PointerDownEvent event) { if (_firstTap == null) { switch (event.buttons) { case kPrimaryButton: if (onHoldDragStart == null && onHoldDragUpdate == null && onHoldDragCancel == null) { return false; } break; default: return false; } } return super.isPointerAllowed(event); } @override void addAllowedPointer(PointerDownEvent event) { // 检测按下事件 if (_firstTap != null) { if (!_firstTap!.isWithinGlobalTolerance(event, kDoubleTapSlop)) { // Ignore out-of-bounds second taps. return; } else if (!_firstTap!.hasElapsedMinTime() || !_firstTap!.hasSameButton(event)) { // Restart when the second tap is too close to the first (touch screens // often detect touches intermittently), or when buttons mismatch. _reset(); return _trackTap(event); } else if (onHoldDragDown != null) { invokeCallback( 'onHoldDragDown', () => onHoldDragDown!(DragDownDetails( globalPosition: event.position, localPosition: event.localPosition))); } } _trackTap(event); // 捕捉第一次tap } void _trackTap(PointerDownEvent event) { _stopFirstTapUpTimer(); _stopSecondTapDownTimer(); final _TapTracker tracker = _TapTracker( event: event, entry: GestureBinding.instance!.gestureArena.add(event.pointer, this), doubleTapMinTime: kDoubleTapMinTime, gestureSettings: gestureSettings, ); _trackers[event.pointer] = tracker; tracker.startTrackingPointer(_handleEvent, event.transform); } // 实际的逻辑应该是第二次down后一段时间没有抬起则表示start 刚好是双击取反 void _handleEvent(PointerEvent event) { final _TapTracker tracker = _trackers[event.pointer]!; if (event is PointerUpEvent) { if (_firstTap == null && _secondTap == null) { _registerFirstTap(tracker); } else { // 检测到其他的抬起事件则取消 _reject(tracker); } } else if (event is PointerDownEvent) { if (_firstTap != null && _secondTap == null) { _registerSecondTap(tracker); } } else if (event is PointerMoveEvent) { // 检测到first tap move 则取消,检测到second tap move且已经通过竞技场则update if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) { if (_firstTap != null && _firstTap!.pointer == event.pointer) { // first tap move _reject(tracker); } else if (_secondTap != null && _secondTap!.pointer == event.pointer) { // debugPrint("_secondTap move"); // second tap move if (!_isStart) { _resolve(); } if (onHoldDragUpdate != null) onHoldDragUpdate!(DragUpdateDetails( globalPosition: event.position, localPosition: event.localPosition, delta: event.delta)); } } } else if (event is PointerCancelEvent) { _reject(tracker); } } @override void acceptGesture(int pointer) {} @override void rejectGesture(int pointer) { _TapTracker? tracker = _trackers[pointer]; // If tracker isn't in the list, check if this is the first tap tracker if (tracker == null && _firstTap != null && _firstTap!.pointer == pointer) { tracker = _firstTap; } // If tracker is still null, we rejected ourselves already if (tracker != null) { _reject(tracker); } } void _resolve() { _stopSecondTapDownTimer(); _firstTap?.entry.resolve(GestureDisposition.accepted); _secondTap?.entry.resolve(GestureDisposition.accepted); _isStart = true; // TODO start details if (onHoldDragStart != null) onHoldDragStart!(DragStartDetails()); } void _reject(_TapTracker tracker) { _checkCancel(); _isStart = false; _trackers.remove(tracker.pointer); tracker.entry.resolve(GestureDisposition.rejected); _freezeTracker(tracker); _reset(); } @override void dispose() { _reset(); super.dispose(); } void _reset() { _isStart = false; // debugPrint("reset"); _stopFirstTapUpTimer(); _stopSecondTapDownTimer(); if (_firstTap != null) { if (_trackers.isNotEmpty) { _checkCancel(); } // Note, order is important below in order for the resolve -> reject logic // to work properly. final _TapTracker tracker = _firstTap!; _firstTap = null; _reject(tracker); GestureBinding.instance!.gestureArena.release(tracker.pointer); if (_secondTap != null) { final _TapTracker tracker = _secondTap!; _secondTap = null; _reject(tracker); GestureBinding.instance!.gestureArena.release(tracker.pointer); } } // TODO 正确的释放资源 _firstTap = null; _secondTap = null; _clearTrackers(); } void _registerFirstTap(_TapTracker tracker) { _startFirstTapUpTimer(); GestureBinding.instance!.gestureArena.hold(tracker.pointer); // Note, order is important below in order for the clear -> reject logic to // work properly. _freezeTracker(tracker); _trackers.remove(tracker.pointer); _firstTap = tracker; } void _registerSecondTap(_TapTracker tracker) { // 清除first tap的状态 if (_firstTap != null) { _stopFirstTapUpTimer(); _freezeTracker(_firstTap!); _firstTap = null; } _startSecondTapDownTimer(); GestureBinding.instance!.gestureArena.hold(tracker.pointer); _secondTap = tracker; // TODO } void _clearTrackers() { _trackers.values.toList().forEach(_reject); assert(_trackers.isEmpty); } void _freezeTracker(_TapTracker tracker) { tracker.stopTrackingPointer(_handleEvent); } void _startFirstTapUpTimer() { _firstTapUpTimer ??= Timer(kDoubleTapTimeout, _reset); } void _startSecondTapDownTimer() { _secondTapDownTimer ??= Timer(kDoubleTapTimeout, _resolve); } void _stopFirstTapUpTimer() { if (_firstTapUpTimer != null) { _firstTapUpTimer!.cancel(); _firstTapUpTimer = null; } } void _stopSecondTapDownTimer() { if (_secondTapDownTimer != null) { _secondTapDownTimer!.cancel(); _secondTapDownTimer = null; } } void _checkCancel() { if (onHoldDragCancel != null) { invokeCallback('onHoldDragCancel', onHoldDragCancel!); } } @override String get debugDescription => 'double tap'; } class DoubleFinerTapGestureRecognizer extends GestureRecognizer { DoubleFinerTapGestureRecognizer({ Object? debugOwner, Set? supportedDevices, }) : super( debugOwner: debugOwner, supportedDevices: supportedDevices, ); GestureTapDownCallback? onDoubleFinerTapDown; GestureTapDownCallback? onDoubleFinerTap; GestureTapCancelCallback? onDoubleFinerTapCancel; Timer? _firstTapTimer; // 第一次点击后的计时 超时未等到第二次操作则reject _TapTracker? _firstTap; var _isStart = false; final Set _upTap = {}; final Map _trackers = {}; @override bool isPointerAllowed(PointerDownEvent event) { if (_firstTap == null) { switch (event.buttons) { case kPrimaryButton: if (onDoubleFinerTapDown == null && onDoubleFinerTap == null && onDoubleFinerTapCancel == null) { return false; } break; default: return false; } } return super.isPointerAllowed(event); } @override void addAllowedPointer(PointerDownEvent event) { // 检测按下事件 debugPrint("addAllowedPointer"); if (_isStart) { // second if (onDoubleFinerTapDown != null) { final TapDownDetails details = TapDownDetails( globalPosition: event.position, localPosition: event.localPosition, kind: getKindForPointer(event.pointer), ); invokeCallback( 'onDoubleFinerTapDown', () => onDoubleFinerTapDown!(details)); } } else { // first tap _isStart = true; _startFirstTapDownTimer(); } _trackTap(event); // 捕捉tap } void _trackTap(PointerDownEvent event) { final _TapTracker tracker = _TapTracker( event: event, entry: GestureBinding.instance!.gestureArena.add(event.pointer, this), doubleTapMinTime: kDoubleTapMinTime, gestureSettings: gestureSettings, ); _trackers[event.pointer] = tracker; // debugPrint("_trackers:$_trackers"); tracker.startTrackingPointer(_handleEvent, event.transform); _registerTap(tracker); } // 实际的逻辑应该是第二次down后一段时间没有抬起则表示start 刚好是双击取反 void _handleEvent(PointerEvent event) { final _TapTracker tracker = _trackers[event.pointer]!; if (event is PointerUpEvent) { debugPrint("PointerUpEvent"); _upTap.add(tracker.pointer); } else if (event is PointerMoveEvent) { if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) _reject(tracker); } else if (event is PointerCancelEvent) { _reject(tracker); } } @override void acceptGesture(int pointer) {} @override void rejectGesture(int pointer) { _TapTracker? tracker = _trackers[pointer]; // If tracker isn't in the list, check if this is the first tap tracker if (tracker == null && _firstTap != null && _firstTap!.pointer == pointer) { tracker = _firstTap; } // If tracker is still null, we rejected ourselves already if (tracker != null) { _reject(tracker); } } void _reject(_TapTracker tracker) { _trackers.remove(tracker.pointer); tracker.entry.resolve(GestureDisposition.rejected); _freezeTracker(tracker); if (_firstTap != null) { if (tracker == _firstTap) { _reset(); } else { _checkCancel(); if (_trackers.isEmpty) { _reset(); } } } } @override void dispose() { _reset(); super.dispose(); } void _reset() { _stopFirstTapUpTimer(); _firstTap = null; // TODO 正确的释放资源 _clearTrackers(); } void _registerTap(_TapTracker tracker) { GestureBinding.instance!.gestureArena.hold(tracker.pointer); // Note, order is important below in order for the clear -> reject logic to // work properly. } void _clearTrackers() { _trackers.values.toList().forEach(_reject); assert(_trackers.isEmpty); } void _freezeTracker(_TapTracker tracker) { tracker.stopTrackingPointer(_handleEvent); } void _startFirstTapDownTimer() { _firstTapTimer ??= Timer(kDoubleTapTimeout, _timeoutCheck); } void _stopFirstTapUpTimer() { if (_firstTapTimer != null) { _firstTapTimer!.cancel(); _firstTapTimer = null; } } void _timeoutCheck() { _isStart = false; if (_upTap.length == 2) { _resolve(); } else { _reset(); } _upTap.clear(); } void _resolve() { // TODO tap down details if (onDoubleFinerTap != null) onDoubleFinerTap!(TapDownDetails()); _trackers.forEach((key, value) { value.entry.resolve(GestureDisposition.accepted); }); _reset(); } void _checkCancel() { if (onDoubleFinerTapCancel != null) { invokeCallback('onHoldDragCancel', onDoubleFinerTapCancel!); } } @override String get debugDescription => 'double tap'; } /// TapTracker helps track individual tap sequences as part of a /// larger gesture. class _TapTracker { _TapTracker({ required PointerDownEvent event, required this.entry, required Duration doubleTapMinTime, required this.gestureSettings, }) : assert(doubleTapMinTime != null), assert(event != null), assert(event.buttons != null), pointer = event.pointer, _initialGlobalPosition = event.position, initialButtons = event.buttons, _doubleTapMinTimeCountdown = _CountdownZoned(duration: doubleTapMinTime); final DeviceGestureSettings? gestureSettings; final int pointer; final GestureArenaEntry entry; final Offset _initialGlobalPosition; final int initialButtons; final _CountdownZoned _doubleTapMinTimeCountdown; bool _isTrackingPointer = false; void startTrackingPointer(PointerRoute route, Matrix4? transform) { if (!_isTrackingPointer) { _isTrackingPointer = true; GestureBinding.instance!.pointerRouter .addRoute(pointer, route, transform); } } void stopTrackingPointer(PointerRoute route) { if (_isTrackingPointer) { _isTrackingPointer = false; GestureBinding.instance!.pointerRouter.removeRoute(pointer, route); } } bool isWithinGlobalTolerance(PointerEvent event, double tolerance) { final Offset offset = event.position - _initialGlobalPosition; return offset.distance <= tolerance; } bool hasElapsedMinTime() { return _doubleTapMinTimeCountdown.timeout; } bool hasSameButton(PointerDownEvent event) { return event.buttons == initialButtons; } } /// CountdownZoned tracks whether the specified duration has elapsed since /// creation, honoring [Zone]. class _CountdownZoned { _CountdownZoned({required Duration duration}) : assert(duration != null) { Timer(duration, _onTimeout); } bool _timeout = false; bool get timeout => _timeout; void _onTimeout() { _timeout = true; } } RawGestureDetector getMixinGestureDetector({ Widget? child, GestureTapUpCallback? onTapUp, GestureTapDownCallback? onDoubleTapDown, GestureDoubleTapCallback? onDoubleTap, GestureLongPressDownCallback? onLongPressDown, GestureLongPressCallback? onLongPress, GestureDragStartCallback? onHoldDragStart, GestureDragUpdateCallback? onHoldDragUpdate, GestureDragCancelCallback? onHoldDragCancel, GestureTapDownCallback? onDoubleFinerTap, GestureDragStartCallback? onOneFingerPanStart, GestureDragUpdateCallback? onOneFingerPanUpdate, GestureDragEndCallback? onOneFingerPanEnd, GestureScaleUpdateCallback? onTwoFingerScaleUpdate, GestureScaleEndCallback? onTwoFingerScaleEnd, GestureDragUpdateCallback? onTwoFingerHorizontalDragUpdate, GestureDragUpdateCallback? onTwoFingerVerticalDragUpdate, GestureDragStartCallback? onTwoFingerPanStart, GestureDragUpdateCallback? onTwoFingerPanUpdate, }) { return RawGestureDetector( child: child, gestures: { // Official TapGestureRecognizer: GestureRecognizerFactoryWithHandlers( () => TapGestureRecognizer(), (instance) { instance.onTapUp = onTapUp; }), DoubleTapGestureRecognizer: GestureRecognizerFactoryWithHandlers( () => DoubleTapGestureRecognizer(), (instance) { instance ..onDoubleTapDown = onDoubleTapDown ..onDoubleTap = onDoubleTap; }), LongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers( () => LongPressGestureRecognizer(), (instance) { instance ..onLongPressDown = onLongPressDown ..onLongPress = onLongPress; }), // Customized HoldTapMoveGestureRecognizer: GestureRecognizerFactoryWithHandlers( () => HoldTapMoveGestureRecognizer(), (instance) => { instance ..onHoldDragStart = onHoldDragStart ..onHoldDragUpdate = onHoldDragUpdate ..onHoldDragCancel = onHoldDragCancel }), DoubleFinerTapGestureRecognizer: GestureRecognizerFactoryWithHandlers< DoubleFinerTapGestureRecognizer>( () => DoubleFinerTapGestureRecognizer(), (instance) { instance.onDoubleFinerTap = onDoubleFinerTap; }), CustomTouchGestureRecognizer: GestureRecognizerFactoryWithHandlers( () => CustomTouchGestureRecognizer(), (instance) { instance ..onOneFingerPanStart = onOneFingerPanStart ..onOneFingerPanUpdate = onOneFingerPanUpdate ..onOneFingerPanEnd = onOneFingerPanEnd ..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate ..onTwoFingerScaleEnd = onTwoFingerScaleEnd ..onTwoFingerHorizontalDragUpdate = onTwoFingerHorizontalDragUpdate ..onTwoFingerVerticalDragUpdate = onTwoFingerVerticalDragUpdate ..onTwoFingerPanStart = onTwoFingerPanStart ..onTwoFingerPanUpdate = onTwoFingerPanUpdate; }) }); }