Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix TextField horizontal drag conflicts #147341

Merged
merged 13 commits into from May 1, 2024

Conversation

Renzo-Olivares
Copy link
Contributor

@Renzo-Olivares Renzo-Olivares commented Apr 24, 2024

Currently on iOS TextField horizontal drag gestures will have precedence over parent horizontal drag gestures when competing with each other because the default touchSlop value used by TextField to calculate whether the threshold for a drag has been met is the same as the one used by parent horizontal drag gestures like PageView, and other Scrollables. The default value is 18.0, and because the TextField receives the PointerEvent first in this scenario, it always declares victory before any parent horizontal drag gestures has the chance to.

Native iOS behavior: The parent horizontal drag gestures will always win unless the drag originated on the cursor (collapsed selection), in that case the TextField cursor drag gestures will win.

This change:

  • Introduces BaseTapAndDragGestureRecognizer.eagerVictoryOnDrag which can be used to configure the recognizer to declare victory immediately when it detects a drag, or when disabled it will wait until it is the last recognizer in the arena before declaring victory. The default behavior is that it declares victory immediately when the drag is detected.
  • Eliminates the iOS cursor drag logic from TextSelectionGestureDetector, this logic is now delegated to the selection handle overlay which already had this logic in place.
  • Enables iOS cursor to always beat other drag gestures by setting the touch slop lower.
  • Disables eagerVictoryOnDrag for iOS to allow for parent drag gestures to win and match native behavior.

Fixes #124421, Fixes #130198, Fixes #142624, Fixes #142447, Fixes #127017

Pre-launch Checklist

  • I read the [Contributor Guide] and followed the process outlined there for submitting PRs.
  • I read the [Tree Hygiene] wiki page, which explains my responsibilities.
  • I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement].
  • I signed the [CLA].
  • I listed at least one issue that this PR fixes in the description above.
  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is [test-exempt].
  • I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
  • All existing and new tests are passing.

@github-actions github-actions bot added a: text input Entering text in a text field or keyboard related problems framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. f: cupertino flutter/packages/flutter/cupertino repository f: gestures flutter/packages/flutter/gestures repository. labels Apr 24, 2024
@Renzo-Olivares Renzo-Olivares changed the title [WIP] Fix TextField Drag Conflicts [WIP] Fix TextField drag conflicts Apr 24, 2024
@Renzo-Olivares Renzo-Olivares force-pushed the delay-drag branch 2 times, most recently from 08935d9 to d5bd704 Compare April 25, 2024 07:18
Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's my understanding, tell me if anything is wrong:

For the case where a TextField is inside of a horizontal scrollable on iOS, dragging the selection handle should move the selection handle and not scroll. On other platforms, it should scroll horizontally.

The current behavior is that it always scrolls horizontally. The reason is that the touchSlop on the scrollable is less than the touchSlop on the selection handle. When the drag exceeds the lower touchSlop, that recognizer immediately wins.


What would happen if the touchSlops were identical? By having different touchSlops, are we sidestepping the part of the gesture arena where two competing recognizers decide who receives the gesture?

Are the different touchSlops intentional, like we know that native recognizes these drags after different distances?

I want to understand whether this is a canonical Flutter solution to competing gesture detectors, or if not, make sure that this is a compromise that we're willing to make. If so I'm totally on board, this looks like a pretty straightforward change.

