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" />
This commit is contained in:
Renzo Olivares 2023-04-24 12:25:21 -07:00 committed by GitHub
parent 624bdd38b0
commit 1a51dc2131
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 114 additions and 2 deletions

View file

@ -2587,6 +2587,7 @@ 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.
@ -2609,6 +2610,29 @@ class TextSelectionGestureDetectorBuilder {
} else {
switch (defaultTargetPlatform) {
case TargetPlatform.iOS:
switch (details.kind) {
case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
case PointerDeviceKind.stylus:
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:
case TargetPlatform.fuchsia:
switch (details.kind) {
@ -2632,7 +2656,6 @@ class TextSelectionGestureDetectorBuilder {
_showMagnifierIfSupportedByPlatform(details.globalPosition);
}
case null:
break;
}
case TargetPlatform.linux:
case TargetPlatform.macOS:
@ -2754,7 +2777,6 @@ class TextSelectionGestureDetectorBuilder {
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
_dragBeganOnPreviousSelection ??= _positionOnSelection(dragStartGlobalPosition, _dragStartSelection);
assert(_dragBeganOnPreviousSelection != null);
if (renderEditable.hasFocus
&& _dragStartSelection!.isCollapsed

View file

@ -5372,6 +5372,51 @@ void main() {
expect(controller.selection.extentOffset, testValue.indexOf('g'));
});
testWidgets('Cursor should not move on a quick touch drag when touch does not begin on previous selection (iOS)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.pumpAndSettle(const Duration(milliseconds: 200));
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
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);
// The position we tap during a drag start is not on the collapsed selection,
// so the cursor should not move.
await gesture.down(textOffsetToPosition(tester, 7));
await gesture.moveTo(iPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();

View file

@ -2189,6 +2189,51 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Cursor should not move on a quick touch drag when touch does not begin on previous selection (iOS)', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset aPos = textOffsetToPosition(tester, testValue.indexOf('a'));
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);
// The position we tap during a drag start is not on the collapsed selection,
// so the cursor should not move.
await gesture.down(textOffsetToPosition(tester, 7));
await gesture.moveTo(iPos);
await tester.pumpAndSettle();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
},
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
);
testWidgets('Can move cursor when dragging, when tap is on collapsed selection (iOS) - multiline', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();