Fix TextField horizontal drag conflicts (#147341)

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 `Scrollable`s. 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
This commit is contained in:
Renzo Olivares 2024-05-01 13:51:54 -07:00 committed by GitHub
parent 586e1fce1b
commit 5181086fac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 343 additions and 84 deletions

View File

@ -143,9 +143,10 @@ class CupertinoTextSelectionControls extends TextSelectionControls {
..translate(-desiredSize.width / 2, -desiredSize.height / 2),
child: handle,
);
// iOS doesn't draw anything for collapsed selections.
// iOS should draw an invisible box so the handle can still receive gestures
// on collapsed selections.
case TextSelectionHandleType.collapsed:
return const SizedBox.shrink();
return SizedBox.fromSize(size: getHandleSize(textLineHeight));
}
}

View File

@ -652,9 +652,12 @@ mixin _TapStatusTrackerMixin on OneSequenceGestureRecognizer {
/// ### When competing with `TapGestureRecognizer` and `DragGestureRecognizer`
///
/// Similar to [TapGestureRecognizer] and [DragGestureRecognizer],
/// [BaseTapAndDragGestureRecognizer] will not aggressively declare victory when it detects
/// a tap, so when it is competing with those gesture recognizers and others it has a chance
/// of losing.
/// [BaseTapAndDragGestureRecognizer] will not aggressively declare victory when
/// it detects a tap, so when it is competing with those gesture recognizers and
/// others it has a chance of losing. Similarly, when `eagerVictoryOnDrag` is set
/// to `false`, this recognizer will not aggressively declare victory when it
/// detects a drag. By default, `eagerVictoryOnDrag` is set to `true`, so this
/// recognizer will aggressively declare victory when it detects a drag.
///
/// When competing against [TapGestureRecognizer], if the pointer does not move past the tap
/// tolerance, then the recognizer that entered the arena first will win. In this case the
@ -748,6 +751,7 @@ sealed class BaseTapAndDragGestureRecognizer extends OneSequenceGestureRecognize
super.debugOwner,
super.supportedDevices,
super.allowedButtonsFilter,
this.eagerVictoryOnDrag = true,
}) : _deadline = kPressTimeout,
dragStartBehavior = DragStartBehavior.start;
@ -782,6 +786,15 @@ sealed class BaseTapAndDragGestureRecognizer extends OneSequenceGestureRecognize
@override
int? maxConsecutiveTap;
/// Whether this recognizer eagerly declares victory when it has detected
/// a drag.
///
/// When this value is `false`, this recognizer will wait until it is the last
/// recognizer in the gesture arena before declaring victory on a drag.
///
/// Defaults to `true`.
bool eagerVictoryOnDrag;
/// {@macro flutter.gestures.tap.TapGestureRecognizer.onTapDown}
///
/// This triggers after the down event, once a short timeout ([kPressTimeout]) has
@ -984,14 +997,24 @@ sealed class BaseTapAndDragGestureRecognizer extends OneSequenceGestureRecognize
_wonArenaForPrimaryPointer = true;
// resolve(GestureDisposition.accepted) will be called when the [PointerMoveEvent] has
// moved a sufficient global distance.
if (_start != null) {
// resolve(GestureDisposition.accepted) will be called when the [PointerMoveEvent]
// has moved a sufficient global distance to be considered a drag and
// `eagerVictoryOnDrag` is set to `true`.
if (_start != null && eagerVictoryOnDrag) {
assert(_dragState == _DragState.accepted);
assert(currentUp == null);
_acceptDrag(_start!);
}
// This recognizer will wait until it is the last one in the gesture arena
// before accepting a drag when `eagerVictoryOnDrag` is set to `false`.
if (_start != null && !eagerVictoryOnDrag) {
assert(_dragState == _DragState.possible);
assert(currentUp == null);
_dragState = _DragState.accepted;
_acceptDrag(_start!);
}
if (currentUp != null) {
_checkTapUp(currentUp!);
}
@ -1076,7 +1099,8 @@ sealed class BaseTapAndDragGestureRecognizer extends OneSequenceGestureRecognize
// This can occur when the recognizer is accepted before a [PointerMoveEvent] has been
// received that moves the pointer a sufficient global distance to be considered a drag.
if (_start != null) {
if (_start != null && _wonArenaForPrimaryPointer) {
_dragState = _DragState.accepted;
_acceptDrag(_start!);
}
}
@ -1156,9 +1180,11 @@ sealed class BaseTapAndDragGestureRecognizer extends OneSequenceGestureRecognize
if (_hasSufficientGlobalDistanceToAccept(event.kind)
|| (_wonArenaForPrimaryPointer && _globalDistanceMovedAllAxes.abs() > computePanSlop(event.kind, gestureSettings))) {
_start = event;
_dragState = _DragState.accepted;
if (!_wonArenaForPrimaryPointer) {
resolve(GestureDisposition.accepted);
if (eagerVictoryOnDrag) {
_dragState = _DragState.accepted;
if (!_wonArenaForPrimaryPointer) {
resolve(GestureDisposition.accepted);
}
}
}
}

View File

@ -1898,6 +1898,11 @@ class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay> with S
math.max((interactiveRect.height - handleRect.height) / 2, 0),
);
// Make sure a drag is eagerly accepted. This is used on iOS to match the
// behavior where a drag directly on a collapse handle will always win against
// other drag gestures.
final bool eagerlyAcceptDragWhenCollapsed = widget.type == TextSelectionHandleType.collapsed && defaultTargetPlatform == TargetPlatform.iOS;
return CompositedTransformFollower(
link: widget.handleLayerLink,
offset: interactiveRect.topLeft,
@ -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
..onStart = widget.onSelectionHandleDragStart
..onUpdate = widget.onSelectionHandleDragUpdate
..onEnd = widget.onSelectionHandleDragEnd;
@ -2081,18 +2087,6 @@ class TextSelectionGestureDetectorBuilder {
&& selection.end >= textPosition.offset;
}
/// Returns true if position was on selection.
bool _positionOnSelection(Offset position, TextSelection? targetSelection) {
if (targetSelection == null) {
return false;
}
final TextPosition textPosition = renderEditable.getPositionForPoint(position);
return targetSelection.start <= textPosition.offset
&& targetSelection.end >= textPosition.offset;
}
// Expand the selection to the given global position.
//
// Either base or extent will be moved to the last tapped position, whichever
@ -2203,15 +2197,6 @@ class TextSelectionGestureDetectorBuilder {
// inversion of the base and offset happens.
TextSelection? _dragStartSelection;
// For tap + drag gesture on iOS, whether the position where the drag started
// was on the previous TextSelection. iOS uses this value to determine if
// the cursor should move on drag update.
//
// If the drag started on the previous selection then the cursor will move on
// drag update. If the drag did not start on the previous selection then the
// cursor will not move on drag update.
bool? _dragBeganOnPreviousSelection;
// For iOS long press behavior when the field is not focused. iOS uses this value
// to determine if a long press began on a field that was not focused.
//
@ -2807,7 +2792,6 @@ class TextSelectionGestureDetectorBuilder {
_dragStartSelection = renderEditable.selection;
_dragStartScrollOffset = _scrollPosition;
_dragStartViewportOffset = renderEditable.offset.pixels;
_dragBeganOnPreviousSelection = _positionOnSelection(details.globalPosition, _dragStartSelection);
if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount(details.consecutiveTapCount) > 1) {
// Do not set the selection on a consecutive tap and drag.
@ -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
// editable has focus and the drag began on the previous selection.
assert(_dragBeganOnPreviousSelection != null);
if (renderEditable.hasFocus && _dragBeganOnPreviousSelection!) {
renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
_showMagnifierIfSupportedByPlatform(details.globalPosition);
}
case null:
}
case TargetPlatform.android:
@ -2975,12 +2949,10 @@ class TextSelectionGestureDetectorBuilder {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
// With a touch device, nothing should happen, unless there was a double tap, or
// there was a collapsed selection, and the tap/drag position is at the collapsed selection.
// In that case the caret should move with the drag position.
//
// With a mouse device, a drag should select the range from the origin of the drag
// to the current position of the drag.
//
// With a touch device, nothing should happen.
switch (details.kind) {
case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
@ -2993,17 +2965,6 @@ class TextSelectionGestureDetectorBuilder {
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
assert(_dragBeganOnPreviousSelection != null);
if (renderEditable.hasFocus
&& _dragStartSelection!.isCollapsed
&& _dragBeganOnPreviousSelection!
) {
renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
return _showMagnifierIfSupportedByPlatform(details.globalPosition);
}
case null:
break;
}
@ -3100,8 +3061,6 @@ class TextSelectionGestureDetectorBuilder {
/// callback.
@protected
void onDragSelectionEnd(TapDragEndDetails details) {
_dragBeganOnPreviousSelection = null;
if (_shouldShowSelectionToolbar && _TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) {
editableText.showToolbar();
}
@ -3437,6 +3396,7 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
// Text selection should start from the position of the first pointer
// down event.
..dragStartBehavior = DragStartBehavior.down
..eagerVictoryOnDrag = defaultTargetPlatform != TargetPlatform.iOS
..onTapTrackStart = _handleTapTrackStart
..onTapTrackReset = _handleTapTrackReset
..onTapDown = _handleTapDown

View File

@ -5923,17 +5923,14 @@ void main() {
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and attempt move
// to '|g'. The cursor will not move because the `VerticalDragGestureRecognizer`
// in the scrollable will beat the `TapAndHorizontalDragGestureRecognizer`
// in the TextField. This is because moving from `|a` to `|g` is a completely
// vertical movement.
// to '|g'.
await gesture.down(aPos);
await tester.pump();
await gesture.moveTo(gPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
expect(controller.selection.baseOffset, testValue.indexOf('g'));
// Release the pointer.
await gesture.up();
@ -5941,11 +5938,8 @@ void main() {
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and move to '|i'.
// Unlike our previous attempt to drag to `|g`, this works because moving
// to `|i` includes a horizontal movement so the `TapAndHorizontalDragGestureRecognizer`
// in TextField can beat the `VerticalDragGestureRecognizer` in the scrollable.
await gesture.down(aPos);
// Here we tap on '|g', where our selection was previously, and move to '|i'.
await gesture.down(gPos);
await tester.pump();
await gesture.moveTo(iPos);
await tester.pumpAndSettle();

View File

@ -17,9 +17,12 @@ void main() {
late List<String> events;
late BaseTapAndDragGestureRecognizer tapAndDrag;
void setUpTapAndPanGestureRecognizer() {
void setUpTapAndPanGestureRecognizer({
bool eagerVictoryOnDrag = true, // This is the default for [BaseTapAndDragGestureRecognizer].
}) {
tapAndDrag = TapAndPanGestureRecognizer()
..dragStartBehavior = DragStartBehavior.down
..eagerVictoryOnDrag = eagerVictoryOnDrag
..maxConsecutiveTap = 3
..onTapDown = (TapDragDownDetails details) {
events.add('down#${details.consecutiveTapCount}');
@ -653,6 +656,41 @@ void main() {
'panend#1']);
});
testGesture('Recognizer loses when competing against a DragGestureRecognizer for a drag when eagerVictoryOnDrag is disabled', (GestureTester tester) {
setUpTapAndPanGestureRecognizer(eagerVictoryOnDrag: false);
final PanGestureRecognizer pans = PanGestureRecognizer()
..onStart = (DragStartDetails details) {
events.add('panstart');
}
..onUpdate = (DragUpdateDetails details) {
events.add('panupdate');
}
..onEnd = (DragEndDetails details) {
events.add('panend');
}
..onCancel = () {
events.add('pancancel');
};
final TestPointer pointer = TestPointer(5);
final PointerDownEvent downB = pointer.down(const Offset(10.0, 10.0));
// When competing against another [DragGestureRecognizer], the [TapAndPanGestureRecognizer]
// will only win when it is the last recognizer in the arena.
tapAndDrag.addPointer(downB);
pans.addPointer(downB);
tester.closeArena(5);
tester.route(downB);
tester.route(pointer.move(const Offset(40.0, 45.0)));
tester.route(pointer.up());
expect(
events,
<String>[
'panstart',
'panend',
],
);
});
testGesture('Beats LongPressGestureRecognizer on a consecutive tap greater than one', (GestureTester tester) {
setUpTapAndPanGestureRecognizer();

View File

@ -2506,6 +2506,191 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - PageView', (WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/142624.
final TextEditingController controller = _textEditingController();
final PageController pageController = PageController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: PageView(
controller: pageController,
children: <Widget>[
Center(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
const SizedBox(
height: 200.0,
child: Center(
child: Text('Page 2'),
),
),
],
),
),
),
);
const String testValue = 'abc def ghi jkl mno pqr stu vwx yz';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
// Tap on text field to gain focus, and set selection to '|a'. On iOS
// the selection is set to the word edge closest to the tap position.
// We await for kDoubleTapTimeout after the up event, so our next down event
// does not register as a double tap.
final TestGesture gesture = await tester.startGesture(aPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and attempt move
// to '|g'.
await gesture.down(aPos);
await tester.pump();
await gesture.moveTo(gPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('g'));
// Release the pointer.
await gesture.up();
await tester.pumpAndSettle();
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|g', where our selection was previously, and move to '|i'.
await gesture.down(gPos);
await tester.pump();
await gesture.moveTo(iPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('i'));
// End gesture and skip the magnifier hide animation, so it can release
// resources.
await gesture.up();
await tester.pumpAndSettle();
expect(pageController.page, isNotNull);
expect(pageController.page, 0.0);
// A horizontal drag directly on the TextField, but not on the current
// collapsed selection should move the page view to the next page.
final Rect textFieldRect = tester.getRect(find.byType(TextField));
await tester.dragFrom(textFieldRect.centerRight - const Offset(0.1, 0.0), const Offset(-500.0, 0.0));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, testValue.indexOf('i'));
expect(pageController.page, isNotNull);
expect(pageController.page, 1.0);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - TextField in Dismissible', (WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/124421.
final TextEditingController controller = _textEditingController();
bool dismissed = false;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: ListView(
children: <Widget>[
Dismissible(
key: UniqueKey(),
onDismissed: (DismissDirection? direction) {
dismissed = true;
},
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
],
),
),
),
);
const String testValue = 'abc def ghi jkl mno pqr stu vwx yz';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
// Tap on text field to gain focus, and set selection to '|a'. On iOS
// the selection is set to the word edge closest to the tap position.
// We await for kDoubleTapTimeout after the up event, so our next down event
// does not register as a double tap.
final TestGesture gesture = await tester.startGesture(aPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and attempt move
// to '|g'.
await gesture.down(aPos);
await tester.pump();
await gesture.moveTo(gPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('g'));
// Release the pointer.
await gesture.up();
await tester.pumpAndSettle();
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|g', where our selection was previously, and move to '|i'.
await gesture.down(gPos);
await tester.pump();
await gesture.moveTo(iPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, testValue.indexOf('i'));
// End gesture and skip the magnifier hide animation, so it can release
// resources.
await gesture.up();
await tester.pumpAndSettle();
expect(dismissed, false);
// A horizontal drag directly on the TextField, but not on the current
// collapsed selection should allow for the Dismissible to be dismissed.
await tester.dragFrom(tester.getRect(find.byType(TextField)).centerRight - const Offset(0.1, 0.0), const Offset(-400.0, 0.0));
await tester.pumpAndSettle();
expect(dismissed, true);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - ListView', (WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/122519
@ -2550,17 +2735,14 @@ void main() {
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and attempt move
// to '|g'. The cursor will not move because the `VerticalDragGestureRecognizer`
// in the scrollable will beat the `TapAndHorizontalDragGestureRecognizer`
// in the TextField. This is because moving from `|a` to `|g` is a completely
// vertical movement.
// to '|g'.
await gesture.down(aPos);
await tester.pump();
await gesture.moveTo(gPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
expect(controller.selection.baseOffset, testValue.indexOf('g'));
// Release the pointer.
await gesture.up();
@ -2568,11 +2750,8 @@ void main() {
// If the position we tap during a drag start is on the collapsed selection, then
// we can move the cursor with a drag.
// Here we tap on '|a', where our selection was previously, and move to '|i'.
// Unlike our previous attempt to drag to `|g`, this works because moving
// to `|i` includes a horizontal movement so the `TapAndHorizontalDragGestureRecognizer`
// in TextField can beat the `VerticalDragGestureRecognizer` in the scrollable.
await gesture.down(aPos);
// Here we tap on '|g', where our selection was previously, and move to '|i'.
await gesture.down(gPos);
await tester.pump();
await gesture.moveTo(iPos);
await tester.pumpAndSettle();

View File

@ -3395,6 +3395,67 @@ void main() {
},
);
testWidgets('PageView beats SelectableText drag gestures (iOS)', (WidgetTester tester) async {
// This is a regression test for
// https://github.com/flutter/flutter/issues/130198.
final PageController pageController = PageController();
const String testValue = 'abc def ghi jkl mno pqr stu vwx yz';
await tester.pumpWidget(
MaterialApp(
home: Material(
child: PageView(
controller: pageController,
children: const <Widget>[
Center(
child: SelectableText(testValue),
),
SizedBox(
height: 200.0,
child: Center(
child: Text('Page 2'),
),
),
],
),
),
),
);
await skipPastScrollingAnimation(tester);
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
final Offset pPos = textOffsetToPosition(tester, testValue.indexOf('p'));
// A double tap + drag should take precendence over parent drags.
final TestGesture gesture = await tester.startGesture(gPos);
await tester.pump();
await gesture.up();
await tester.pump();
await gesture.down(gPos);
await tester.pumpAndSettle();
await gesture.moveTo(pPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
final TextEditingValue currentValue =
tester.state<EditableTextState>(find.byType(EditableText)).textEditingValue;
expect(currentValue.selection, TextSelection(baseOffset: testValue.indexOf('g'), extentOffset: testValue.indexOf('p') + 3));
expect(pageController.page, isNotNull);
expect(pageController.page, 0.0);
// A horizontal drag directly on the SelectableText should move the page
// view to the next page.
final Rect selectableTextRect = tester.getRect(find.byType(SelectableText));
await tester.dragFrom(selectableTextRect.centerRight - const Offset(0.1, 0.0), const Offset(-500.0, 0.0));
await tester.pumpAndSettle();
expect(pageController.page, isNotNull);
expect(pageController.page, 1.0);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets(
'long press tap cannot initiate a double tap',
(WidgetTester tester) async {

View File

@ -1175,8 +1175,8 @@ void main() {
final Rect hitRect = tester.getRect(gestureDetector);
final Rect textFieldRect = tester.getRect(find.byType(TextField));
expect(hitRect.size.width, lessThan(textFieldRect.size.width));
expect(hitRect.size.height, lessThan(textFieldRect.size.height));
expect(hitRect.size.width, lessThanOrEqualTo(textFieldRect.size.width));
expect(hitRect.size.height, lessThanOrEqualTo(textFieldRect.size.height));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
group('SelectionOverlay', () {