if (_start != null && !eagerVictoryOnDrag) {
assert(_dragState == _DragState.possible);
assert(currentUp == null);
_dragState = _DragState.accepted;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not set in the previous if too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because in the case of eagerVictoryOnDrag == true, it is set immediately when the drag is detected.

@@ -1924,6 +1929,7 @@ class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay> with S
(PanGestureRecognizer instance) {
instance
..dragStartBehavior = widget.dragStartBehavior
..gestureSettings = eagerlyAcceptDragWhenCollapsed ? const DeviceGestureSettings(touchSlop: 1.0) : null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 1.0? Could it be competing-slop-value minus one, or is 1.0 better? (I know you said you're still thinking about cleaning this up.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of competing-slop-value minus one, or maybe minus something higher than 1 to be sure that it always wins.

@@ -2839,16 +2823,6 @@ class TextSelectionGestureDetectorBuilder {
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
// For iOS platforms, a touch drag does not initiate unless the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this removed logic covered now? I guess the drag listener on the selection handle?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the drag listener on the selection handle already has this logic by default.

@Renzo-Olivares
Copy link
Contributor Author

For the case where a TextField is inside of a horizontal scrollable on iOS, dragging the selection handle should move the selection handle and not scroll. On other platforms, it should scroll horizontally.

The current behavior is that it always scrolls horizontally. The reason is that the touchSlop on the scrollable is less than the touchSlop on the selection handle. When the drag exceeds the lower touchSlop, that recognizer immediately wins.

On iOS the current behavior is that the TextField will always win horizontal drags. This is because the touchSlop of a Scrollable defaults to 18.0, and the touchSlop that the TextField is using is also 18.0. Because they are identical the first recognizer to receive the drag will win, and that will be the TextField. The desired behavior here is that the parent Scrollable will always win drag gestures, unless the gesture began on the cursor.

On Android the current behavior is that the Scrollable always wins horizontal drags. This is because the touchSlop of a Scrollable on Android provided by the native platform seems to be around 8.0, and the touchSlop on the TextField is 18.0. Because the Scrollable has a lower touch slop it will detect the drag first even if the drag is on the TextField. This is the expected behavior on Android, but if anyone changes their touchSlops i.e. making the Scrollables touchSlop higher or identical to the touchSlop of the TextField (18.0) then they will also experience the unexpected behavior that is being observed on iOS.

The changes in this PR only affect iOS at the moment.

What would happen if the touchSlops were identical? By having different touchSlops, are we sidestepping the part of the gesture arena where two competing recognizers decide who receives the gesture?

Are the different touchSlops intentional, like we know that native recognizes these drags after different distances?

If touchSlops are identical then the recognizer that first receives the PointerEvent will declare victory first. I'm not too sure if different touchSlops are intentional, Android provides getScaledTouchSlop which we utilize to set the default on Android https://developer.android.com/reference/android/view/ViewConfiguration#getScaledTouchSlop() , which is why Android devices aren't experiencing this issue. iOS does not provide such a constant besides some related documentation in the UILongPressGestureRecognizer https://developer.apple.com/documentation/uikit/uilongpressgesturerecognizer/1616427-allowablemovement?language=objc (states 10.0). I think changing the touchSlop is sidestepping the arena, but I think it is reasonable in the scenario that it is being used in this PR (to allow the cursor to always win). An alternative would be to add some type of flag to PanGestureRecognizer so that it can achieve this behavior, but i'm leaning towards overriding the touchSlop versus expanding the API surface.

I want to understand whether this is a canonical Flutter solution to competing gesture detectors, or if not, make sure that this is a compromise that we're willing to make. If so I'm totally on board, this looks like a pretty straightforward change.

Delaying the drag acceptance until all other recognizers have lost is similar to what we do with TapGestureRecognizer so that other recognizers can beat it.

Copy link
Contributor

@justinmc justinmc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with nits 👍

Thanks for talking this over with me yesterday and for the clear explanation of the changes in the PR description, it saved me some time in understanding everything.

tester.route(pointer.up());
expect(events, <String>[
'panstart',
'panend']);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super nit: I would typically do

  'panend',
]);

expect(dismissed, true);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Consider also testing the behavior for this on the other platforms (cursor is not dragged).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add these tests in a follow up PR that changes the Android/Fuchsia behavior.

@Renzo-Olivares Renzo-Olivares added the autosubmit Merge PR when tree becomes green via auto submit App label May 1, 2024
@auto-submit auto-submit bot merged commit 5181086 into flutter:master May 1, 2024
72 checks passed
auto-submit bot pushed a commit that referenced this pull request May 2, 2024
engine-flutter-autoroll added a commit to engine-flutter-autoroll/packages that referenced this pull request May 2, 2024
auto-submit bot pushed a commit to flutter/packages that referenced this pull request May 2, 2024
Roll Flutter from d33bb8fa5eb8 to bf7191fd3884 (34 revisions)

flutter/flutter@d33bb8f...bf7191f

2024-05-02 engine-flutter-autoroll@skia.org Roll Flutter Engine from b989d239e281 to 1fb36ac9d718 (2 revisions) (flutter/flutter#147713)
2024-05-02 engine-flutter-autoroll@skia.org Roll Flutter Engine from 3c9c2ce8369e to b989d239e281 (1 revision) (flutter/flutter#147711)
2024-05-02 engine-flutter-autoroll@skia.org Roll Flutter Engine from 58b031c096ea to 3c9c2ce8369e (2 revisions) (flutter/flutter#147703)
2024-05-02 engine-flutter-autoroll@skia.org Roll Flutter Engine from fc28057dbd4d to 58b031c096ea (1 revision) (flutter/flutter#147701)
2024-05-02 engine-flutter-autoroll@skia.org Roll Flutter Engine from bfc6787eedc3 to fc28057dbd4d (1 revision) (flutter/flutter#147700)
2024-05-02 engine-flutter-autoroll@skia.org Roll Flutter Engine from 7cbef71f4f54 to bfc6787eedc3 (1 revision) (flutter/flutter#147699)
2024-05-02 engine-flutter-autoroll@skia.org Roll Flutter Engine from 90ce9e5959fc to 7cbef71f4f54 (1 revision) (flutter/flutter#147696)
2024-05-02 engine-flutter-autoroll@skia.org Roll Flutter Engine from 78dced50c467 to 90ce9e5959fc (1 revision) (flutter/flutter#147695)
2024-05-02 andrewrkolos@gmail.com add verbose logging to select hot reload/hot restart tests (flutter/flutter#147673)
2024-05-02 engine-flutter-autoroll@skia.org Roll Flutter Engine from 3087ec1adddd to 78dced50c467 (3 revisions) (flutter/flutter#147693)
2024-05-02 98614782+auto-submit[bot]@users.noreply.github.com Reverts "Implement computeDryBaseline for `RenderWrap` (#146260)" (flutter/flutter#147692)
2024-05-02 engine-flutter-autoroll@skia.org Roll Flutter Engine from f56c20c6ac67 to 3087ec1adddd (2 revisions) (flutter/flutter#147688)
2024-05-02 gspencergoog@users.noreply.github.com Allow explicit exclusion of packages from pinned packages in `flutter update-packages --force-update` (flutter/flutter#147679)
2024-05-02 31859944+LongCatIsLooong@users.noreply.github.com Implement getDryBaseline for Stack and Overlay (flutter/flutter#146253)
2024-05-02 engine-flutter-autoroll@skia.org Roll Flutter Engine from 2d73fa207927 to f56c20c6ac67 (2 revisions) (flutter/flutter#147681)
2024-05-02 polinach@google.com Update selectable_text_test.dart (flutter/flutter#147677)
2024-05-01 engine-flutter-autoroll@skia.org Roll Flutter Engine from c536a14052e5 to 2d73fa207927 (2 revisions) (flutter/flutter#147678)
2024-05-01 31859944+LongCatIsLooong@users.noreply.github.com Implement computeDryBaseline for `RenderWrap` (flutter/flutter#146260)
2024-05-01 engine-flutter-autoroll@skia.org Roll Flutter Engine from 842cf254ec58 to c536a14052e5 (1 revision) (flutter/flutter#147675)
2024-05-01 49699333+dependabot[bot]@users.noreply.github.com Bump codecov/codecov-action from 4.3.0 to 4.3.1 (flutter/flutter#147674)
2024-05-01 ian@hixie.ch Remove obsolete performance analysis tools. (flutter/flutter#147663)
2024-05-01 engine-flutter-autoroll@skia.org Roll Flutter Engine from 5129b4919434 to 842cf254ec58 (3 revisions) (flutter/flutter#147670)
2024-05-01 120297255+PurplePolyhedron@users.noreply.github.com fix DropdownMenu overflow (flutter/flutter#147233)
2024-05-01 yjbanov@google.com [web] remove platform_messages_integration test (flutter/flutter#147654)
2024-05-01 rmolivares@renzo-olivares.dev Fix `TextField` horizontal drag conflicts (flutter/flutter#147341)
2024-05-01 nate.w5687@gmail.com `flutter/lib/src/`: refactoring if-chains into switch expressions (flutter/flutter#147472)
2024-05-01 98614782+auto-submit[bot]@users.noreply.github.com Reverts "Draggable feedback positioning (#145647)" (flutter/flutter#147658)
2024-05-01 engine-flutter-autoroll@skia.org Roll Flutter Engine from 0014e0353aa9 to 5129b4919434 (1 revision) (flutter/flutter#147655)
2024-05-01 yjbanov@google.com add lang attribute to the a11y_assessments app (flutter/flutter#147586)
2024-05-01 hi@timcreated.it Draggable feedback positioning (flutter/flutter#145647)
2024-05-01 engine-flutter-autoroll@skia.org Roll Flutter Engine from 0ce67714ce4c to 0014e0353aa9 (13 revisions) (flutter/flutter#147649)
2024-05-01 sokolovskyi.konstantin@gmail.com Add test for animated_fractionally_sized_box.0.dart API example. (flutter/flutter#146721)
2024-05-01 engine-flutter-autoroll@skia.org Roll Packages from cc47b06 to aea93d2 (5 revisions) (flutter/flutter#147647)
2024-05-01 73608287+ellet0@users.noreply.github.com Update reorderable_list.dart to use Dart 3 return switch statement for consistency (flutter/flutter#147505)

If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-packages
Please CC dit@google.com,rmistry@google.com,stuartmorgan@google.com on the revert to ensure that a human
is aware of the problem.

To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose

To report a problem with the AutoRoller itself, please file a bug:
https://issues.skia.org/issues/new?component=1389291&template=1850622

...
@polina-c polina-c mentioned this pull request May 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
a: text input Entering text in a text field or keyboard related problems autosubmit Merge PR when tree becomes green via auto submit App f: cupertino flutter/packages/flutter/cupertino repository f: gestures flutter/packages/flutter/gestures repository. f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.
Projects
None yet
2 participants