Unverified Commit 21273fde authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

[TextSelectionControls] move the tap gesture callback into...

[TextSelectionControls] move the tap gesture callback into _TextSelectionHandlePainter, remove `_TransparentTapGestureRecognizer`. (#83639)
parent 89a3c353
...@@ -67,7 +67,7 @@ class _CupertinoDesktopTextSelectionControls extends TextSelectionControls { ...@@ -67,7 +67,7 @@ class _CupertinoDesktopTextSelectionControls extends TextSelectionControls {
/// Builds the text selection handles, but desktop has none. /// Builds the text selection handles, but desktop has none.
@override @override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) { Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, VoidCallback? onTap) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
......
...@@ -247,7 +247,9 @@ class CupertinoTextSelectionControls extends TextSelectionControls { ...@@ -247,7 +247,9 @@ class CupertinoTextSelectionControls extends TextSelectionControls {
/// Builder for iOS text selection edges. /// Builder for iOS text selection edges.
@override @override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) { Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, VoidCallback? onTap) {
// iOS selection handles do not respond to taps.
// We want a size that's a vertical line the height of the text plus a 18.0 // We want a size that's a vertical line the height of the text plus a 18.0
// padding in every direction that will constitute the selection drag area. // padding in every direction that will constitute the selection drag area.
final Size desiredSize = getHandleSize(textLineHeight); final Size desiredSize = getHandleSize(textLineHeight);
......
...@@ -53,7 +53,7 @@ class _DesktopTextSelectionControls extends TextSelectionControls { ...@@ -53,7 +53,7 @@ class _DesktopTextSelectionControls extends TextSelectionControls {
/// Builds the text selection handles, but desktop has none. /// Builds the text selection handles, but desktop has none.
@override @override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) { Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, VoidCallback? onTap) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
......
...@@ -54,7 +54,7 @@ class MaterialTextSelectionControls extends TextSelectionControls { ...@@ -54,7 +54,7 @@ class MaterialTextSelectionControls extends TextSelectionControls {
/// Builder for material-style text selection handles. /// Builder for material-style text selection handles.
@override @override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight) { Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight, VoidCallback? onTap) {
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final Color handleColor = TextSelectionTheme.of(context).selectionHandleColor ?? theme.colorScheme.primary; final Color handleColor = TextSelectionTheme.of(context).selectionHandleColor ?? theme.colorScheme.primary;
final Widget handle = SizedBox( final Widget handle = SizedBox(
...@@ -64,6 +64,10 @@ class MaterialTextSelectionControls extends TextSelectionControls { ...@@ -64,6 +64,10 @@ class MaterialTextSelectionControls extends TextSelectionControls {
painter: _TextSelectionHandlePainter( painter: _TextSelectionHandlePainter(
color: handleColor, color: handleColor,
), ),
child: GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.translucent,
),
), ),
); );
......
...@@ -110,11 +110,16 @@ class ToolbarItemsParentData extends ContainerBoxParentData<RenderBox> { ...@@ -110,11 +110,16 @@ class ToolbarItemsParentData extends ContainerBoxParentData<RenderBox> {
/// ///
/// Override text operations such as [handleCut] if needed. /// Override text operations such as [handleCut] if needed.
abstract class TextSelectionControls { abstract class TextSelectionControls {
/// Builds a selection handle of the given type. /// Builds a selection handle of the given `type`.
/// ///
/// The top left corner of this widget is positioned at the bottom of the /// The top left corner of this widget is positioned at the bottom of the
/// selection position. /// selection position.
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight); ///
/// The supplied [onTap] should be invoked when the handle is tapped, if such
/// interaction is allowed. As a counterexample, the default selection handle
/// on iOS [cupertinoTextSelectionControls] does not call [onTap] at all,
/// since its handles are not meant to be tapped.
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, VoidCallback? onTap);
/// Get the anchor point of the handle relative to itself. The anchor point is /// Get the anchor point of the handle relative to itself. The anchor point is
/// the point that is aligned with a specific point in the text. A handle /// the point that is aligned with a specific point in the text. A handle
...@@ -401,11 +406,19 @@ class TextSelectionOverlay { ...@@ -401,11 +406,19 @@ class TextSelectionOverlay {
final DragStartBehavior dragStartBehavior; final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.TextSelectionOverlay.onSelectionHandleTapped} /// {@template flutter.widgets.TextSelectionOverlay.onSelectionHandleTapped}
/// A callback that's invoked when a selection handle is tapped. /// A callback that's optionally invoked when a selection handle is tapped.
/// ///
/// Both regular taps and long presses invoke this callback, but a drag /// The [TextSelectionControls.buildHandle] implementation the text field
/// gesture won't. /// uses decides where the the handle's tap "hotspot" is, or whether the
/// selection handle supports tap gestures at all. For instance,
/// [MaterialTextSelectionControls] calls [onSelectionHandleTapped] when the
/// selection handle's "knob" is tapped, while
/// [CupertinoTextSelectionControls] builds a handle that's not sufficiently
/// large for tapping (as it's not meant to be tapped) so it does not call
/// [onSelectionHandleTapped] even when tapped.
/// {@endtemplate} /// {@endtemplate}
// See https://github.com/flutter/flutter/issues/39376#issuecomment-848406415
// for provenance.
final VoidCallback? onSelectionHandleTapped; final VoidCallback? onSelectionHandleTapped;
/// Maintains the status of the clipboard for determining if its contents can /// Maintains the status of the clipboard for determining if its contents can
...@@ -568,7 +581,8 @@ class TextSelectionOverlay { ...@@ -568,7 +581,8 @@ class TextSelectionOverlay {
} }
Widget _buildHandle(BuildContext context, _TextSelectionHandlePosition position) { Widget _buildHandle(BuildContext context, _TextSelectionHandlePosition position) {
Widget handle; final Widget handle;
final TextSelectionControls? selectionControls = this.selectionControls;
if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) || if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) ||
selectionControls == null) selectionControls == null)
handle = Container(); // hide the second handle when collapsed handle = Container(); // hide the second handle when collapsed
...@@ -690,7 +704,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget { ...@@ -690,7 +704,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget {
final RenderEditable renderObject; final RenderEditable renderObject;
final ValueChanged<TextSelection> onSelectionHandleChanged; final ValueChanged<TextSelection> onSelectionHandleChanged;
final VoidCallback? onSelectionHandleTapped; final VoidCallback? onSelectionHandleTapped;
final TextSelectionControls? selectionControls; final TextSelectionControls selectionControls;
final DragStartBehavior dragStartBehavior; final DragStartBehavior dragStartBehavior;
@override @override
...@@ -747,7 +761,7 @@ class _TextSelectionHandleOverlayState ...@@ -747,7 +761,7 @@ class _TextSelectionHandleOverlayState
} }
void _handleDragStart(DragStartDetails details) { void _handleDragStart(DragStartDetails details) {
final Size handleSize = widget.selectionControls!.getHandleSize( final Size handleSize = widget.selectionControls.getHandleSize(
widget.renderObject.preferredLineHeight, widget.renderObject.preferredLineHeight,
); );
_dragPosition = details.globalPosition + Offset(0.0, -handleSize.height); _dragPosition = details.globalPosition + Offset(0.0, -handleSize.height);
...@@ -784,10 +798,6 @@ class _TextSelectionHandleOverlayState ...@@ -784,10 +798,6 @@ class _TextSelectionHandleOverlayState
widget.onSelectionHandleChanged(newSelection); widget.onSelectionHandleChanged(newSelection);
} }
void _handleTap() {
widget.onSelectionHandleTapped?.call();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final LayerLink layerLink; final LayerLink layerLink;
...@@ -814,11 +824,11 @@ class _TextSelectionHandleOverlayState ...@@ -814,11 +824,11 @@ class _TextSelectionHandleOverlayState
break; break;
} }
final Offset handleAnchor = widget.selectionControls!.getHandleAnchor( final Offset handleAnchor = widget.selectionControls.getHandleAnchor(
type, type,
widget.renderObject.preferredLineHeight, widget.renderObject.preferredLineHeight,
); );
final Size handleSize = widget.selectionControls!.getHandleSize( final Size handleSize = widget.selectionControls.getHandleSize(
widget.renderObject.preferredLineHeight, widget.renderObject.preferredLineHeight,
); );
...@@ -855,7 +865,6 @@ class _TextSelectionHandleOverlayState ...@@ -855,7 +865,6 @@ class _TextSelectionHandleOverlayState
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
onPanStart: _handleDragStart, onPanStart: _handleDragStart,
onPanUpdate: _handleDragUpdate, onPanUpdate: _handleDragUpdate,
onTap: _handleTap,
child: Padding( child: Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: padding.left, left: padding.left,
...@@ -863,10 +872,11 @@ class _TextSelectionHandleOverlayState ...@@ -863,10 +872,11 @@ class _TextSelectionHandleOverlayState
right: padding.right, right: padding.right,
bottom: padding.bottom, bottom: padding.bottom,
), ),
child: widget.selectionControls!.buildHandle( child: widget.selectionControls.buildHandle(
context, context,
type, type,
widget.renderObject.preferredLineHeight, widget.renderObject.preferredLineHeight,
widget.onSelectionHandleTapped,
), ),
), ),
), ),
...@@ -1520,12 +1530,9 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec ...@@ -1520,12 +1530,9 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{}; final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
// Use _TransparentTapGestureRecognizer so that TextSelectionGestureDetector gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
// can receive the same tap events that a selection handle placed visually () => TapGestureRecognizer(debugOwner: this),
// on top of it also receives. (TapGestureRecognizer instance) {
gestures[_TransparentTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<_TransparentTapGestureRecognizer>(
() => _TransparentTapGestureRecognizer(debugOwner: this),
(_TransparentTapGestureRecognizer instance) {
instance instance
..onSecondaryTap = widget.onSecondaryTap ..onSecondaryTap = widget.onSecondaryTap
..onSecondaryTapDown = widget.onSecondaryTapDown ..onSecondaryTapDown = widget.onSecondaryTapDown
...@@ -1588,35 +1595,6 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec ...@@ -1588,35 +1595,6 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
} }
} }
// A TapGestureRecognizer which allows other GestureRecognizers to win in the
// GestureArena. This means both _TransparentTapGestureRecognizer and other
// GestureRecognizers can handle the same event.
//
// This enables proper handling of events on both the selection handle and the
// underlying input, since there is significant overlap between the two given
// the handle's padded hit area. For example, the selection handle needs to
// handle single taps on itself, but double taps need to be handled by the
// underlying input.
class _TransparentTapGestureRecognizer extends TapGestureRecognizer {
_TransparentTapGestureRecognizer({
Object? debugOwner,
}) : super(debugOwner: debugOwner);
@override
void rejectGesture(int pointer) {
// Accept new gestures that another recognizer has already won.
// Specifically, this needs to accept taps on the text selection handle on
// behalf of the text field in order to handle double tap to select. It must
// not accept other gestures like longpresses and drags that end outside of
// the text field.
if (state == GestureRecognizerState.ready) {
acceptGesture(pointer);
} else {
super.rejectGesture(pointer);
}
}
}
/// A [ValueNotifier] whose [value] indicates whether the current contents of /// A [ValueNotifier] whose [value] indicates whether the current contents of
/// the clipboard can be pasted. /// the clipboard can be pasted.
/// ///
......
...@@ -35,7 +35,7 @@ class MockClipboard { ...@@ -35,7 +35,7 @@ class MockClipboard {
class MockTextSelectionControls extends TextSelectionControls { class MockTextSelectionControls extends TextSelectionControls {
@override @override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) { Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, VoidCallback? onTap) {
throw UnimplementedError(); throw UnimplementedError();
} }
......
...@@ -156,6 +156,7 @@ void main() { ...@@ -156,6 +156,7 @@ void main() {
context, context,
TextSelectionHandleType.right, TextSelectionHandleType.right,
10.0, 10.0,
null,
), ),
), ),
), ),
......
...@@ -488,7 +488,7 @@ void main() { ...@@ -488,7 +488,7 @@ void main() {
await tester.tap(find.byKey(icon)); await tester.tap(find.byKey(icon));
await tester.pump(); await tester.pump();
expect(controller.text, ''); expect(controller.text, '');
expect(controller.selection, const TextSelection.collapsed(offset: 0, affinity: TextAffinity.upstream)); expect(controller.selection, const TextSelection.collapsed(offset: 0, affinity: TextAffinity.downstream));
}); });
testWidgets('Cursor radius is 2.0', (WidgetTester tester) async { testWidgets('Cursor radius is 2.0', (WidgetTester tester) async {
...@@ -9718,4 +9718,43 @@ void main() { ...@@ -9718,4 +9718,43 @@ void main() {
expect(state.currentTextEditingValue.composing, TextRange.empty); expect(state.currentTextEditingValue.composing, TextRange.empty);
}); });
}); });
testWidgets('prefix/suffix buttons do not leak touch events', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/39376.
int textFieldTapCount = 0;
int prefixTapCount = 0;
int suffixTapCount = 0;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: TextField(
onTap: () { textFieldTapCount += 1; },
decoration: InputDecoration(
labelText: 'Label',
prefix: RaisedButton(
onPressed: () { prefixTapCount += 1; },
child: const Text('prefix'),
),
suffix: RaisedButton(
onPressed: () { suffixTapCount += 1; },
child: const Text('suffix'),
),
),
),
),
),
);
await tester.tap(find.text('prefix'));
expect(textFieldTapCount, 0);
expect(prefixTapCount, 1);
expect(suffixTapCount, 0);
await tester.tap(find.text('suffix'));
expect(textFieldTapCount, 0);
expect(prefixTapCount, 1);
expect(suffixTapCount, 1);
});
} }
...@@ -566,7 +566,7 @@ void main() { ...@@ -566,7 +566,7 @@ void main() {
padding: const EdgeInsets.symmetric(horizontal: 250), padding: const EdgeInsets.symmetric(horizontal: 250),
child: FittedBox( child: FittedBox(
child: materialTextSelectionControls.buildHandle( child: materialTextSelectionControls.buildHandle(
context, TextSelectionHandleType.right, 10.0, context, TextSelectionHandleType.right, 10.0, null,
), ),
), ),
), ),
......
...@@ -82,6 +82,7 @@ void main() { ...@@ -82,6 +82,7 @@ void main() {
context, context,
TextSelectionHandleType.left, TextSelectionHandleType.left,
10.0, 10.0,
null,
); );
}, },
), ),
...@@ -129,6 +130,7 @@ void main() { ...@@ -129,6 +130,7 @@ void main() {
context, context,
TextSelectionHandleType.left, TextSelectionHandleType.left,
10.0, 10.0,
null,
); );
}, },
), ),
...@@ -186,6 +188,7 @@ void main() { ...@@ -186,6 +188,7 @@ void main() {
context, context,
TextSelectionHandleType.left, TextSelectionHandleType.left,
10.0, 10.0,
null,
); );
}, },
), ),
......
...@@ -706,7 +706,7 @@ void main() { ...@@ -706,7 +706,7 @@ void main() {
tester.renderObjectList<RenderBox>( tester.renderObjectList<RenderBox>(
find.descendant( find.descendant(
of: find.byType(CompositedTransformFollower), of: find.byType(CompositedTransformFollower),
matching: find.byType(GestureDetector), matching: find.byType(Padding),
), ),
), ),
); );
...@@ -722,7 +722,7 @@ void main() { ...@@ -722,7 +722,7 @@ void main() {
tester.renderObjectList<RenderBox>( tester.renderObjectList<RenderBox>(
find.descendant( find.descendant(
of: find.byType(CompositedTransformFollower), of: find.byType(CompositedTransformFollower),
matching: find.byType(GestureDetector), matching: find.byType(Padding),
), ),
), ),
); );
...@@ -3907,7 +3907,7 @@ void main() { ...@@ -3907,7 +3907,7 @@ void main() {
tester.renderObjectList<RenderBox>( tester.renderObjectList<RenderBox>(
find.descendant( find.descendant(
of: find.byType(CompositedTransformFollower), of: find.byType(CompositedTransformFollower),
matching: find.byType(GestureDetector), matching: find.byType(Padding),
), ),
), ),
); );
...@@ -4027,7 +4027,7 @@ void main() { ...@@ -4027,7 +4027,7 @@ void main() {
tester.renderObjectList<RenderBox>( tester.renderObjectList<RenderBox>(
find.descendant( find.descendant(
of: find.byType(CompositedTransformFollower), of: find.byType(CompositedTransformFollower),
matching: find.byType(GestureDetector), matching: find.byType(Padding),
), ),
), ),
); );
...@@ -5058,7 +5058,7 @@ void main() { ...@@ -5058,7 +5058,7 @@ void main() {
tester.renderObjectList<RenderBox>( tester.renderObjectList<RenderBox>(
find.descendant( find.descendant(
of: find.byType(CompositedTransformFollower), of: find.byType(CompositedTransformFollower),
matching: find.byType(GestureDetector), matching: find.byType(Padding),
), ),
), ),
); );
...@@ -7569,7 +7569,7 @@ class MockTextSelectionControls extends Fake implements TextSelectionControls { ...@@ -7569,7 +7569,7 @@ class MockTextSelectionControls extends Fake implements TextSelectionControls {
} }
@override @override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) { Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, VoidCallback? onTap) {
return Container(); return Container();
} }
......
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