Unverified Commit ad946f84 authored by Renzo Olivares's avatar Renzo Olivares Committed by GitHub

Add ability to show magnifier on long press (#111224)

parent d339517b
......@@ -65,7 +65,9 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
@override
void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
if (delegate.selectionEnabled) {
switch (Theme.of(_state.context).platform) {
final TargetPlatform targetPlatform = Theme.of(_state.context).platform;
switch (targetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
renderEditable.selectPositionAt(
......@@ -84,6 +86,18 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
);
break;
}
switch (targetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.showMagnifier(details.globalPosition);
break;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
}
}
......@@ -97,7 +111,9 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
@override
void onSingleLongTapStart(LongPressStartDetails details) {
if (delegate.selectionEnabled) {
switch (Theme.of(_state.context).platform) {
final TargetPlatform targetPlatform = Theme.of(_state.context).platform;
switch (targetPlatform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
renderEditable.selectPositionAt(
......@@ -113,6 +129,18 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
Feedback.forLongPress(_state.context);
break;
}
switch (targetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.showMagnifier(details.globalPosition);
break;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
}
}
}
......
......@@ -3324,6 +3324,37 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
/// Shows the magnifier at the position given by `positionToShow`,
/// if there is no magnifier visible.
///
/// Updates the magnifier to the position given by `positionToShow`,
/// if there is a magnifier visible.
///
/// Does nothing if a magnifier couldn't be shown, such as when the selection
/// overlay does not currently exist.
void showMagnifier(Offset positionToShow) {
if (_selectionOverlay == null) {
return;
}
if (_selectionOverlay!.magnifierIsVisible) {
_selectionOverlay!.updateMagnifier(positionToShow);
} else {
_selectionOverlay!.showMagnifier(positionToShow);
}
}
/// Hides the magnifier if it is visible.
void hideMagnifier({required bool shouldShowToolbar}) {
if (_selectionOverlay == null) {
return;
}
if (_selectionOverlay!.magnifierIsVisible) {
_selectionOverlay!.hideMagnifier(shouldShowToolbar: shouldShowToolbar);
}
}
// Tracks the location a [_ScribblePlaceholder] should be rendered in the
// text.
//
......
......@@ -408,6 +408,37 @@ class TextSelectionOverlay {
_selectionOverlay.showToolbar();
}
/// {@macro flutter.widgets.SelectionOverlay.showMagnifier}
void showMagnifier(Offset positionToShow) {
final TextPosition position = renderObject.getPositionForPoint(positionToShow);
_updateSelectionOverlay();
_selectionOverlay.showMagnifier(
_buildMagnifier(
currentTextPosition: position,
globalGesturePosition: positionToShow,
renderEditable: renderObject,
),
);
}
/// {@macro flutter.widgets.SelectionOverlay.updateMagnifier}
void updateMagnifier(Offset positionToShow) {
final TextPosition position = renderObject.getPositionForPoint(positionToShow);
_updateSelectionOverlay();
_selectionOverlay.updateMagnifier(
_buildMagnifier(
currentTextPosition: position,
globalGesturePosition: positionToShow,
renderEditable: renderObject,
),
);
}
/// {@macro flutter.widgets.SelectionOverlay.hideMagnifier}
void hideMagnifier({required bool shouldShowToolbar}) {
_selectionOverlay.hideMagnifier(shouldShowToolbar: shouldShowToolbar);
}
/// Updates the overlay after the selection has changed.
///
/// If this method is called while the [SchedulerBinding.schedulerPhase] is
......@@ -457,6 +488,9 @@ class TextSelectionOverlay {
/// Whether the toolbar is currently visible.
bool get toolbarIsVisible => _selectionOverlay._toolbar != null;
/// Whether the magnifier is currently visible.
bool get magnifierIsVisible => _selectionOverlay._magnifierController.shown;
/// {@macro flutter.widgets.SelectionOverlay.hide}
void hide() => _selectionOverlay.hide();
......@@ -554,11 +588,13 @@ class TextSelectionOverlay {
_dragEndPosition = details.globalPosition + Offset(0.0, -handleSize.height);
final TextPosition position = renderObject.getPositionForPoint(_dragEndPosition);
_selectionOverlay.showMagnifier(_buildMagnifier(
currentTextPosition: position,
globalGesturePosition: details.globalPosition,
renderEditable: renderObject,
));
_selectionOverlay.showMagnifier(
_buildMagnifier(
currentTextPosition: position,
globalGesturePosition: details.globalPosition,
renderEditable: renderObject,
),
);
}
void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) {
......@@ -629,11 +665,13 @@ class TextSelectionOverlay {
_dragStartPosition = details.globalPosition + Offset(0.0, -handleSize.height);
final TextPosition position = renderObject.getPositionForPoint(_dragStartPosition);
_selectionOverlay.showMagnifier(_buildMagnifier(
currentTextPosition: position,
globalGesturePosition: details.globalPosition,
renderEditable: renderObject,
));
_selectionOverlay.showMagnifier(
_buildMagnifier(
currentTextPosition: position,
globalGesturePosition: details.globalPosition,
renderEditable: renderObject,
),
);
}
void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) {
......@@ -788,6 +826,7 @@ class SelectionOverlay {
/// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details}
final TextMagnifierConfiguration magnifierConfiguration;
/// {@template flutter.widgets.SelectionOverlay.showMagnifier}
/// Shows the magnifier, and hides the toolbar if it was showing when [showMagnifier]
/// was called. This is safe to call on platforms not mobile, since
/// a magnifierBuilder will not be provided, or the magnifierBuilder will return null
......@@ -796,6 +835,7 @@ class SelectionOverlay {
/// This is NOT the source of truth for if the magnifier is up or not,
/// since magnifiers may hide themselves. If this info is needed, check
/// [MagnifierController.shown].
/// {@endtemplate}
void showMagnifier(MagnifierOverlayInfoBearer initialInfoBearer) {
if (_toolbar != null) {
hideToolbar();
......@@ -813,7 +853,7 @@ class SelectionOverlay {
_magnifierOverlayInfoBearer,
);
if (builtMagnifier == null) {
if (builtMagnifier == null || _handles == null) {
return;
}
......@@ -825,10 +865,12 @@ class SelectionOverlay {
builder: (_) => builtMagnifier);
}
/// {@template flutter.widgets.SelectionOverlay.hideMagnifier}
/// Hide the current magnifier, optionally immediately showing
/// the toolbar.
///
/// This does nothing if there is no magnifier.
/// {@endtemplate}
void hideMagnifier({required bool shouldShowToolbar}) {
// This cannot be a check on `MagnifierController.shown`, since
// it's possible that the magnifier is still in the overlay, but
......@@ -1250,6 +1292,7 @@ class SelectionOverlay {
);
}
/// {@template flutter.widgets.SelectionOverlay.updateMagnifier}
/// Update the current magnifier with new selection data, so the magnifier
/// can respond accordingly.
///
......@@ -1258,6 +1301,7 @@ class SelectionOverlay {
/// itself.
///
/// If there is no magnifier in the overlay, this does nothing,
/// {@endtemplate}
void updateMagnifier(MagnifierOverlayInfoBearer magnifierOverlayInfoBearer) {
if (_magnifierController.overlayEntry == null) {
return;
......@@ -1919,6 +1963,18 @@ class TextSelectionGestureDetectorBuilder {
from: details.globalPosition,
cause: SelectionChangedCause.longPress,
);
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.showMagnifier(details.globalPosition);
break;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
}
}
......@@ -1938,6 +1994,18 @@ class TextSelectionGestureDetectorBuilder {
from: details.globalPosition,
cause: SelectionChangedCause.longPress,
);
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.showMagnifier(details.globalPosition);
break;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
}
}
......@@ -1951,6 +2019,17 @@ class TextSelectionGestureDetectorBuilder {
/// callback.
@protected
void onSingleLongTapEnd(LongPressEndDetails details) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
editableText.hideMagnifier(shouldShowToolbar: false);
break;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
break;
}
if (shouldShowSelectionToolbar) {
editableText.showToolbar();
}
......
......@@ -6178,6 +6178,66 @@ void main() {
expect(find.byKey(fakeMagnifier.key!), findsNothing);
}, variant: TargetPlatformVariant.only(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;
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
magnifierConfiguration: TextMagnifierConfiguration(
magnifierBuilder: (
_,
MagnifierController controller,
ValueNotifier<MagnifierOverlayInfoBearer> localInfoBearer
) {
infoBearer = localInfoBearer;
return fakeMagnifier;
},
),
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
await tester.pumpAndSettle();
// Tap at 'e' to set the selection to position 5 on Android.
// Tap at 'e' to set the selection to the closest word edge, which is position 4 on iOS.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pumpAndSettle(const Duration(milliseconds: 300));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 4);
expect(find.byKey(fakeMagnifier.key!), findsNothing);
// Long press the 'e' to move the cursor in front of the 'e' and show the magnifier.
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pumpAndSettle(const Duration(milliseconds: 1000));
expect(controller.selection.baseOffset, 5);
expect(controller.selection.extentOffset, 5);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
final Offset firstLongPressGesturePosition = infoBearer.value.globalGesturePosition;
// Move the gesture to 'h' to update the magnifier and move the cursor to 'h'.
await gesture.moveTo(textOffsetToPosition(tester, testValue.indexOf('h')));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, 9);
expect(controller.selection.extentOffset, 9);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
// Expect the position the magnifier gets to have moved.
expect(firstLongPressGesturePosition, isNot(infoBearer.value.globalGesturePosition));
// End the long press to hide the magnifier.
await gesture.up();
await tester.pumpAndSettle();
expect(find.byKey(fakeMagnifier.key!), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }));
group('TapRegion integration', () {
testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
......
......@@ -12254,6 +12254,70 @@ void main() {
expect(find.byKey(fakeMagnifier.key!), findsNothing);
});
testWidgets('Can long press to show, unshow, and update magnifier', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
final bool isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
magnifierConfiguration: TextMagnifierConfiguration(
magnifierBuilder: (
_,
MagnifierController controller,
ValueNotifier<MagnifierOverlayInfoBearer> localInfoBearer
) {
infoBearer = localInfoBearer;
return fakeMagnifier;
},
),
),
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
// Tap at 'e' to set the selection to position 5 on Android.
// Tap at 'e' to set the selection to the closest word edge, which is position 4 on iOS.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pumpAndSettle(const Duration(milliseconds: 300));
expect(controller.selection.isCollapsed, true);
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 4);
expect(find.byKey(fakeMagnifier.key!), findsNothing);
// Long press the 'e' to select 'def' on Android and show magnifier.
// Long press the 'e' to move the cursor in front of the 'e' on iOS and show the magnifier.
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pumpAndSettle(const Duration(milliseconds: 1000));
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 4 : 5);
expect(controller.selection.extentOffset, isTargetPlatformAndroid ? 7 : 5);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
final Offset firstLongPressGesturePosition = infoBearer.value.globalGesturePosition;
// Move the gesture to 'h' on Android to update the magnifier and select 'ghi'.
// Move the gesture to 'h' on iOS to update the magnifier and move the cursor to 'h'.
await gesture.moveTo(textOffsetToPosition(tester, testValue.indexOf('h')));
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 4 : 9);
expect(controller.selection.extentOffset, isTargetPlatformAndroid ? 11 : 9);
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
// Expect the position the magnifier gets to have moved.
expect(firstLongPressGesturePosition, isNot(infoBearer.value.globalGesturePosition));
// End the long press to hide the magnifier.
await gesture.up();
await tester.pumpAndSettle();
expect(find.byKey(fakeMagnifier.key!), findsNothing);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }));
group('TapRegion integration', () {
testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
......
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