mirror of
https://github.com/rustdesk/rustdesk.git
synced 2025-12-13 11:35:56 +00:00
742
flutter/lib/common/widgets/gestures.dart
Normal file
742
flutter/lib/common/widgets/gestures.dart
Normal file
@@ -0,0 +1,742 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum GestureState {
|
||||
none,
|
||||
oneFingerPan,
|
||||
twoFingerScale,
|
||||
threeFingerVerticalDrag
|
||||
}
|
||||
|
||||
class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
CustomTouchGestureRecognizer({
|
||||
Object? debugOwner,
|
||||
Set<PointerDeviceKind>? supportedDevices,
|
||||
}) : super(
|
||||
debugOwner: debugOwner,
|
||||
supportedDevices: supportedDevices,
|
||||
) {
|
||||
_init();
|
||||
}
|
||||
|
||||
// oneFingerPan
|
||||
GestureDragStartCallback? onOneFingerPanStart;
|
||||
GestureDragUpdateCallback? onOneFingerPanUpdate;
|
||||
GestureDragEndCallback? onOneFingerPanEnd;
|
||||
|
||||
// twoFingerScale : scale + pan event
|
||||
GestureScaleStartCallback? onTwoFingerScaleStart;
|
||||
GestureScaleUpdateCallback? onTwoFingerScaleUpdate;
|
||||
GestureScaleEndCallback? onTwoFingerScaleEnd;
|
||||
|
||||
// threeFingerVerticalDrag
|
||||
GestureDragStartCallback? onThreeFingerVerticalDragStart;
|
||||
GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate;
|
||||
GestureDragEndCallback? onThreeFingerVerticalDragEnd;
|
||||
|
||||
var _currentState = GestureState.none;
|
||||
Timer? _debounceTimer;
|
||||
|
||||
void _init() {
|
||||
debugPrint("CustomTouchGestureRecognizer init");
|
||||
// onStart = (d) {};
|
||||
onUpdate = (d) {
|
||||
_debounceTimer?.cancel();
|
||||
if (d.pointerCount == 1 && _currentState != GestureState.oneFingerPan) {
|
||||
onOneFingerStartDebounce(d);
|
||||
} else if (d.pointerCount == 2 &&
|
||||
_currentState != GestureState.twoFingerScale) {
|
||||
onTwoFingerStartDebounce(d);
|
||||
} else if (d.pointerCount == 3 &&
|
||||
_currentState != GestureState.threeFingerVerticalDrag) {
|
||||
_currentState = GestureState.threeFingerVerticalDrag;
|
||||
if (onThreeFingerVerticalDragStart != null) {
|
||||
onThreeFingerVerticalDragStart!(
|
||||
DragStartDetails(globalPosition: d.localFocalPoint));
|
||||
}
|
||||
debugPrint("start threeFingerScale");
|
||||
}
|
||||
if (_currentState != GestureState.none) {
|
||||
switch (_currentState) {
|
||||
case GestureState.oneFingerPan:
|
||||
if (onOneFingerPanUpdate != null) {
|
||||
onOneFingerPanUpdate!(_getDragUpdateDetails(d));
|
||||
}
|
||||
break;
|
||||
case GestureState.twoFingerScale:
|
||||
if (onTwoFingerScaleUpdate != null) {
|
||||
onTwoFingerScaleUpdate!(d);
|
||||
}
|
||||
break;
|
||||
case GestureState.threeFingerVerticalDrag:
|
||||
if (onThreeFingerVerticalDragUpdate != null) {
|
||||
onThreeFingerVerticalDragUpdate!(_getDragUpdateDetails(d));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
onEnd = (d) {
|
||||
debugPrint("ScaleGestureRecognizer onEnd");
|
||||
_debounceTimer?.cancel();
|
||||
// end
|
||||
switch (_currentState) {
|
||||
case GestureState.oneFingerPan:
|
||||
debugPrint("TwoFingerState.pan onEnd");
|
||||
if (onOneFingerPanEnd != null) {
|
||||
onOneFingerPanEnd!(_getDragEndDetails(d));
|
||||
}
|
||||
break;
|
||||
case GestureState.twoFingerScale:
|
||||
debugPrint("TwoFingerState.scale onEnd");
|
||||
if (onTwoFingerScaleEnd != null) {
|
||||
onTwoFingerScaleEnd!(d);
|
||||
}
|
||||
break;
|
||||
case GestureState.threeFingerVerticalDrag:
|
||||
debugPrint("ThreeFingerState.vertical onEnd");
|
||||
if (onThreeFingerVerticalDragEnd != null) {
|
||||
onThreeFingerVerticalDragEnd!(_getDragEndDetails(d));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
_debounceTimer = Timer(Duration(milliseconds: 200), () {
|
||||
_currentState = GestureState.none;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
void onOneFingerStartDebounce(ScaleUpdateDetails d) {
|
||||
final start = (ScaleUpdateDetails d) {
|
||||
_currentState = GestureState.oneFingerPan;
|
||||
if (onOneFingerPanStart != null) {
|
||||
onOneFingerPanStart!(DragStartDetails(
|
||||
localPosition: d.localFocalPoint, globalPosition: d.focalPoint));
|
||||
}
|
||||
};
|
||||
if (_currentState != GestureState.none) {
|
||||
_debounceTimer = Timer(Duration(milliseconds: 200), () {
|
||||
start(d);
|
||||
debugPrint("debounce start oneFingerPan");
|
||||
});
|
||||
} else {
|
||||
start(d);
|
||||
debugPrint("start oneFingerPan");
|
||||
}
|
||||
}
|
||||
|
||||
void onTwoFingerStartDebounce(ScaleUpdateDetails d) {
|
||||
final start = (ScaleUpdateDetails d) {
|
||||
_currentState = GestureState.twoFingerScale;
|
||||
if (onTwoFingerScaleStart != null) {
|
||||
onTwoFingerScaleStart!(ScaleStartDetails(
|
||||
localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint));
|
||||
}
|
||||
};
|
||||
if (_currentState == GestureState.threeFingerVerticalDrag) {
|
||||
_debounceTimer = Timer(Duration(milliseconds: 200), () {
|
||||
start(d);
|
||||
debugPrint("debounce start twoFingerScale");
|
||||
});
|
||||
} else {
|
||||
start(d);
|
||||
debugPrint("start twoFingerScale");
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
GestureDragEndCallback? onHoldDragEnd;
|
||||
|
||||
bool _isStart = false;
|
||||
|
||||
Timer? _firstTapUpTimer;
|
||||
Timer? _secondTapDownTimer;
|
||||
_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 &&
|
||||
onHoldDragEnd == 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void _handleEvent(PointerEvent event) {
|
||||
final _TapTracker tracker = _trackers[event.pointer]!;
|
||||
if (event is PointerUpEvent) {
|
||||
if (_firstTap == null && _secondTap == null) {
|
||||
_registerFirstTap(tracker);
|
||||
} else if (_secondTap != null) {
|
||||
if (event.pointer == _secondTap!.pointer) {
|
||||
if (onHoldDragEnd != null) onHoldDragEnd!(DragEndDetails());
|
||||
}
|
||||
} else {
|
||||
_reject(tracker);
|
||||
}
|
||||
} else if (event is PointerDownEvent) {
|
||||
if (_firstTap != null && _secondTap == null) {
|
||||
_registerSecondTap(tracker);
|
||||
}
|
||||
} else if (event is PointerMoveEvent) {
|
||||
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) {
|
||||
try {
|
||||
_checkCancel();
|
||||
_isStart = false;
|
||||
_trackers.remove(tracker.pointer);
|
||||
tracker.entry.resolve(GestureDisposition.rejected);
|
||||
_freezeTracker(tracker);
|
||||
_reset();
|
||||
} catch (e) {
|
||||
debugPrint("Failed to _reject:$e");
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
_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) {
|
||||
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;
|
||||
_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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
_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,
|
||||
}) : 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}) {
|
||||
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,
|
||||
GestureDragEndCallback? onHoldDragEnd,
|
||||
GestureTapDownCallback? onDoubleFinerTap,
|
||||
GestureDragStartCallback? onOneFingerPanStart,
|
||||
GestureDragUpdateCallback? onOneFingerPanUpdate,
|
||||
GestureDragEndCallback? onOneFingerPanEnd,
|
||||
GestureScaleUpdateCallback? onTwoFingerScaleUpdate,
|
||||
GestureScaleEndCallback? onTwoFingerScaleEnd,
|
||||
GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate,
|
||||
}) {
|
||||
return RawGestureDetector(
|
||||
child: child,
|
||||
gestures: <Type, GestureRecognizerFactory>{
|
||||
// Official
|
||||
TapGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||
() => TapGestureRecognizer(), (instance) {
|
||||
instance.onTapUp = onTapUp;
|
||||
}),
|
||||
DoubleTapGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
|
||||
() => DoubleTapGestureRecognizer(), (instance) {
|
||||
instance
|
||||
..onDoubleTapDown = onDoubleTapDown
|
||||
..onDoubleTap = onDoubleTap;
|
||||
}),
|
||||
LongPressGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
||||
() => LongPressGestureRecognizer(), (instance) {
|
||||
instance
|
||||
..onLongPressDown = onLongPressDown
|
||||
..onLongPress = onLongPress;
|
||||
}),
|
||||
// Customized
|
||||
HoldTapMoveGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
|
||||
() => HoldTapMoveGestureRecognizer(),
|
||||
(instance) => instance
|
||||
..onHoldDragStart = onHoldDragStart
|
||||
..onHoldDragUpdate = onHoldDragUpdate
|
||||
..onHoldDragCancel = onHoldDragCancel
|
||||
..onHoldDragEnd = onHoldDragEnd),
|
||||
DoubleFinerTapGestureRecognizer: GestureRecognizerFactoryWithHandlers<
|
||||
DoubleFinerTapGestureRecognizer>(
|
||||
() => DoubleFinerTapGestureRecognizer(), (instance) {
|
||||
instance.onDoubleFinerTap = onDoubleFinerTap;
|
||||
}),
|
||||
CustomTouchGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
|
||||
() => CustomTouchGestureRecognizer(), (instance) {
|
||||
instance
|
||||
..onOneFingerPanStart = onOneFingerPanStart
|
||||
..onOneFingerPanUpdate = onOneFingerPanUpdate
|
||||
..onOneFingerPanEnd = onOneFingerPanEnd
|
||||
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
|
||||
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
|
||||
..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate;
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
import '../../models/input_model.dart';
|
||||
import 'package:flutter_hbb/models/platform_model.dart';
|
||||
import 'package:flutter_hbb/common.dart';
|
||||
import 'package:flutter_hbb/models/model.dart';
|
||||
import 'package:flutter_hbb/models/input_model.dart';
|
||||
|
||||
import './gestures.dart';
|
||||
|
||||
class RawKeyFocusScope extends StatelessWidget {
|
||||
final FocusNode? focusNode;
|
||||
@@ -30,6 +36,290 @@ class RawKeyFocusScope extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class RawTouchGestureDetectorRegion extends StatefulWidget {
|
||||
final Widget child;
|
||||
final FFI ffi;
|
||||
|
||||
late final InputModel inputModel = ffi.inputModel;
|
||||
late final FfiModel ffiModel = ffi.ffiModel;
|
||||
|
||||
RawTouchGestureDetectorRegion({
|
||||
required this.child,
|
||||
required this.ffi,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RawTouchGestureDetectorRegion> createState() =>
|
||||
_RawTouchGestureDetectorRegionState();
|
||||
}
|
||||
|
||||
/// touchMode only:
|
||||
/// LongPress -> right click
|
||||
/// OneFingerPan -> start/end -> left down start/end
|
||||
/// onDoubleTapDown -> move to
|
||||
/// onLongPressDown => move to
|
||||
///
|
||||
/// mouseMode only:
|
||||
/// DoubleFiner -> right click
|
||||
/// HoldDrag -> left drag
|
||||
class _RawTouchGestureDetectorRegionState
|
||||
extends State<RawTouchGestureDetectorRegion> {
|
||||
Offset _cacheLongPressPosition = Offset(0, 0);
|
||||
double _mouseScrollIntegral = 0; // mouse scroll speed controller
|
||||
double _scale = 1;
|
||||
|
||||
PointerDeviceKind? lastDeviceKind;
|
||||
|
||||
FFI get ffi => widget.ffi;
|
||||
FfiModel get ffiModel => widget.ffiModel;
|
||||
InputModel get inputModel => widget.inputModel;
|
||||
bool get handleTouch => isDesktop || ffiModel.touchMode;
|
||||
SessionID get sessionId => ffi.sessionId;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RawGestureDetector(
|
||||
child: widget.child,
|
||||
gestures: makeGestures(context),
|
||||
);
|
||||
}
|
||||
|
||||
onTapDown(TapDownDetails d) {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
}
|
||||
inputModel.tapDown(MouseButtons.left);
|
||||
}
|
||||
|
||||
onTapUp(TapUpDetails d) {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
}
|
||||
inputModel.tapUp(MouseButtons.left);
|
||||
}
|
||||
|
||||
onDoubleTapDown(TapDownDetails d) {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
}
|
||||
}
|
||||
|
||||
onDoubleTap() {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
inputModel.tap(MouseButtons.left);
|
||||
inputModel.tap(MouseButtons.left);
|
||||
}
|
||||
|
||||
onLongPressDown(LongPressDownDetails d) {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
_cacheLongPressPosition = d.localPosition;
|
||||
}
|
||||
}
|
||||
|
||||
// for mobiles
|
||||
onLongPress() {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
ffi.cursorModel
|
||||
.move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
|
||||
}
|
||||
inputModel.tap(MouseButtons.right);
|
||||
}
|
||||
|
||||
onDoubleFinerTap(TapDownDetails d) {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
inputModel.tap(MouseButtons.right);
|
||||
}
|
||||
}
|
||||
|
||||
onHoldDragStart(DragStartDetails d) {
|
||||
lastDeviceKind = d.kind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
inputModel.sendMouse('down', MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
onHoldDragUpdate(DragUpdateDetails d) {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
|
||||
}
|
||||
}
|
||||
|
||||
onHoldDragEnd(DragEndDetails d) {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (!handleTouch) {
|
||||
inputModel.sendMouse('up', MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
onOneFingerPanStart(BuildContext context, DragStartDetails d) {
|
||||
lastDeviceKind = d.kind ?? lastDeviceKind;
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
inputModel.sendMouse('down', MouseButtons.left);
|
||||
} else {
|
||||
final offset = ffi.cursorModel.offset;
|
||||
final cursorX = offset.dx;
|
||||
final cursorY = offset.dy;
|
||||
final visible =
|
||||
ffi.cursorModel.getVisibleRect().inflate(1); // extend edges
|
||||
final size = MediaQueryData.fromView(View.of(context)).size;
|
||||
if (!visible.contains(Offset(cursorX, cursorY))) {
|
||||
ffi.cursorModel.move(size.width / 2, size.height / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOneFingerPanUpdate(DragUpdateDetails d) {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
|
||||
}
|
||||
|
||||
onOneFingerPanEnd(DragEndDetails d) {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (handleTouch) {
|
||||
inputModel.sendMouse('up', MouseButtons.left);
|
||||
}
|
||||
}
|
||||
|
||||
// scale + pan event
|
||||
onTwoFingerScaleUpdate(ScaleUpdateDetails d) {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop) {
|
||||
// to-do
|
||||
} else {
|
||||
// mobile
|
||||
// to-do: Is this correct?
|
||||
ffi.canvasModel.updateScale(d.scale / _scale);
|
||||
_scale = d.scale;
|
||||
ffi.canvasModel.panX(d.focalPointDelta.dx);
|
||||
ffi.canvasModel.panY(d.focalPointDelta.dy);
|
||||
}
|
||||
}
|
||||
|
||||
onTwoFingerScaleEnd(ScaleEndDetails d) {
|
||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||
return;
|
||||
}
|
||||
if (isDesktop) {
|
||||
// to-do
|
||||
} else {
|
||||
// mobile
|
||||
// to-do: Is this correct?
|
||||
_scale = 1;
|
||||
bind.sessionSetViewStyle(sessionId: sessionId, value: "");
|
||||
}
|
||||
}
|
||||
|
||||
get onHoldDragCancel => null;
|
||||
get onThreeFingerVerticalDragUpdate => ffi.ffiModel.isPeerAndroid
|
||||
? null
|
||||
: (d) {
|
||||
_mouseScrollIntegral += d.delta.dy / 4;
|
||||
if (_mouseScrollIntegral > 1) {
|
||||
inputModel.scroll(1);
|
||||
_mouseScrollIntegral = 0;
|
||||
} else if (_mouseScrollIntegral < -1) {
|
||||
inputModel.scroll(-1);
|
||||
_mouseScrollIntegral = 0;
|
||||
}
|
||||
};
|
||||
|
||||
makeGestures(BuildContext context) {
|
||||
return <Type, GestureRecognizerFactory>{
|
||||
// Official
|
||||
TapGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||
() => TapGestureRecognizer(), (instance) {
|
||||
instance
|
||||
..onTapDown = onTapDown
|
||||
..onTapUp = onTapUp;
|
||||
}),
|
||||
DoubleTapGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
|
||||
() => DoubleTapGestureRecognizer(), (instance) {
|
||||
instance
|
||||
..onDoubleTapDown = onDoubleTapDown
|
||||
..onDoubleTap = onDoubleTap;
|
||||
}),
|
||||
LongPressGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
|
||||
() => LongPressGestureRecognizer(), (instance) {
|
||||
instance
|
||||
..onLongPressDown = onLongPressDown
|
||||
..onLongPress = onLongPress;
|
||||
}),
|
||||
// Customized
|
||||
HoldTapMoveGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
|
||||
() => HoldTapMoveGestureRecognizer(),
|
||||
(instance) => instance
|
||||
..onHoldDragStart = onHoldDragStart
|
||||
..onHoldDragUpdate = onHoldDragUpdate
|
||||
..onHoldDragCancel = onHoldDragCancel
|
||||
..onHoldDragEnd = onHoldDragEnd),
|
||||
DoubleFinerTapGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>(
|
||||
() => DoubleFinerTapGestureRecognizer(), (instance) {
|
||||
instance.onDoubleFinerTap = onDoubleFinerTap;
|
||||
}),
|
||||
CustomTouchGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
|
||||
() => CustomTouchGestureRecognizer(), (instance) {
|
||||
instance.onOneFingerPanStart =
|
||||
(DragStartDetails d) => onOneFingerPanStart(context, d);
|
||||
instance
|
||||
..onOneFingerPanUpdate = onOneFingerPanUpdate
|
||||
..onOneFingerPanEnd = onOneFingerPanEnd
|
||||
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
|
||||
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
|
||||
..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate;
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class RawPointerMouseRegion extends StatelessWidget {
|
||||
final InputModel inputModel;
|
||||
final Widget child;
|
||||
@@ -39,36 +329,39 @@ class RawPointerMouseRegion extends StatelessWidget {
|
||||
final PointerDownEventListener? onPointerDown;
|
||||
final PointerUpEventListener? onPointerUp;
|
||||
|
||||
RawPointerMouseRegion(
|
||||
{this.onEnter,
|
||||
this.onExit,
|
||||
this.cursor,
|
||||
this.onPointerDown,
|
||||
this.onPointerUp,
|
||||
required this.inputModel,
|
||||
required this.child});
|
||||
RawPointerMouseRegion({
|
||||
this.onEnter,
|
||||
this.onExit,
|
||||
this.cursor,
|
||||
this.onPointerDown,
|
||||
this.onPointerUp,
|
||||
required this.inputModel,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Listener(
|
||||
onPointerHover: inputModel.onPointHoverImage,
|
||||
onPointerDown: (evt) {
|
||||
onPointerDown?.call(evt);
|
||||
inputModel.onPointDownImage(evt);
|
||||
},
|
||||
onPointerUp: (evt) {
|
||||
onPointerUp?.call(evt);
|
||||
inputModel.onPointUpImage(evt);
|
||||
},
|
||||
onPointerMove: inputModel.onPointMoveImage,
|
||||
onPointerSignal: inputModel.onPointerSignalImage,
|
||||
onPointerPanZoomStart: inputModel.onPointerPanZoomStart,
|
||||
onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate,
|
||||
onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd,
|
||||
child: MouseRegion(
|
||||
cursor: cursor ?? MouseCursor.defer,
|
||||
onEnter: onEnter,
|
||||
onExit: onExit,
|
||||
child: child));
|
||||
onPointerHover: inputModel.onPointHoverImage,
|
||||
onPointerDown: (evt) {
|
||||
onPointerDown?.call(evt);
|
||||
inputModel.onPointDownImage(evt);
|
||||
},
|
||||
onPointerUp: (evt) {
|
||||
onPointerUp?.call(evt);
|
||||
inputModel.onPointUpImage(evt);
|
||||
},
|
||||
onPointerMove: inputModel.onPointMoveImage,
|
||||
onPointerSignal: inputModel.onPointerSignalImage,
|
||||
onPointerPanZoomStart: inputModel.onPointerPanZoomStart,
|
||||
onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate,
|
||||
onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd,
|
||||
child: MouseRegion(
|
||||
cursor: cursor ?? MouseCursor.defer,
|
||||
onEnter: onEnter,
|
||||
onExit: onExit,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user