From 40b4bc996c32c9705c78db2ab168ceebb0012acd Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Tue, 20 Jun 2023 16:36:45 -0700 Subject: [PATCH] Fix: Magnifier appears and won't dismiss (#128545) Fixes a bug when tapping near certain TextFields. --- .../lib/src/widgets/text_selection.dart | 48 ++++++++++++++- .../test/material/text_field_test.dart | 58 +++++++++++++++++-- 2 files changed, 98 insertions(+), 8 deletions(-) diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index 33d0e9b518..6c58a6da8c 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -1088,10 +1088,26 @@ class SelectionOverlay { void _handleStartHandleDragStart(DragStartDetails details) { assert(!_isDraggingStartHandle); + // Calling OverlayEntry.remove may not happen until the following frame, so + // it's possible for the handles to receive a gesture after calling remove. + if (_handles == null) { + _isDraggingStartHandle = false; + return; + } _isDraggingStartHandle = details.kind == PointerDeviceKind.touch; onStartHandleDragStart?.call(details); } + void _handleStartHandleDragUpdate(DragUpdateDetails details) { + // Calling OverlayEntry.remove may not happen until the following frame, so + // it's possible for the handles to receive a gesture after calling remove. + if (_handles == null) { + _isDraggingStartHandle = false; + return; + } + onStartHandleDragUpdate?.call(details); + } + /// Called when the users drag the start selection handles to new locations. final ValueChanged? onStartHandleDragUpdate; @@ -1101,6 +1117,11 @@ class SelectionOverlay { void _handleStartHandleDragEnd(DragEndDetails details) { _isDraggingStartHandle = false; + // Calling OverlayEntry.remove may not happen until the following frame, so + // it's possible for the handles to receive a gesture after calling remove. + if (_handles == null) { + return; + } onStartHandleDragEnd?.call(details); } @@ -1147,10 +1168,26 @@ class SelectionOverlay { void _handleEndHandleDragStart(DragStartDetails details) { assert(!_isDraggingEndHandle); + // Calling OverlayEntry.remove may not happen until the following frame, so + // it's possible for the handles to receive a gesture after calling remove. + if (_handles == null) { + _isDraggingEndHandle = false; + return; + } _isDraggingEndHandle = details.kind == PointerDeviceKind.touch; onEndHandleDragStart?.call(details); } + void _handleEndHandleDragUpdate(DragUpdateDetails details) { + // Calling OverlayEntry.remove may not happen until the following frame, so + // it's possible for the handles to receive a gesture after calling remove. + if (_handles == null) { + _isDraggingEndHandle = false; + return; + } + onEndHandleDragUpdate?.call(details); + } + /// Called when the users drag the end selection handles to new locations. final ValueChanged? onEndHandleDragUpdate; @@ -1160,6 +1197,11 @@ class SelectionOverlay { void _handleEndHandleDragEnd(DragEndDetails details) { _isDraggingEndHandle = false; + // Calling OverlayEntry.remove may not happen until the following frame, so + // it's possible for the handles to receive a gesture after calling remove. + if (_handles == null) { + return; + } onEndHandleDragEnd?.call(details); } @@ -1472,7 +1514,7 @@ class SelectionOverlay { handleLayerLink: startHandleLayerLink, onSelectionHandleTapped: onSelectionHandleTapped, onSelectionHandleDragStart: _handleStartHandleDragStart, - onSelectionHandleDragUpdate: onStartHandleDragUpdate, + onSelectionHandleDragUpdate: _handleStartHandleDragUpdate, onSelectionHandleDragEnd: _handleStartHandleDragEnd, selectionControls: selectionControls, visibility: startHandlesVisible, @@ -1499,7 +1541,7 @@ class SelectionOverlay { handleLayerLink: endHandleLayerLink, onSelectionHandleTapped: onSelectionHandleTapped, onSelectionHandleDragStart: _handleEndHandleDragStart, - onSelectionHandleDragUpdate: onEndHandleDragUpdate, + onSelectionHandleDragUpdate: _handleEndHandleDragUpdate, onSelectionHandleDragEnd: _handleEndHandleDragEnd, selectionControls: selectionControls, visibility: endHandlesVisible, @@ -1752,7 +1794,7 @@ class _SelectionHandleOverlayState extends State<_SelectionHandleOverlay> with S // Make sure the GestureDetector is big enough to be easily interactive. final Rect interactiveRect = handleRect.expandToInclude( - Rect.fromCircle(center: handleRect.center, radius: kMinInteractiveDimension/ 2), + Rect.fromCircle(center: handleRect.center, radius: kMinInteractiveDimension / 2), ); final RelativeRect padding = RelativeRect.fromLTRB( math.max((interactiveRect.width - handleRect.width) / 2, 0), diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 08026d59b4..01df2af483 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -15803,7 +15803,7 @@ void main() { ); final TextField textField = TextField( magnifierConfiguration: TextMagnifierConfiguration( - magnifierBuilder: (_, __, ___) => customMagnifier, + magnifierBuilder: (BuildContext context, MagnifierController controller, ValueNotifier? info) => customMagnifier, ), ); @@ -15901,7 +15901,7 @@ void main() { controller: controller, magnifierConfiguration: TextMagnifierConfiguration( magnifierBuilder: ( - _, + BuildContext context, MagnifierController controller, ValueNotifier localMagnifierInfo ) { @@ -15965,7 +15965,7 @@ void main() { controller: controller, magnifierConfiguration: TextMagnifierConfiguration( magnifierBuilder: ( - _, + BuildContext context, MagnifierController controller, ValueNotifier localMagnifierInfo ) { @@ -16067,7 +16067,7 @@ void main() { controller: controller, magnifierConfiguration: TextMagnifierConfiguration( magnifierBuilder: ( - _, + BuildContext context, MagnifierController controller, ValueNotifier localMagnifierInfo ) { @@ -16118,6 +16118,54 @@ void main() { await tester.pumpAndSettle(); expect(find.byKey(fakeMagnifier.key!), findsNothing); }, variant: const TargetPlatformVariant({ TargetPlatform.android, TargetPlatform.iOS })); + + testWidgets('magnifier does not show when tapping outside field', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/128321 + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Padding( + padding: const EdgeInsets.all(20), + child: TextField( + magnifierConfiguration: TextMagnifierConfiguration( + magnifierBuilder: ( + BuildContext context, + MagnifierController controller, + ValueNotifier localMagnifierInfo + ) { + magnifierInfo = localMagnifierInfo; + return fakeMagnifier; + }, + ), + onTapOutside: (PointerDownEvent event) { + FocusManager.instance.primaryFocus?.unfocus(); + } + ), + ), + ), + ), + ); + + await tester.tapAt( + tester.getCenter(find.byType(TextField)), + ); + await tester.pump(); + + expect(find.byKey(fakeMagnifier.key!), findsNothing); + + final TestGesture gesture = await tester.startGesture( + tester.getBottomLeft(find.byType(TextField)) - const Offset(10.0, 20.0), + ); + await tester.pump(); + expect(find.byKey(fakeMagnifier.key!), findsNothing); + await gesture.up(); + await tester.pump(); + + expect(find.byKey(fakeMagnifier.key!), findsNothing); + }, + skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. + variant: const TargetPlatformVariant({ TargetPlatform.android }), + ); }); group('TapRegion integration', () { @@ -16513,7 +16561,7 @@ class _ObscureTextTestWidgetState extends State<_ObscureTextTestWidget> { return MaterialApp( home: Scaffold( body: Builder( - builder: (_) { + builder: (BuildContext context) { return Column( children: [ TextField(