Skip to content

Commit

Permalink
Show magnifier on touch drag gestures (#125151)
Browse files Browse the repository at this point in the history
This change shows the magnifier on touch drag gestures for Android and iOS and hides it when the drag ends.

Fixes #118268
  • Loading branch information
Renzo-Olivares authored Apr 20, 2023
1 parent f5b0f0a commit 8ed26d8
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 35 deletions.
1 change: 1 addition & 0 deletions packages/flutter/lib/src/cupertino/text_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe
@override
void onDragSelectionEnd(TapDragEndDetails details) {
_state._requestKeyboard();
super.onDragSelectionEnd(details);
}
}

Expand Down
87 changes: 52 additions & 35 deletions packages/flutter/lib/src/widgets/text_selection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1858,6 +1858,33 @@ class TextSelectionGestureDetectorBuilder {
@protected
final TextSelectionGestureDetectorBuilderDelegate delegate;

// Shows the magnifier on supported platforms at the given offset, currently
// only Android and iOS.
void _showMagnifierIfSupportedByPlatform(Offset positionToShow) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.showMagnifier(positionToShow);
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
}
}

// Hides the magnifier on supported platforms, currently only Android and iOS.
void _hideMagnifierIfSupportedByPlatform() {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.hideMagnifier();
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
}
}

/// Returns true if lastSecondaryTapDownPosition was on selection.
bool get _lastSecondaryTapWasOnSelection {
assert(renderEditable.lastSecondaryTapDownPosition != null);
Expand Down Expand Up @@ -2291,16 +2318,7 @@ class TextSelectionGestureDetectorBuilder {
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
}

switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.showMagnifier(details.globalPosition);
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
_showMagnifierIfSupportedByPlatform(details.globalPosition);

_dragStartViewportOffset = renderEditable.offset.pixels;
_dragStartScrollOffset = _scrollPosition;
Expand Down Expand Up @@ -2354,16 +2372,7 @@ class TextSelectionGestureDetectorBuilder {
);
}

switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.showMagnifier(details.globalPosition);
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
_showMagnifierIfSupportedByPlatform(details.globalPosition);
}
}

Expand All @@ -2377,16 +2386,7 @@ class TextSelectionGestureDetectorBuilder {
/// callback.
@protected
void onSingleLongTapEnd(LongPressEndDetails details) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.hideMagnifier();
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
_hideMagnifierIfSupportedByPlatform();
if (shouldShowSelectionToolbar) {
editableText.showToolbar();
}
Expand Down Expand Up @@ -2599,12 +2599,12 @@ class TextSelectionGestureDetectorBuilder {
switch (details.kind) {
case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
// For Android, Fucshia, and iOS platforms, a touch drag
Expand All @@ -2614,6 +2614,7 @@ class TextSelectionGestureDetectorBuilder {
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
_showMagnifierIfSupportedByPlatform(details.globalPosition);
}
case null:
break;
Expand Down Expand Up @@ -2659,11 +2660,23 @@ class TextSelectionGestureDetectorBuilder {

// Select word by word.
if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) {
return renderEditable.selectWordsInRange(
renderEditable.selectWordsInRange(
from: dragStartGlobalPosition - editableOffset - scrollableOffset,
to: details.globalPosition,
cause: SelectionChangedCause.drag,
);

switch (details.kind) {
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
return _showMagnifierIfSupportedByPlatform(details.globalPosition);
case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad:
case null:
return;
}
}

// Select paragraph-by-paragraph.
Expand Down Expand Up @@ -2732,10 +2745,11 @@ class TextSelectionGestureDetectorBuilder {
&& _dragStartSelection!.isCollapsed
&& _dragBeganOnPreviousSelection!
) {
return renderEditable.selectPositionAt(
renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
return _showMagnifierIfSupportedByPlatform(details.globalPosition);
}
case null:
break;
Expand All @@ -2759,10 +2773,11 @@ class TextSelectionGestureDetectorBuilder {
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
if (renderEditable.hasFocus) {
return renderEditable.selectPositionAt(
renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
return _showMagnifierIfSupportedByPlatform(details.globalPosition);
}
case null:
break;
Expand Down Expand Up @@ -2838,6 +2853,8 @@ class TextSelectionGestureDetectorBuilder {
if (isShiftPressed) {
_dragStartSelection = null;
}

_hideMagnifierIfSupportedByPlatform();
}

/// Returns a [TextSelectionGestureDetector] configured with the handlers
Expand Down
99 changes: 99 additions & 0 deletions packages/flutter/test/cupertino/text_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8907,6 +8907,105 @@ void main() {
expect(find.byKey(fakeMagnifier.key!), findsNothing);
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS));

testWidgets('Can drag to show, unshow, and update magnifier', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
magnifierConfiguration: TextMagnifierConfiguration(
magnifierBuilder: (
_,
MagnifierController controller,
ValueNotifier<MagnifierInfo> localMagnifierInfo
) {
magnifierInfo = localMagnifierInfo;
return fakeMagnifier;
},
),
),
),
),
);

const String testValue = 'abc def ghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.pumpAndSettle();

// Tap at '|a' to move the selection to position 0.
await tester.tapAt(textOffsetToPosition(tester, 0));
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
expect(find.byKey(fakeMagnifier.key!), findsNothing);

// Start a drag gesture to move the selection to the dragged position, showing
// the magnifier.
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(tester, 0));
await tester.pump();

await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);

Offset firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;

await gesture.moveTo(textOffsetToPosition(tester, 10));
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 10);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
// Expect the position the magnifier gets to have moved.
expect(firstDragGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));

