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 {
/// Builds the text selection handles, but desktop has none.
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) {
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, VoidCallback? onTap) {
return const SizedBox.shrink();
}
......
......@@ -247,7 +247,9 @@ class CupertinoTextSelectionControls extends TextSelectionControls {
/// Builder for iOS text selection edges.
@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
// padding in every direction that will constitute the selection drag area.
final Size desiredSize = getHandleSize(textLineHeight);
......
......@@ -53,7 +53,7 @@ class _DesktopTextSelectionControls extends TextSelectionControls {
/// Builds the text selection handles, but desktop has none.
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) {
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, VoidCallback? onTap) {
return const SizedBox.shrink();
}
......
......@@ -54,7 +54,7 @@ class MaterialTextSelectionControls extends TextSelectionControls {
/// Builder for material-style text selection handles.
@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 Color handleColor = TextSelectionTheme.of(context).selectionHandleColor ?? theme.colorScheme.primary;
final Widget handle = SizedBox(
......@@ -64,6 +64,10 @@ class MaterialTextSelectionControls extends TextSelectionControls {
painter: _TextSelectionHandlePainter(
color: handleColor,
),
child: GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.translucent,
),
),
);
......
......@@ -110,11 +110,16 @@ class ToolbarItemsParentData extends ContainerBoxParentData<RenderBox> {
///
/// Override text operations such as [handleCut] if needed.
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
/// 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
/// the point that is aligned with a specific point in the text. A handle
......@@ -401,11 +406,19 @@ class TextSelectionOverlay {
final DragStartBehavior dragStartBehavior;
/// {@template flutter.widgets.TextSelectionOverlay.onSelectionHandleTapped}
/// A callback that's invoked when a selection handle is tapped.
///
/// Both regular taps and long presses invoke this callback, but a drag
/// gesture won't.
/// A callback that's optionally invoked when a selection handle is tapped.
///
/// The [TextSelectionControls.buildHandle] implementation the text field
/// 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}
// See https://github.com/flutter/flutter/issues/39376#issuecomment-848406415
// for provenance.
final VoidCallback? onSelectionHandleTapped;
/// Maintains the status of the clipboard for determining if its contents can
......@@ -568,7 +581,8 @@ class TextSelectionOverlay {
}
Widget _buildHandle(BuildContext context, _TextSelectionHandlePosition position) {
Widget handle;
final Widget handle;
final TextSelectionControls? selectionControls = this.selectionControls;
if ((_selection.isCollapsed && position == _TextSelectionHandlePosition.end) ||
selectionControls == null)
handle = Container(); // hide the second handle when collapsed
......@@ -690,7 +704,7 @@ class _TextSelectionHandleOverlay extends StatefulWidget {
final RenderEditable renderObject;
final ValueChanged<TextSelection> onSelectionHandleChanged;
final VoidCallback? onSelectionHandleTapped;
final TextSelectionControls? selectionControls;
final TextSelectionControls selectionControls;
final DragStartBehavior dragStartBehavior;
@override
......@@ -747,7 +761,7 @@ class _TextSelectionHandleOverlayState
}
void _handleDragStart(DragStartDetails details) {
final Size handleSize = widget.selectionControls!.getHandleSize(
final Size handleSize = widget.selectionControls.getHandleSize(
widget.renderObject.preferredLineHeight,
);
_dragPosition = details.globalPosition + Offset(0.0, -handleSize.height);
......@@ -784,10 +798,6 @@ class _TextSelectionHandleOverlayState
widget.onSelectionHandleChanged(newSelection);
}
void _handleTap() {
widget.onSelectionHandleTapped?.call();
}
@override
Widget build(BuildContext context) {
final LayerLink layerLink;
......@@ -814,11 +824,11 @@ class _TextSelectionHandleOverlayState
break;
}
final Offset handleAnchor = widget.selectionControls!.getHandleAnchor(
final Offset handleAnchor = widget.selectionControls.getHandleAnchor(
type,
widget.renderObject.preferredLineHeight,
);
final Size handleSize = widget.selectionControls!.getHandleSize(
final Size handleSize = widget.selectionControls.getHandleSize(
widget.renderObject.preferredLineHeight,
);
......@@ -855,7 +865,6 @@ class _TextSelectionHandleOverlayState
dragStartBehavior: widget.dragStartBehavior,
onPanStart: _handleDragStart,
onPanUpdate: _handleDragUpdate,
onTap: _handleTap,
child: Padding(
padding: EdgeInsets.only(
left: padding.left,
......@@ -863,10 +872,11 @@ class _TextSelectionHandleOverlayState
right: padding.right,
bottom: padding.bottom,
),
child: widget.selectionControls!.buildHandle(
child: widget.selectionControls.buildHandle(
context,
type,
widget.renderObject.preferredLineHeight,
widget.onSelectionHandleTapped,
),
),
),
......@@ -1520,12 +1530,9 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
// Use _TransparentTapGestureRecognizer so that TextSelectionGestureDetector
// can receive the same tap events that a selection handle placed visually
// on top of it also receives.
gestures[_TransparentTapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<_TransparentTapGestureRecognizer>(
() => _TransparentTapGestureRecognizer(debugOwner: this),
(_TransparentTapGestureRecognizer instance) {
gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
instance
..onSecondaryTap = widget.onSecondaryTap
..onSecondaryTapDown = widget.onSecondaryTapDown
......@@ -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
/// the clipboard can be pasted.
///
......
......@@ -35,7 +35,7 @@ class MockClipboard {
class MockTextSelectionControls extends TextSelectionControls {
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) {
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, VoidCallback? onTap) {
throw UnimplementedError();
}
......
......@@ -156,6 +156,7 @@ void main() {
context,
TextSelectionHandleType.right,
10.0,
null,
),
),
),
......
......@@ -488,7 +488,7 @@ void main() {
await tester.tap(find.byKey(icon));
await tester.pump();
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 {
......@@ -9718,4 +9718,43 @@ void main() {
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() {
padding: const EdgeInsets.symmetric(horizontal: 250),
child: FittedBox(
child: materialTextSelectionControls.buildHandle(
context, TextSelectionHandleType.right, 10.0,
context, TextSelectionHandleType.right, 10.0, null,
),
),
),
......
......@@ -82,6 +82,7 @@ void main() {
context,
TextSelectionHandleType.left,
10.0,
null,
);
},
),
......@@ -129,6 +130,7 @@ void main() {
context,
TextSelectionHandleType.left,
10.0,
null,
);
},
),
......@@ -186,6 +188,7 @@ void main() {
context,
TextSelectionHandleType.left,
10.0,
null,
);
},
),
......
......@@ -706,7 +706,7 @@ void main() {
tester.renderObjectList<RenderBox>(
find.descendant(
of: find.byType(CompositedTransformFollower),
matching: find.byType(GestureDetector),
matching: find.byType(Padding),
),
),
);
......@@ -722,7 +722,7 @@ void main() {
tester.renderObjectList<RenderBox>(
find.descendant(
of: find.byType(CompositedTransformFollower),
matching: find.byType(GestureDetector),
matching: find.byType(Padding),
),
),
);
......@@ -3907,7 +3907,7 @@ void main() {
tester.renderObjectList<RenderBox>(
find.descendant(
of: find.byType(CompositedTransformFollower),
matching: find.byType(GestureDetector),
matching: find.byType(Padding),
),
),
);
......@@ -4027,7 +4027,7 @@ void main() {
tester.renderObjectList<RenderBox>(
find.descendant(
of: find.byType(CompositedTransformFollower),
matching: find.byType(GestureDetector),
matching: find.byType(Padding),
),
),
);
......@@ -5058,7 +5058,7 @@ void main() {
tester.renderObjectList<RenderBox>(
find.descendant(
of: find.byType(CompositedTransformFollower),
matching: find.byType(GestureDetector),
matching: find.byType(Padding),
),
),
);
......@@ -7569,7 +7569,7 @@ class MockTextSelectionControls extends Fake implements TextSelectionControls {
}
@override
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight) {
Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textLineHeight, VoidCallback? onTap) {
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