From d15937730cee14bd2a96907873d1d87abb418c16 Mon Sep 17 00:00:00 2001 From: THARUN Date: Sun, 3 May 2026 23:20:55 +0530 Subject: [PATCH] fix(android): add two-finger scrolling to avoid OEM gesture conflicts --- flutter/lib/common/widgets/gestures.dart | 127 +++++++++++-------- flutter/lib/common/widgets/remote_input.dart | 11 +- flutter/pubspec.lock | 116 +++++++++++------ 3 files changed, 160 insertions(+), 94 deletions(-) diff --git a/flutter/lib/common/widgets/gestures.dart b/flutter/lib/common/widgets/gestures.dart index 0501ca453..350a55363 100644 --- a/flutter/lib/common/widgets/gestures.dart +++ b/flutter/lib/common/widgets/gestures.dart @@ -7,6 +7,7 @@ enum GestureState { none, oneFingerPan, twoFingerScale, + twoFingerVerticalDrag, threeFingerVerticalDrag } @@ -32,7 +33,12 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer { GestureScaleUpdateCallback? onTwoFingerScaleUpdate; GestureScaleEndCallback? onTwoFingerScaleEnd; - // threeFingerVerticalDrag + // twoFingerVerticalDrag (scroll) — primary scroll gesture + GestureDragStartCallback? onTwoFingerVerticalDragStart; + GestureDragUpdateCallback? onTwoFingerVerticalDragUpdate; + GestureDragEndCallback? onTwoFingerVerticalDragEnd; + + // threeFingerVerticalDrag — kept for devices where it isn't intercepted by OS GestureDragStartCallback? onThreeFingerVerticalDragStart; GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate; GestureDragEndCallback? onThreeFingerVerticalDragEnd; @@ -42,79 +48,102 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer { 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 == 2) { + final dx = d.focalPointDelta.dx; + final dy = d.focalPointDelta.dy; + if (dy.abs() > dx.abs() * 1.5 && dy.abs() > 2) { + // Two-finger vertical drag → treat as scroll + if (_currentState != GestureState.twoFingerVerticalDrag) { + _currentState = GestureState.twoFingerVerticalDrag; + final startDetails = DragStartDetails(globalPosition: d.localFocalPoint); + onTwoFingerVerticalDragStart?.call(startDetails); + // Also fire the three-finger callback for backward compatibility + onThreeFingerVerticalDragStart?.call(startDetails); + debugPrint("start twoFingerVerticalDrag (scroll)"); + } + } else { + if (_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"); + final startDetails = DragStartDetails(globalPosition: d.localFocalPoint); + onThreeFingerVerticalDragStart?.call(startDetails); + debugPrint("start threeFingerVerticalDrag"); } + if (_currentState != GestureState.none) { switch (_currentState) { case GestureState.oneFingerPan: - if (onOneFingerPanUpdate != null) { - onOneFingerPanUpdate!(_getDragUpdateDetails(d)); - } + onOneFingerPanUpdate?.call(_getDragUpdateDetails(d)); break; + case GestureState.twoFingerScale: - if (onTwoFingerScaleUpdate != null) { - onTwoFingerScaleUpdate!(d); - } + onTwoFingerScaleUpdate?.call(d); break; + + case GestureState.twoFingerVerticalDrag: + final update = _getDragUpdateDetails(d); + onTwoFingerVerticalDragUpdate?.call(update); + // Fire three-finger callback too for backward compatibility + onThreeFingerVerticalDragUpdate?.call(update); + break; + case GestureState.threeFingerVerticalDrag: - if (onThreeFingerVerticalDragUpdate != null) { - onThreeFingerVerticalDragUpdate!(_getDragUpdateDetails(d)); - } + onThreeFingerVerticalDragUpdate?.call(_getDragUpdateDetails(d)); break; + default: break; } return; } }; + onEnd = (d) { debugPrint("ScaleGestureRecognizer onEnd"); _debounceTimer?.cancel(); - // end switch (_currentState) { case GestureState.oneFingerPan: debugPrint("OneFingerState.pan onEnd"); - if (onOneFingerPanEnd != null) { - onOneFingerPanEnd!(_getDragEndDetails(d)); - } + onOneFingerPanEnd?.call(_getDragEndDetails(d)); break; + case GestureState.twoFingerScale: debugPrint("TwoFingerState.scale onEnd"); - if (onTwoFingerScaleEnd != null) { - onTwoFingerScaleEnd!(d); - } + onTwoFingerScaleEnd?.call(d); if (isSpecialHoldDragActive) { - // If we are in special drag mode, we need to reset the state. - // Otherwise, the next `onTwoFingerScaleUpdate()` will handle a wrong `focalPoint`. _currentState = GestureState.none; return; } break; - case GestureState.threeFingerVerticalDrag: - debugPrint("ThreeFingerState.vertical onEnd"); - if (onThreeFingerVerticalDragEnd != null) { - onThreeFingerVerticalDragEnd!(_getDragEndDetails(d)); - } + + case GestureState.twoFingerVerticalDrag: + debugPrint("twoFingerVerticalDrag onEnd"); + final endDetails = _getDragEndDetails(d); + onTwoFingerVerticalDragEnd?.call(endDetails); + // Fire three-finger callback too for backward compatibility + onThreeFingerVerticalDragEnd?.call(endDetails); break; + + case GestureState.threeFingerVerticalDrag: + debugPrint("threeFingerVerticalDrag onEnd"); + onThreeFingerVerticalDragEnd?.call(_getDragEndDetails(d)); + break; + default: break; } - _debounceTimer = Timer(Duration(milliseconds: 200), () { + _debounceTimer = Timer(const Duration(milliseconds: 200), () { _currentState = GestureState.none; }); }; @@ -125,14 +154,12 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer { void onOneFingerStartDebounce(ScaleUpdateDetails d) { start(ScaleUpdateDetails d) { _currentState = GestureState.oneFingerPan; - if (onOneFingerPanStart != null) { - onOneFingerPanStart!(DragStartDetails( - localPosition: d.localFocalPoint, globalPosition: d.focalPoint)); - } + onOneFingerPanStart?.call(DragStartDetails( + localPosition: d.localFocalPoint, globalPosition: d.focalPoint)); } if (_currentState != GestureState.none) { - _debounceTimer = Timer(Duration(milliseconds: 200), () { + _debounceTimer = Timer(const Duration(milliseconds: 200), () { start(d); debugPrint("debounce start oneFingerPan"); }); @@ -145,14 +172,12 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer { void onTwoFingerStartDebounce(ScaleUpdateDetails d) { start(ScaleUpdateDetails d) { _currentState = GestureState.twoFingerScale; - if (onTwoFingerScaleStart != null) { - onTwoFingerScaleStart!(ScaleStartDetails( - localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint)); - } + onTwoFingerScaleStart?.call(ScaleStartDetails( + localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint)); } if (_currentState == GestureState.threeFingerVerticalDrag) { - _debounceTimer = Timer(Duration(milliseconds: 200), () { + _debounceTimer = Timer(const Duration(milliseconds: 200), () { start(d); debugPrint("debounce start twoFingerScale"); }); @@ -176,15 +201,12 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer { super.rejectGesture(pointer); switch (_currentState) { case GestureState.oneFingerPan: - if (onOneFingerPanCancel != null) { - onOneFingerPanCancel!(); - } + onOneFingerPanCancel?.call(); break; case GestureState.twoFingerScale: - // Reset scale state if needed, currently self-contained break; + case GestureState.twoFingerVerticalDrag: case GestureState.threeFingerVerticalDrag: - // Reset drag state if needed, currently self-contained break; default: break; @@ -192,7 +214,6 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer { _currentState = GestureState.none; } } - class HoldTapMoveGestureRecognizer extends GestureRecognizer { HoldTapMoveGestureRecognizer({ Object? debugOwner, @@ -742,6 +763,9 @@ RawGestureDetector getMixinGestureDetector({ GestureDragCancelCallback? onOneFingerPanCancel, GestureScaleUpdateCallback? onTwoFingerScaleUpdate, GestureScaleEndCallback? onTwoFingerScaleEnd, + GestureDragStartCallback? onTwoFingerVerticalDragStart, + GestureDragUpdateCallback? onTwoFingerVerticalDragUpdate, + GestureDragEndCallback? onTwoFingerVerticalDragEnd, GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate, }) { return RawGestureDetector( @@ -790,6 +814,9 @@ RawGestureDetector getMixinGestureDetector({ ..onOneFingerPanEnd = onOneFingerPanEnd ..onOneFingerPanCancel = onOneFingerPanCancel ..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate + ..onTwoFingerVerticalDragStart = onTwoFingerVerticalDragStart + ..onTwoFingerVerticalDragUpdate = onTwoFingerVerticalDragUpdate + ..onTwoFingerVerticalDragEnd = onTwoFingerVerticalDragEnd ..onTwoFingerScaleEnd = onTwoFingerScaleEnd ..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate; }), diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 9515ca759..3231669bb 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -514,7 +514,7 @@ class _RawTouchGestureDetectorRegionState } get onHoldDragCancel => null; - get onThreeFingerVerticalDragUpdate => ffi.ffiModel.isPeerAndroid + get onScrollVerticalDragUpdate => ffi.ffiModel.isPeerAndroid ? null : (d) { _mouseScrollIntegral += d.delta.dy / 4; @@ -527,6 +527,8 @@ class _RawTouchGestureDetectorRegionState } }; + get onThreeFingerVerticalDragUpdate => onScrollVerticalDragUpdate; + makeGestures(BuildContext context) { return { // Official @@ -594,7 +596,12 @@ class _RawTouchGestureDetectorRegionState ..onTwoFingerScaleStart = onTwoFingerScaleStart ..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate ..onTwoFingerScaleEnd = onTwoFingerScaleEnd - ..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate; + // Two-finger vertical drag: primary scroll for Android devices where + // the OS (e.g. MIUI on Xiaomi) intercepts three-finger gestures. + ..onTwoFingerVerticalDragUpdate = onScrollVerticalDragUpdate + // Three-finger vertical drag: kept for devices where the OS does not + // intercept three-finger gestures. + ..onThreeFingerVerticalDragUpdate = onScrollVerticalDragUpdate; }), }; } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index c6f8aa1c2..a2b99b00b 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,15 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "72.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.2" + version: "67.0.0" after_layout: dependency: transitive description: @@ -26,10 +21,10 @@ packages: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.4.1" animations: dependency: transitive description: @@ -202,10 +197,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -234,10 +229,10 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: @@ -250,10 +245,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" contextmenu: dependency: "direct main" description: @@ -298,10 +293,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.6" dash_chat_2: dependency: "direct main" description: @@ -417,6 +412,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" ffi: dependency: "direct main" description: @@ -644,6 +647,11 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -653,10 +661,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: a434911f643466d78462625df76fd9eb13e57348ff43fe1f77bbe909522c67a1 url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "2.5.2" freezed_annotation: dependency: "direct main" description: @@ -849,6 +857,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" lints: dependency: transitive description: @@ -865,38 +897,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - macros: - dependency: transitive - description: - name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" - url: "https://pub.dev" - source: hosted - version: "0.1.2-main.4" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: df0c643f44ad098eb37988027a8e2b2b5a031fd3977f06bbfd3a76637e8df739 url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.18.2" mime: dependency: transitive description: @@ -957,10 +981,10 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_parsing: dependency: transitive description: @@ -1205,7 +1229,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: @@ -1290,10 +1314,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.11" texture_rgba_renderer: dependency: "direct main" description: @@ -1473,13 +1497,13 @@ packages: source: hosted version: "1.1.16" vector_math: - dependency: transitive + dependency: "direct main" description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: "47a1b32ee755c3fcffa33db52a7258c137f97bdb2209a1075be847809fac4ccf" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.3.0" video_player: dependency: transitive description: @@ -1528,6 +1552,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.0+2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" wakelock_plus: dependency: "direct main" description: @@ -1659,5 +1691,5 @@ packages: source: hosted version: "0.2.4" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.10.0-0 <4.0.0" flutter: ">=3.24.0"