rustdesk/lib/gestures.dart
2022-02-17 15:22:14 +08:00

773 lines
23 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart';
enum CustomTouchGestureState {
none,
oneFingerPan,
twoFingerScale,
twoFingerVerticalDrag,
twoFingerHorizontalDrag
}
const kScaleSlop = kPrecisePointerPanSlop / 10;
class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
CustomTouchGestureRecognizer({
Object? debugOwner,
Set<PointerDeviceKind>? 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;
void _init() {
debugPrint("CustomTouchGestureRecognizer init");
onStart = (d) {
if (d.pointerCount == 1) {
_currentState = CustomTouchGestureState.oneFingerPan;
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;
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;
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 (onOneFingerPanStart != null) {
onOneFingerPanStart!(_getDragStartDetails(d));
}
_reset();
} else if (_sumHorizontal.abs() > kPrecisePointerPanSlop) {
debugPrint("start Horizontal");
_currentState = CustomTouchGestureState.twoFingerHorizontalDrag;
if (onTwoFingerHorizontalDragUpdate != null) {
onTwoFingerHorizontalDragUpdate!(_getDragUpdateDetails(d));
}
_reset();
} else if (_sumVertical.abs() > kPrecisePointerPanSlop) {
debugPrint("start Vertical");
if (onTwoFingerVerticalDragStart != null) {
_getDragStartDetails(d);
}
_currentState = CustomTouchGestureState.twoFingerVerticalDrag;
_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<PointerDeviceKind>? 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<int, _TapTracker> _trackers = <int, _TapTracker>{};
@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<void>(
'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<void>('onHoldDragCancel', onHoldDragCancel!);
}
}
@override
String get debugDescription => 'double tap';
}
class DoubleFinerTapGestureRecognizer extends GestureRecognizer {
DoubleFinerTapGestureRecognizer({
Object? debugOwner,
Set<PointerDeviceKind>? supportedDevices,
}) : super(
debugOwner: debugOwner,
supportedDevices: supportedDevices,
);
GestureTapDownCallback? onDoubleFinerTapDown;
GestureTapDownCallback? onDoubleFinerTap;
GestureTapCancelCallback? onDoubleFinerTapCancel;
Timer? _firstTapTimer; // 第一次点击后的计时 超时未等到第二次操作则reject
_TapTracker? _firstTap;
var _isStart = false;
final Set<int> _upTap = {};
final Map<int, _TapTracker> _trackers = <int, _TapTracker>{};
@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<void>(
'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<void>('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,
GestureDoubleTapCallback? onDoubleTap,
GestureDragStartCallback? onHoldDragStart,
GestureDragUpdateCallback? onHoldDragUpdate,
GestureDragCancelCallback? onHoldDragCancel,
GestureTapDownCallback? onDoubleFinerTap,
GestureDragStartCallback? onOneFingerPanStart,
GestureDragUpdateCallback? onOneFingerPanUpdate,
GestureScaleUpdateCallback? onTwoFingerScaleUpdate,
GestureScaleEndCallback? onTwoFingerScaleEnd,
GestureDragUpdateCallback? onTwoFingerHorizontalDragUpdate,
GestureDragUpdateCallback? onTwoFingerVerticalDragUpdate,
}) {
return RawGestureDetector(
child: child,
gestures: <Type, GestureRecognizerFactory>{
// Official
TapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(), (instance) {
instance.onTapUp = onTapUp;
}),
DoubleTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
() => DoubleTapGestureRecognizer(), (instance) {
instance.onDoubleTap = onDoubleTap;
}),
// Customized
HoldTapMoveGestureRecognizer:
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
() => HoldTapMoveGestureRecognizer(),
(instance) => {
instance
..onHoldDragStart = onHoldDragStart
..onHoldDragUpdate = onHoldDragUpdate
..onHoldDragCancel = onHoldDragCancel
}),
DoubleFinerTapGestureRecognizer: GestureRecognizerFactoryWithHandlers<
DoubleFinerTapGestureRecognizer>(
() => DoubleFinerTapGestureRecognizer(), (instance) {
instance.onDoubleFinerTap = onDoubleFinerTap;
}),
CustomTouchGestureRecognizer:
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
() => CustomTouchGestureRecognizer(), (instance) {
instance
..onOneFingerPanStart = onOneFingerPanStart
..onOneFingerPanUpdate = onOneFingerPanUpdate
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
..onTwoFingerHorizontalDragUpdate = onTwoFingerHorizontalDragUpdate
..onTwoFingerVerticalDragUpdate = onTwoFingerVerticalDragUpdate;
})
});
}