Unverified Commit 8ed26d8b authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

Show magnifier on touch drag gestures (#125151)

This change shows the magnifier on touch drag gestures for Android and iOS and hides it when the drag ends.

Fixes #118268
parent f5b0f0a2
...@@ -123,6 +123,7 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe ...@@ -123,6 +123,7 @@ class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGe
@override @override
void onDragSelectionEnd(TapDragEndDetails details) { void onDragSelectionEnd(TapDragEndDetails details) {
_state._requestKeyboard(); _state._requestKeyboard();
super.onDragSelectionEnd(details);
} }
} }
......
...@@ -1858,6 +1858,33 @@ class TextSelectionGestureDetectorBuilder { ...@@ -1858,6 +1858,33 @@ class TextSelectionGestureDetectorBuilder {
@protected @protected
final TextSelectionGestureDetectorBuilderDelegate delegate; 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. /// Returns true if lastSecondaryTapDownPosition was on selection.
bool get _lastSecondaryTapWasOnSelection { bool get _lastSecondaryTapWasOnSelection {
assert(renderEditable.lastSecondaryTapDownPosition != null); assert(renderEditable.lastSecondaryTapDownPosition != null);
...@@ -2291,16 +2318,7 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2291,16 +2318,7 @@ class TextSelectionGestureDetectorBuilder {
renderEditable.selectWord(cause: SelectionChangedCause.longPress); renderEditable.selectWord(cause: SelectionChangedCause.longPress);
} }
switch (defaultTargetPlatform) { _showMagnifierIfSupportedByPlatform(details.globalPosition);
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.showMagnifier(details.globalPosition);
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
_dragStartViewportOffset = renderEditable.offset.pixels; _dragStartViewportOffset = renderEditable.offset.pixels;
_dragStartScrollOffset = _scrollPosition; _dragStartScrollOffset = _scrollPosition;
...@@ -2354,16 +2372,7 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2354,16 +2372,7 @@ class TextSelectionGestureDetectorBuilder {
); );
} }
switch (defaultTargetPlatform) { _showMagnifierIfSupportedByPlatform(details.globalPosition);
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.showMagnifier(details.globalPosition);
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
} }
} }
...@@ -2377,16 +2386,7 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2377,16 +2386,7 @@ class TextSelectionGestureDetectorBuilder {
/// callback. /// callback.
@protected @protected
void onSingleLongTapEnd(LongPressEndDetails details) { void onSingleLongTapEnd(LongPressEndDetails details) {
switch (defaultTargetPlatform) { _hideMagnifierIfSupportedByPlatform();
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.hideMagnifier();
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
if (shouldShowSelectionToolbar) { if (shouldShowSelectionToolbar) {
editableText.showToolbar(); editableText.showToolbar();
} }
...@@ -2599,12 +2599,12 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2599,12 +2599,12 @@ class TextSelectionGestureDetectorBuilder {
switch (details.kind) { switch (details.kind) {
case PointerDeviceKind.mouse: case PointerDeviceKind.mouse:
case PointerDeviceKind.trackpad: case PointerDeviceKind.trackpad:
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
renderEditable.selectPositionAt( renderEditable.selectPositionAt(
from: details.globalPosition, from: details.globalPosition,
cause: SelectionChangedCause.drag, cause: SelectionChangedCause.drag,
); );
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch: case PointerDeviceKind.touch:
case PointerDeviceKind.unknown: case PointerDeviceKind.unknown:
// For Android, Fucshia, and iOS platforms, a touch drag // For Android, Fucshia, and iOS platforms, a touch drag
...@@ -2614,6 +2614,7 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2614,6 +2614,7 @@ class TextSelectionGestureDetectorBuilder {
from: details.globalPosition, from: details.globalPosition,
cause: SelectionChangedCause.drag, cause: SelectionChangedCause.drag,
); );
_showMagnifierIfSupportedByPlatform(details.globalPosition);
} }
case null: case null:
break; break;
...@@ -2659,11 +2660,23 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2659,11 +2660,23 @@ class TextSelectionGestureDetectorBuilder {
// Select word by word. // Select word by word.
if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) { if (_TextSelectionGestureDetectorState._getEffectiveConsecutiveTapCount(details.consecutiveTapCount) == 2) {
return renderEditable.selectWordsInRange( renderEditable.selectWordsInRange(
from: dragStartGlobalPosition - editableOffset - scrollableOffset, from: dragStartGlobalPosition - editableOffset - scrollableOffset,
to: details.globalPosition, to: details.globalPosition,
cause: SelectionChangedCause.drag, 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. // Select paragraph-by-paragraph.
...@@ -2732,10 +2745,11 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2732,10 +2745,11 @@ class TextSelectionGestureDetectorBuilder {
&& _dragStartSelection!.isCollapsed && _dragStartSelection!.isCollapsed
&& _dragBeganOnPreviousSelection! && _dragBeganOnPreviousSelection!
) { ) {
return renderEditable.selectPositionAt( renderEditable.selectPositionAt(
from: details.globalPosition, from: details.globalPosition,
cause: SelectionChangedCause.drag, cause: SelectionChangedCause.drag,
); );
return _showMagnifierIfSupportedByPlatform(details.globalPosition);
} }
case null: case null:
break; break;
...@@ -2759,10 +2773,11 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2759,10 +2773,11 @@ class TextSelectionGestureDetectorBuilder {
case PointerDeviceKind.touch: case PointerDeviceKind.touch:
case PointerDeviceKind.unknown: case PointerDeviceKind.unknown:
if (renderEditable.hasFocus) { if (renderEditable.hasFocus) {
return renderEditable.selectPositionAt( renderEditable.selectPositionAt(
from: details.globalPosition, from: details.globalPosition,
cause: SelectionChangedCause.drag, cause: SelectionChangedCause.drag,
); );
return _showMagnifierIfSupportedByPlatform(details.globalPosition);
} }
case null: case null:
break; break;
...@@ -2838,6 +2853,8 @@ class TextSelectionGestureDetectorBuilder { ...@@ -2838,6 +2853,8 @@ class TextSelectionGestureDetectorBuilder {
if (isShiftPressed) { if (isShiftPressed) {
_dragStartSelection = null; _dragStartSelection = null;
} }
_hideMagnifierIfSupportedByPlatform();
} }
/// Returns a [TextSelectionGestureDetector] configured with the handlers /// Returns a [TextSelectionGestureDetector] configured with the handlers
......
...@@ -8907,6 +8907,105 @@ void main() { ...@@ -8907,6 +8907,105 @@ void main() {
expect(find.byKey(fakeMagnifier.key!), findsNothing); expect(find.byKey(fakeMagnifier.key!), findsNothing);
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); }, 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 { testWidgets('Can long press to show, unshow, and update magnifier on non-Apple platforms', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
final bool isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android; final bool isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
......
...@@ -15770,6 +15770,107 @@ void main() { ...@@ -15770,6 +15770,107 @@ void main() {
expect(find.byKey(fakeMagnifier.key!), findsNothing); 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 { testWidgets('Can long press to show, unshow, and update magnifier', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
final bool isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android; final bool isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment