Skip to content

Commit 1a51dc2

Browse files
Fix iOS touch drag behavior (#125169)
Before this change on a quick touch drag the cursor, where the touch is not on the previous collapsed selection, the cursor would move to the tapped position. After this change on a quick touch drag the cursor does not move unless the touch is on the previously collapsed selection. This is inline with native behavior. Before|After|Native --|--|-- <video src="https://user-images.githubusercontent.com/948037/233224775-f33b42b5-5638-416c-9278-39ecd964e3bb.mov" />|<video src="https://user-images.githubusercontent.com/948037/233224760-2d1af657-8d99-45fc-8499-9567f17d533e.mov" />|<video src="https://user-images.githubusercontent.com/948037/233224790-f5997cfa-7370-4891-8952-11ef8057a729.mov" />
1 parent 624bdd3 commit 1a51dc2

File tree

3 files changed

+114
-2
lines changed

3 files changed

+114
-2
lines changed

packages/flutter/lib/src/widgets/text_selection.dart

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2587,6 +2587,7 @@ class TextSelectionGestureDetectorBuilder {
25872587
_dragStartSelection = renderEditable.selection;
25882588
_dragStartScrollOffset = _scrollPosition;
25892589
_dragStartViewportOffset = renderEditable.offset.pixels;
2590+
_dragBeganOnPreviousSelection = _positionOnSelection(details.globalPosition, _dragStartSelection);
25902591

25912592
if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount(details.consecutiveTapCount) > 1) {
25922593
// Do not set the selection on a consecutive tap and drag.
@@ -2609,6 +2610,29 @@ class TextSelectionGestureDetectorBuilder {
26092610
} else {
26102611
switch (defaultTargetPlatform) {
26112612
case TargetPlatform.iOS:
2613+
switch (details.kind) {
2614+
case PointerDeviceKind.mouse:
2615+
case PointerDeviceKind.trackpad:
2616+
renderEditable.selectPositionAt(
2617+
from: details.globalPosition,
2618+
cause: SelectionChangedCause.drag,
2619+
);
2620+
case PointerDeviceKind.stylus:
2621+
case PointerDeviceKind.invertedStylus:
2622+
case PointerDeviceKind.touch:
2623+
case PointerDeviceKind.unknown:
2624+
// For iOS platforms, a touch drag does not initiate unless the
2625+
// editable has focus and the drag began on the previous selection.
2626+
assert(_dragBeganOnPreviousSelection != null);
2627+
if (renderEditable.hasFocus && _dragBeganOnPreviousSelection!) {
2628+
renderEditable.selectPositionAt(
2629+
from: details.globalPosition,
2630+
cause: SelectionChangedCause.drag,
2631+
);
2632+
_showMagnifierIfSupportedByPlatform(details.globalPosition);
2633+
}
2634+
case null:
2635+
}
26122636
case TargetPlatform.android:
26132637
case TargetPlatform.fuchsia:
26142638
switch (details.kind) {
@@ -2632,7 +2656,6 @@ class TextSelectionGestureDetectorBuilder {
26322656
_showMagnifierIfSupportedByPlatform(details.globalPosition);
26332657
}
26342658
case null:
2635-
break;
26362659
}
26372660
case TargetPlatform.linux:
26382661
case TargetPlatform.macOS:
@@ -2754,7 +2777,6 @@ class TextSelectionGestureDetectorBuilder {
27542777
case PointerDeviceKind.invertedStylus:
27552778
case PointerDeviceKind.touch:
27562779
case PointerDeviceKind.unknown:
2757-
_dragBeganOnPreviousSelection ??= _positionOnSelection(dragStartGlobalPosition, _dragStartSelection);
27582780
assert(_dragBeganOnPreviousSelection != null);
27592781
if (renderEditable.hasFocus
27602782
&& _dragStartSelection!.isCollapsed

packages/flutter/test/cupertino/text_field_test.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5372,6 +5372,51 @@ void main() {
53725372
expect(controller.selection.extentOffset, testValue.indexOf('g'));
53735373
});
53745374

5375+
testWidgets('Cursor should not move on a quick touch drag when touch does not begin on previous selection (iOS)', (WidgetTester tester) async {
5376+
final TextEditingController controller = TextEditingController();
5377+
5378+
await tester.pumpWidget(
5379+
CupertinoApp(
5380+
home: CupertinoPageScaffold(
5381+
child: CupertinoTextField(
5382+
dragStartBehavior: DragStartBehavior.down,
5383+
controller: controller,
5384+
),
5385+
),
5386+
),
5387+
);
5388+
5389+
const String testValue = 'abc def ghi';
5390+
await tester.enterText(find.byType(CupertinoTextField), testValue);
5391+
await tester.pumpAndSettle(const Duration(milliseconds: 200));
5392+
5393+
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
5394+
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
5395+
5396+
// Tap on text field to gain focus, and set selection to '|a'. On iOS
5397+
// the selection is set to the word edge closest to the tap position.
5398+
// We await for [kDoubleTapTimeout] after the up event, so our next down
5399+
// event does not register as a double tap.
5400+
final TestGesture gesture = await tester.startGesture(aPos);
5401+
await tester.pump();
5402+
await gesture.up();
5403+
await tester.pumpAndSettle(kDoubleTapTimeout);
5404+
5405+
expect(controller.selection.isCollapsed, true);
5406+
expect(controller.selection.baseOffset, 0);
5407+
5408+
// The position we tap during a drag start is not on the collapsed selection,
5409+
// so the cursor should not move.
5410+
await gesture.down(textOffsetToPosition(tester, 7));
5411+
await gesture.moveTo(iPos);
5412+
await tester.pumpAndSettle();
5413+
5414+
expect(controller.selection.isCollapsed, true);
5415+
expect(controller.selection.baseOffset, 0);
5416+
},
5417+
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
5418+
);
5419+
53755420
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS)', (WidgetTester tester) async {
53765421
final TextEditingController controller = TextEditingController();
53775422

packages/flutter/test/material/text_field_test.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2189,6 +2189,51 @@ void main() {
21892189
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
21902190
);
21912191

2192+
testWidgets('Cursor should not move on a quick touch drag when touch does not begin on previous selection (iOS)', (WidgetTester tester) async {
2193+
final TextEditingController controller = TextEditingController();
2194+
2195+
await tester.pumpWidget(
2196+
MaterialApp(
2197+
home: Material(
2198+
child: TextField(
2199+
dragStartBehavior: DragStartBehavior.down,
2200+
controller: controller,
2201+
),
2202+
),
2203+
),
2204+
);
2205+
2206+
const String testValue = 'abc def ghi';
2207+
await tester.enterText(find.byType(TextField), testValue);
2208+
await skipPastScrollingAnimation(tester);
2209+
2210+
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
2211+
final Offset iPos = textOffsetToPosition(tester, testValue.indexOf('i'));
2212+
2213+
// Tap on text field to gain focus, and set selection to '|a'. On iOS
2214+
// the selection is set to the word edge closest to the tap position.
2215+
// We await for kDoubleTapTimeout after the up event, so our next down event
2216+
// does not register as a double tap.
2217+
final TestGesture gesture = await tester.startGesture(aPos);
2218+
await tester.pump();
2219+
await gesture.up();
2220+
await tester.pumpAndSettle(kDoubleTapTimeout);
2221+
2222+
expect(controller.selection.isCollapsed, true);
2223+
expect(controller.selection.baseOffset, 0);
2224+
2225+
// The position we tap during a drag start is not on the collapsed selection,
2226+
// so the cursor should not move.
2227+
await gesture.down(textOffsetToPosition(tester, 7));
2228+
await gesture.moveTo(iPos);
2229+
await tester.pumpAndSettle();
2230+
2231+
expect(controller.selection.isCollapsed, true);
2232+
expect(controller.selection.baseOffset, 0);
2233+
},
2234+
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
2235+
);
2236+
21922237
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - multiline', (WidgetTester tester) async {
21932238
final TextEditingController controller = TextEditingController();
21942239

0 commit comments

Comments
 (0)