Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ad946f8

Browse files
Add ability to show magnifier on long press (#111224)
1 parent d339517 commit ad946f8

File tree

5 files changed

+275
-13
lines changed

5 files changed

+275
-13
lines changed

packages/flutter/lib/src/material/text_field.dart

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
6565
@override
6666
void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
6767
if (delegate.selectionEnabled) {
68-
switch (Theme.of(_state.context).platform) {
68+
final TargetPlatform targetPlatform = Theme.of(_state.context).platform;
69+
70+
switch (targetPlatform) {
6971
case TargetPlatform.iOS:
7072
case TargetPlatform.macOS:
7173
renderEditable.selectPositionAt(
@@ -84,6 +86,18 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
8486
);
8587
break;
8688
}
89+
90+
switch (targetPlatform) {
91+
case TargetPlatform.android:
92+
case TargetPlatform.iOS:
93+
editableText.showMagnifier(details.globalPosition);
94+
break;
95+
case TargetPlatform.fuchsia:
96+
case TargetPlatform.linux:
97+
case TargetPlatform.macOS:
98+
case TargetPlatform.windows:
99+
break;
100+
}
87101
}
88102
}
89103

@@ -97,7 +111,9 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
97111
@override
98112
void onSingleLongTapStart(LongPressStartDetails details) {
99113
if (delegate.selectionEnabled) {
100-
switch (Theme.of(_state.context).platform) {
114+
final TargetPlatform targetPlatform = Theme.of(_state.context).platform;
115+
116+
switch (targetPlatform) {
101117
case TargetPlatform.iOS:
102118
case TargetPlatform.macOS:
103119
renderEditable.selectPositionAt(
@@ -113,6 +129,18 @@ class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDete
113129
Feedback.forLongPress(_state.context);
114130
break;
115131
}
132+
133+
switch (targetPlatform) {
134+
case TargetPlatform.android:
135+
case TargetPlatform.iOS:
136+
editableText.showMagnifier(details.globalPosition);
137+
break;
138+
case TargetPlatform.fuchsia:
139+
case TargetPlatform.linux:
140+
case TargetPlatform.macOS:
141+
case TargetPlatform.windows:
142+
break;
143+
}
116144
}
117145
}
118146
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3324,6 +3324,37 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
33243324
}
33253325
}
33263326

3327+
/// Shows the magnifier at the position given by `positionToShow`,
3328+
/// if there is no magnifier visible.
3329+
///
3330+
/// Updates the magnifier to the position given by `positionToShow`,
3331+
/// if there is a magnifier visible.
3332+
///
3333+
/// Does nothing if a magnifier couldn't be shown, such as when the selection
3334+
/// overlay does not currently exist.
3335+
void showMagnifier(Offset positionToShow) {
3336+
if (_selectionOverlay == null) {
3337+
return;
3338+
}
3339+
3340+
if (_selectionOverlay!.magnifierIsVisible) {
3341+
_selectionOverlay!.updateMagnifier(positionToShow);
3342+
} else {
3343+
_selectionOverlay!.showMagnifier(positionToShow);
3344+
}
3345+
}
3346+
3347+
/// Hides the magnifier if it is visible.
3348+
void hideMagnifier({required bool shouldShowToolbar}) {
3349+
if (_selectionOverlay == null) {
3350+
return;
3351+
}
3352+
3353+
if (_selectionOverlay!.magnifierIsVisible) {
3354+
_selectionOverlay!.hideMagnifier(shouldShowToolbar: shouldShowToolbar);
3355+
}
3356+
}
3357+
33273358
// Tracks the location a [_ScribblePlaceholder] should be rendered in the
33283359
// text.
33293360
//

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

Lines changed: 90 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,37 @@ class TextSelectionOverlay {
408408
_selectionOverlay.showToolbar();
409409
}
410410

411+
/// {@macro flutter.widgets.SelectionOverlay.showMagnifier}
412+
void showMagnifier(Offset positionToShow) {
413+
final TextPosition position = renderObject.getPositionForPoint(positionToShow);
414+
_updateSelectionOverlay();
415+
_selectionOverlay.showMagnifier(
416+
_buildMagnifier(
417+
currentTextPosition: position,
418+
globalGesturePosition: positionToShow,
419+
renderEditable: renderObject,
420+
),
421+
);
422+
}
423+
424+
/// {@macro flutter.widgets.SelectionOverlay.updateMagnifier}
425+
void updateMagnifier(Offset positionToShow) {
426+
final TextPosition position = renderObject.getPositionForPoint(positionToShow);
427+
_updateSelectionOverlay();
428+
_selectionOverlay.updateMagnifier(
429+
_buildMagnifier(
430+
currentTextPosition: position,
431+
globalGesturePosition: positionToShow,
432+
renderEditable: renderObject,
433+
),
434+
);
435+
}
436+
437+
/// {@macro flutter.widgets.SelectionOverlay.hideMagnifier}
438+
void hideMagnifier({required bool shouldShowToolbar}) {
439+
_selectionOverlay.hideMagnifier(shouldShowToolbar: shouldShowToolbar);
440+
}
441+
411442
/// Updates the overlay after the selection has changed.
412443
///
413444
/// If this method is called while the [SchedulerBinding.schedulerPhase] is
@@ -457,6 +488,9 @@ class TextSelectionOverlay {
457488
/// Whether the toolbar is currently visible.
458489
bool get toolbarIsVisible => _selectionOverlay._toolbar != null;
459490

491+
/// Whether the magnifier is currently visible.
492+
bool get magnifierIsVisible => _selectionOverlay._magnifierController.shown;
493+
460494
/// {@macro flutter.widgets.SelectionOverlay.hide}
461495
void hide() => _selectionOverlay.hide();
462496

@@ -554,11 +588,13 @@ class TextSelectionOverlay {
554588
_dragEndPosition = details.globalPosition + Offset(0.0, -handleSize.height);
555589
final TextPosition position = renderObject.getPositionForPoint(_dragEndPosition);
556590

557-
_selectionOverlay.showMagnifier(_buildMagnifier(
558-
currentTextPosition: position,
559-
globalGesturePosition: details.globalPosition,
560-
renderEditable: renderObject,
561-
));
591+
_selectionOverlay.showMagnifier(
592+
_buildMagnifier(
593+
currentTextPosition: position,
594+
globalGesturePosition: details.globalPosition,
595+
renderEditable: renderObject,
596+
),
597+
);
562598
}
563599

564600
void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) {
@@ -629,11 +665,13 @@ class TextSelectionOverlay {
629665
_dragStartPosition = details.globalPosition + Offset(0.0, -handleSize.height);
630666
final TextPosition position = renderObject.getPositionForPoint(_dragStartPosition);
631667

632-
_selectionOverlay.showMagnifier(_buildMagnifier(
633-
currentTextPosition: position,
634-
globalGesturePosition: details.globalPosition,
635-
renderEditable: renderObject,
636-
));
668+
_selectionOverlay.showMagnifier(
669+
_buildMagnifier(
670+
currentTextPosition: position,
671+
globalGesturePosition: details.globalPosition,
672+
renderEditable: renderObject,
673+
),
674+
);
637675
}
638676

639677
void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) {
@@ -788,6 +826,7 @@ class SelectionOverlay {
788826
/// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details}
789827
final TextMagnifierConfiguration magnifierConfiguration;
790828

829+
/// {@template flutter.widgets.SelectionOverlay.showMagnifier}
791830
/// Shows the magnifier, and hides the toolbar if it was showing when [showMagnifier]
792831
/// was called. This is safe to call on platforms not mobile, since
793832
/// a magnifierBuilder will not be provided, or the magnifierBuilder will return null
@@ -796,6 +835,7 @@ class SelectionOverlay {
796835
/// This is NOT the source of truth for if the magnifier is up or not,
797836
/// since magnifiers may hide themselves. If this info is needed, check
798837
/// [MagnifierController.shown].
838+
/// {@endtemplate}
799839
void showMagnifier(MagnifierOverlayInfoBearer initialInfoBearer) {
800840
if (_toolbar != null) {
801841
hideToolbar();
@@ -813,7 +853,7 @@ class SelectionOverlay {
813853
_magnifierOverlayInfoBearer,
814854
);
815855

816-
if (builtMagnifier == null) {
856+
if (builtMagnifier == null || _handles == null) {
817857
return;
818858
}
819859

@@ -825,10 +865,12 @@ class SelectionOverlay {
825865
builder: (_) => builtMagnifier);
826866
}
827867

868+
/// {@template flutter.widgets.SelectionOverlay.hideMagnifier}
828869
/// Hide the current magnifier, optionally immediately showing
829870
/// the toolbar.
830871
///
831872
/// This does nothing if there is no magnifier.
873+
/// {@endtemplate}
832874
void hideMagnifier({required bool shouldShowToolbar}) {
833875
// This cannot be a check on `MagnifierController.shown`, since
834876
// it's possible that the magnifier is still in the overlay, but
@@ -1250,6 +1292,7 @@ class SelectionOverlay {
12501292
);
12511293
}
12521294

1295+
/// {@template flutter.widgets.SelectionOverlay.updateMagnifier}
12531296
/// Update the current magnifier with new selection data, so the magnifier
12541297
/// can respond accordingly.
12551298
///
@@ -1258,6 +1301,7 @@ class SelectionOverlay {
12581301
/// itself.
12591302
///
12601303
/// If there is no magnifier in the overlay, this does nothing,
1304+
/// {@endtemplate}
12611305
void updateMagnifier(MagnifierOverlayInfoBearer magnifierOverlayInfoBearer) {
12621306
if (_magnifierController.overlayEntry == null) {
12631307
return;
@@ -1919,6 +1963,18 @@ class TextSelectionGestureDetectorBuilder {
19191963
from: details.globalPosition,
19201964
cause: SelectionChangedCause.longPress,
19211965
);
1966+
1967+
switch (defaultTargetPlatform) {
1968+
case TargetPlatform.android:
1969+
case TargetPlatform.iOS:
1970+
editableText.showMagnifier(details.globalPosition);
1971+
break;
1972+
case TargetPlatform.fuchsia:
1973+
case TargetPlatform.linux:
1974+
case TargetPlatform.macOS:
1975+
case TargetPlatform.windows:
1976+
break;
1977+
}
19221978
}
19231979
}
19241980

@@ -1938,6 +1994,18 @@ class TextSelectionGestureDetectorBuilder {
19381994
from: details.globalPosition,
19391995
cause: SelectionChangedCause.longPress,
19401996
);
1997+
1998+
switch (defaultTargetPlatform) {
1999+
case TargetPlatform.android:
2000+
case TargetPlatform.iOS:
2001+
editableText.showMagnifier(details.globalPosition);
2002+
break;
2003+
case TargetPlatform.fuchsia:
2004+
case TargetPlatform.linux:
2005+
case TargetPlatform.macOS:
2006+
case TargetPlatform.windows:
2007+
break;
2008+
}
19412009
}
19422010
}
19432011

@@ -1951,6 +2019,17 @@ class TextSelectionGestureDetectorBuilder {
19512019
/// callback.
19522020
@protected
19532021
void onSingleLongTapEnd(LongPressEndDetails details) {
2022+
switch (defaultTargetPlatform) {
2023+
case TargetPlatform.android:
2024+
case TargetPlatform.iOS:
2025+
editableText.hideMagnifier(shouldShowToolbar: false);
2026+
break;
2027+
case TargetPlatform.fuchsia:
2028+
case TargetPlatform.linux:
2029+
case TargetPlatform.macOS:
2030+
case TargetPlatform.windows:
2031+
break;
2032+
}
19542033
if (shouldShowSelectionToolbar) {
19552034
editableText.showToolbar();
19562035
}

packages/flutter/test/cupertino/text_field_test.dart

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6178,6 +6178,66 @@ void main() {
61786178
expect(find.byKey(fakeMagnifier.key!), findsNothing);
61796179
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
61806180

6181+
testWidgets('Can long press to show, unshow, and update magnifier', (WidgetTester tester) async {
6182+
final TextEditingController controller = TextEditingController();
6183+
final bool isTargetPlatformAndroid = defaultTargetPlatform == TargetPlatform.android;
6184+
await tester.pumpWidget(
6185+
CupertinoApp(
6186+
home: Center(
6187+
child: CupertinoTextField(
6188+
dragStartBehavior: DragStartBehavior.down,
6189+
controller: controller,
6190+
magnifierConfiguration: TextMagnifierConfiguration(
6191+
magnifierBuilder: (
6192+
_,
6193+
MagnifierController controller,
6194+
ValueNotifier<MagnifierOverlayInfoBearer> localInfoBearer
6195+
) {
6196+
infoBearer = localInfoBearer;
6197+
return fakeMagnifier;
6198+
},
6199+
),
6200+
),
6201+
),
6202+
),
6203+
);
6204+
6205+
const String testValue = 'abc def ghi';
6206+
await tester.enterText(find.byType(CupertinoTextField), testValue);
6207+
await tester.pumpAndSettle();
6208+
6209+
// Tap at 'e' to set the selection to position 5 on Android.
6210+
// Tap at 'e' to set the selection to the closest word edge, which is position 4 on iOS.
6211+
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
6212+
await tester.pumpAndSettle(const Duration(milliseconds: 300));
6213+
expect(controller.selection.isCollapsed, true);
6214+
expect(controller.selection.baseOffset, isTargetPlatformAndroid ? 5 : 4);
6215+
expect(find.byKey(fakeMagnifier.key!), findsNothing);
6216+
6217+
// Long press the 'e' to move the cursor in front of the 'e' and show the magnifier.
6218+
final TestGesture gesture = await tester.startGesture(textOffsetToPosition(tester, testValue.indexOf('e')));
6219+
await tester.pumpAndSettle(const Duration(milliseconds: 1000));
6220+
expect(controller.selection.baseOffset, 5);
6221+
expect(controller.selection.extentOffset, 5);
6222+
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
6223+
6224+
final Offset firstLongPressGesturePosition = infoBearer.value.globalGesturePosition;
6225+
6226+
// Move the gesture to 'h' to update the magnifier and move the cursor to 'h'.
6227+
await gesture.moveTo(textOffsetToPosition(tester, testValue.indexOf('h')));
6228+
await tester.pumpAndSettle();
6229+
expect(controller.selection.baseOffset, 9);
6230+
expect(controller.selection.extentOffset, 9);
6231+
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
6232+
// Expect the position the magnifier gets to have moved.
6233+
expect(firstLongPressGesturePosition, isNot(infoBearer.value.globalGesturePosition));
6234+
6235+
// End the long press to hide the magnifier.
6236+
await gesture.up();
6237+
await tester.pumpAndSettle();
6238+
expect(find.byKey(fakeMagnifier.key!), findsNothing);
6239+
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.iOS }));
6240+
61816241
group('TapRegion integration', () {
61826242
testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async {
61836243
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');

0 commit comments

Comments
 (0)