// The magnifier should hide when the drag ends.
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 10);
expect(find.byKey(fakeMagnifier.key!), findsNothing);

// Start a double-tap select the word at the tapped position.
await gesture.down(textOffsetToPosition(tester, 1));
await tester.pump();
await gesture.up();
await tester.pump();

await gesture.down(textOffsetToPosition(tester, 1));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 3);

// Start a drag gesture to extend the selection word-by-word, showing the
// magnifier.
await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 7);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);

firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;

await gesture.moveTo(textOffsetToPosition(tester, 10));
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 11);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
// Expect the position the magnifier gets to have moved.
expect(firstDragGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));

// The magnifier should hide when the drag ends.
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 11);
expect(find.byKey(fakeMagnifier.key!), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }));

testWidgets('Can long press to show, unshow, and update magnifier on non-Apple platforms', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
final bool isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
Expand Down
101 changes: 101 additions & 0 deletions packages/flutter/test/material/text_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15770,6 +15770,107 @@ void main() {
expect(find.byKey(fakeMagnifier.key!), findsNothing);
});

testWidgets('Can drag to show, unshow, and update magnifier', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
magnifierConfiguration: TextMagnifierConfiguration(
magnifierBuilder: (
_,
MagnifierController controller,
ValueNotifier<MagnifierInfo> localMagnifierInfo
) {
magnifierInfo = localMagnifierInfo;
return fakeMagnifier;
},
),
),
),
),
),
);

const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);

// Tap at '|a' to move the selection to position 0.
await tester.tapAt(textOffsetToPosition(tester, 0));
await tester.pumpAndSettle(kDoubleTapTimeout);
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 0);
expect(find.byKey(fakeMagnifier.key!), findsNothing);

// Start a drag gesture to move the selection to the dragged position, showing
// the magnifier.
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(tester, 0));
await tester.pump();

await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 5);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);

Offset firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;

await gesture.moveTo(textOffsetToPosition(tester, 10));
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 10);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
// Expect the position the magnifier gets to have moved.
expect(firstDragGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));

// The magnifier should hide when the drag ends.
await gesture.up();
await tester.pump();
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, 10);
expect(find.byKey(fakeMagnifier.key!), findsNothing);

// Start a double-tap select the word at the tapped position.
await gesture.down(textOffsetToPosition(tester, 1));
await tester.pump();
await gesture.up();
await tester.pump();

await gesture.down(textOffsetToPosition(tester, 1));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 3);

// Start a drag gesture to extend the selection word-by-word, showing the
// magnifier.
await gesture.moveTo(textOffsetToPosition(tester, 5));
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 7);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);

firstDragGesturePosition = magnifierInfo.value.globalGesturePosition;

await gesture.moveTo(textOffsetToPosition(tester, 10));
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 11);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
// Expect the position the magnifier gets to have moved.
expect(firstDragGesturePosition, isNot(magnifierInfo.value.globalGesturePosition));

// The magnifier should hide when the drag ends.
await gesture.up();
await tester.pump();
expect(controller.selection.baseOffset, 0);
expect(controller.selection.extentOffset, 11);
expect(find.byKey(fakeMagnifier.key!), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }));

testWidgets('Can long press to show, unshow, and update magnifier', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
final bool isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
Expand Down

0 comments on commit 8ed26d8

Please sign in to comment.