fix(iPad): Magic Mouse, click (#14188)

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2026-01-29 15:25:44 +08:00
committed by GitHub
parent 79ef4c4501
commit 1e6bfa7bb1
2 changed files with 45 additions and 1 deletions

View File

@@ -107,6 +107,8 @@ class _RawTouchGestureDetectorRegionState
// For mouse mode, we need to block the events when the cursor is in a blocked area.
// So we need to cache the last tap down position.
Offset? _lastTapDownPositionForMouseMode;
// Cache global position for onTap (which lacks position info).
Offset? _lastTapDownGlobalPosition;
FFI get ffi => widget.ffi;
FfiModel get ffiModel => widget.ffiModel;
@@ -136,6 +138,7 @@ class _RawTouchGestureDetectorRegionState
onTapDown(TapDownDetails d) async {
lastDeviceKind = d.kind;
_lastTapDownGlobalPosition = d.globalPosition;
if (isNotTouchBasedDevice()) {
return;
}
@@ -154,6 +157,10 @@ class _RawTouchGestureDetectorRegionState
if (isNotTouchBasedDevice()) {
return;
}
// Filter duplicate touch tap events on iOS (Magic Mouse issue).
if (inputModel.shouldIgnoreTouchTap(d.globalPosition)) {
return;
}
if (handleTouch) {
final isMoved =
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
@@ -171,6 +178,11 @@ class _RawTouchGestureDetectorRegionState
if (isNotTouchBasedDevice()) {
return;
}
// Filter duplicate touch tap events on iOS (Magic Mouse issue).
final lastPos = _lastTapDownGlobalPosition;
if (lastPos != null && inputModel.shouldIgnoreTouchTap(lastPos)) {
return;
}
if (!handleTouch) {
// Cannot use `_lastTapDownDetails` because Flutter calls `onTapUp` before `onTap`, clearing the cached details.
// Using `_lastTapDownPositionForMouseMode` instead.

View File

@@ -826,6 +826,9 @@ class InputModel {
Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
final Map<String, dynamic> out = {};
bool hasStaleButtonsOnMouseUp =
type == _kMouseEventUp && evt.buttons == _lastButtons;
// Check update event type and set buttons to be sent.
int buttons = _lastButtons;
if (type == _kMouseEventMove) {
@@ -850,7 +853,7 @@ class InputModel {
buttons = evt.buttons;
}
}
_lastButtons = evt.buttons;
_lastButtons = hasStaleButtonsOnMouseUp ? 0 : evt.buttons;
out['buttons'] = buttons;
out['type'] = type;
@@ -1218,6 +1221,28 @@ class InputModel {
_trackpadLastDelta = Offset.zero;
}
// iOS Magic Mouse duplicate event detection.
// When using Magic Mouse on iPad, iOS may emit both mouse and touch events
// for the same click in certain areas (like top-left corner).
int _lastMouseDownTimeMs = 0;
ui.Offset _lastMouseDownPos = ui.Offset.zero;
/// Check if a touch tap event should be ignored because it's a duplicate
/// of a recent mouse event (iOS Magic Mouse issue).
bool shouldIgnoreTouchTap(ui.Offset pos) {
if (!isIOS) return false;
final nowMs = DateTime.now().millisecondsSinceEpoch;
final dt = nowMs - _lastMouseDownTimeMs;
final distance = (_lastMouseDownPos - pos).distance;
// If touch tap is within 2000ms and 80px of the last mouse down,
// it's likely a duplicate event from the same Magic Mouse click.
if (dt >= 0 && dt < 2000 && distance < 80.0) {
debugPrint("shouldIgnoreTouchTap: IGNORED (dt=$dt, dist=$distance)");
return true;
}
return false;
}
void onPointDownImage(PointerDownEvent e) {
debugPrint("onPointDownImage ${e.kind}");
_stopFling = true;
@@ -1227,6 +1252,13 @@ class InputModel {
if (isViewOnly && !showMyCursor) return;
if (isViewCamera) return;
// Track mouse down events for duplicate detection on iOS.
final nowMs = DateTime.now().millisecondsSinceEpoch;
if (e.kind == ui.PointerDeviceKind.mouse) {
_lastMouseDownTimeMs = nowMs;
_lastMouseDownPos = e.position;
}
if (_relativeMouse.enabled.value) {
_relativeMouse.updatePointerRegionTopLeftGlobal(e);
}