Unverified Commit fc7bd6ad authored by chunhtai's avatar chunhtai Committed by GitHub

refactor out selection handlers (#35207)

parent e91822da
...@@ -68,6 +68,39 @@ enum OverlayVisibilityMode { ...@@ -68,6 +68,39 @@ enum OverlayVisibilityMode {
always, always,
} }
class _CupertinoTextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder {
_CupertinoTextFieldSelectionGestureDetectorBuilder({
@required _CupertinoTextFieldState state
}) : _state = state,
super(delegate: state);
final _CupertinoTextFieldState _state;
@override
void onSingleTapUp(TapUpDetails details) {
// Because TextSelectionGestureDetector listens to taps that happen on
// widgets in front of it, tapping the clear button will also trigger
// this handler. If the the clear button widget recognizes the up event,
// then do not handle it.
if (_state._clearGlobalKey.currentContext != null) {
final RenderBox renderBox = _state._clearGlobalKey.currentContext.findRenderObject();
final Offset localOffset = renderBox.globalToLocal(details.globalPosition);
if (renderBox.hitTest(BoxHitTestResult(), position: localOffset)) {
return;
}
}
super.onSingleTapUp(details);
_state._requestKeyboard();
if (_state.widget.onTap != null)
_state.widget.onTap();
}
@override
void onDragSelectionEnd(DragEndDetails details) {
_state._requestKeyboard();
}
}
/// An iOS-style text field. /// An iOS-style text field.
/// ///
/// A text field lets the user enter text, either with a hardware keyboard or with /// A text field lets the user enter text, either with a hardware keyboard or with
...@@ -506,9 +539,8 @@ class CupertinoTextField extends StatefulWidget { ...@@ -506,9 +539,8 @@ class CupertinoTextField extends StatefulWidget {
} }
} }
class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticKeepAliveClientMixin { class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticKeepAliveClientMixin implements TextSelectionGestureDetectorBuilderDelegate {
final GlobalKey _clearGlobalKey = GlobalKey(); final GlobalKey _clearGlobalKey = GlobalKey();
final GlobalKey<EditableTextState> _editableTextKey = GlobalKey<EditableTextState>();
TextEditingController _controller; TextEditingController _controller;
TextEditingController get _effectiveController => widget.controller ?? _controller; TextEditingController get _effectiveController => widget.controller ?? _controller;
...@@ -516,17 +548,25 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -516,17 +548,25 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
FocusNode _focusNode; FocusNode _focusNode;
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
// The selection overlay should only be shown when the user is interacting
// through a touch screen (via either a finger or a stylus). A mouse shouldn't
// trigger the selection overlay.
// For backwards-compatibility, we treat a null kind the same as touch.
bool _shouldShowSelectionToolbar = true;
bool _showSelectionHandles = false; bool _showSelectionHandles = false;
_CupertinoTextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
// API for TextSelectionGestureDetectorBuilderDelegate.
@override
bool get forcePressEnabled => true;
@override
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
@override
bool get selectionEnabled => widget.selectionEnabled;
// End of API for TextSelectionGestureDetectorBuilderDelegate.
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_selectionGestureDetectorBuilder = _CupertinoTextFieldSelectionGestureDetectorBuilder(state: this);
if (widget.controller == null) { if (widget.controller == null) {
_controller = TextEditingController(); _controller = TextEditingController();
_controller.addListener(updateKeepAlive); _controller.addListener(updateKeepAlive);
...@@ -556,103 +596,16 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -556,103 +596,16 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
super.dispose(); super.dispose();
} }
EditableTextState get _editableText => _editableTextKey.currentState; EditableTextState get _editableText => editableTextKey.currentState;
void _requestKeyboard() { void _requestKeyboard() {
_editableText?.requestKeyboard(); _editableText?.requestKeyboard();
} }
RenderEditable get _renderEditable => _editableText.renderEditable;
void _handleTapDown(TapDownDetails details) {
_renderEditable.handleTapDown(details);
// The selection overlay should only be shown when the user is interacting
// through a touch screen (via either a finger or a stylus). A mouse shouldn't
// trigger the selection overlay.
// For backwards-compatibility, we treat a null kind the same as touch.
final PointerDeviceKind kind = details.kind;
_shouldShowSelectionToolbar =
kind == null ||
kind == PointerDeviceKind.touch ||
kind == PointerDeviceKind.stylus;
}
void _handleForcePressStarted(ForcePressDetails details) {
if (widget.selectionEnabled) {
_renderEditable.selectWordsInRange(
from: details.globalPosition,
cause: SelectionChangedCause.forcePress,
);
}
}
void _handleForcePressEnded(ForcePressDetails details) {
_renderEditable.selectWordsInRange(
from: details.globalPosition,
cause: SelectionChangedCause.forcePress,
);
if (_shouldShowSelectionToolbar)
_editableText.showToolbar();
}
void _handleSingleTapUp(TapUpDetails details) {
// Because TextSelectionGestureDetector listens to taps that happen on
// widgets in front of it, tapping the clear button will also trigger
// this handler. If the the clear button widget recognizes the up event,
// then do not handle it.
if (_clearGlobalKey.currentContext != null) {
final RenderBox renderBox = _clearGlobalKey.currentContext.findRenderObject();
final Offset localOffset = renderBox.globalToLocal(details.globalPosition);
if(renderBox.hitTest(BoxHitTestResult(), position: localOffset)) {
return;
}
}
if (widget.selectionEnabled) {
_renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
}
_requestKeyboard();
if (widget.onTap != null) {
widget.onTap();
}
}
void _handleSingleLongTapStart(LongPressStartDetails details) {
if (widget.selectionEnabled) {
_renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.longPress,
);
}
}
void _handleSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
if (widget.selectionEnabled) {
_renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.longPress,
);
}
}
void _handleSingleLongTapEnd(LongPressEndDetails details) {
if (_shouldShowSelectionToolbar)
_editableText.showToolbar();
}
void _handleDoubleTapDown(TapDownDetails details) {
if (widget.selectionEnabled) {
_renderEditable.selectWord(cause: SelectionChangedCause.tap);
if (_shouldShowSelectionToolbar)
_editableText.showToolbar();
}
}
bool _shouldShowSelectionHandles(SelectionChangedCause cause) { bool _shouldShowSelectionHandles(SelectionChangedCause cause) {
// When the text field is activated by something that doesn't trigger the // When the text field is activated by something that doesn't trigger the
// selection overlay, we shouldn't show the handles either. // selection overlay, we shouldn't show the handles either.
if (!_shouldShowSelectionToolbar) if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar)
return false; return false;
// On iOS, we don't show handles when the selection is collapsed. // On iOS, we don't show handles when the selection is collapsed.
...@@ -668,28 +621,6 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -668,28 +621,6 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
return false; return false;
} }
void _handleMouseDragSelectionStart(DragStartDetails details) {
_renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
}
void _handleMouseDragSelectionUpdate(
DragStartDetails startDetails,
DragUpdateDetails updateDetails,
) {
_renderEditable.selectPositionAt(
from: startDetails.globalPosition,
to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
}
void _handleMouseDragSelectionEnd(DragEndDetails details) {
_requestKeyboard();
}
void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) { void _handleSelectionChanged(TextSelection selection, SelectionChangedCause cause) {
if (cause == SelectionChangedCause.longPress) { if (cause == SelectionChangedCause.longPress) {
_editableText?.bringIntoView(selection.base); _editableText?.bringIntoView(selection.base);
...@@ -870,7 +801,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -870,7 +801,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
padding: widget.padding, padding: widget.padding,
child: RepaintBoundary( child: RepaintBoundary(
child: EditableText( child: EditableText(
key: _editableTextKey, key: editableTextKey,
controller: controller, controller: controller,
readOnly: widget.readOnly, readOnly: widget.readOnly,
showCursor: widget.showCursor, showCursor: widget.showCursor,
...@@ -925,18 +856,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK ...@@ -925,18 +856,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
ignoring: !enabled, ignoring: !enabled,
child: Container( child: Container(
decoration: effectiveDecoration, decoration: effectiveDecoration,
child: TextSelectionGestureDetector( child: _selectionGestureDetectorBuilder.buildGestureDetector(
onTapDown: _handleTapDown,
onForcePressStart: _handleForcePressStarted,
onForcePressEnd: _handleForcePressEnded,
onSingleTapUp: _handleSingleTapUp,
onSingleLongTapStart: _handleSingleLongTapStart,
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
onSingleLongTapEnd: _handleSingleLongTapEnd,
onDoubleTapDown: _handleDoubleTapDown,
onDragSelectionStart: _handleMouseDragSelectionStart,
onDragSelectionUpdate: _handleMouseDragSelectionUpdate,
onDragSelectionEnd: _handleMouseDragSelectionEnd,
behavior: HitTestBehavior.translucent, behavior: HitTestBehavior.translucent,
child: Align( child: Align(
alignment: Alignment(-1.0, _textAlignVertical.y), alignment: Alignment(-1.0, _textAlignVertical.y),
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart' show PointerDeviceKind; import 'package:flutter/gestures.dart' show PointerDeviceKind;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
void main() { void main() {
int tapCount; int tapCount;
...@@ -62,6 +64,30 @@ void main() { ...@@ -62,6 +64,30 @@ void main() {
); );
} }
Future<void> pumpTextSelectionGestureDetectorBuilder(
WidgetTester tester, {
bool forcePressEnabled = true,
bool selectionEnabled = true,
}) async {
final GlobalKey<EditableTextState> editableTextKey = GlobalKey<EditableTextState>();
final FakeTextSelectionGestureDetectorBuilderDelegate delegate = FakeTextSelectionGestureDetectorBuilderDelegate(
editableTextKey: editableTextKey,
forcePressEnabled: forcePressEnabled,
selectionEnabled: selectionEnabled,
);
final TextSelectionGestureDetectorBuilder provider =
TextSelectionGestureDetectorBuilder(delegate: delegate);
await tester.pumpWidget(
MaterialApp(
home: provider.buildGestureDetector(
behavior: HitTestBehavior.translucent,
child: FakeEditableText(key: editableTextKey)
)
)
);
}
testWidgets('a series of taps all call onTaps', (WidgetTester tester) async { testWidgets('a series of taps all call onTaps', (WidgetTester tester) async {
await pumpGestureDetector(tester); await pumpGestureDetector(tester);
await tester.tapAt(const Offset(200, 200)); await tester.tapAt(const Offset(200, 200));
...@@ -380,4 +406,221 @@ void main() { ...@@ -380,4 +406,221 @@ void main() {
await gesture.removePointer(); await gesture.removePointer();
}); });
testWidgets('test TextSelectionGestureDetectorBuilder long press', (WidgetTester tester) async {
await pumpTextSelectionGestureDetectorBuilder(tester);
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pumpAndSettle();
final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
expect(state.showToolbarCalled, isTrue);
expect(renderEditable.selectPositionAtCalled, isTrue);
});
testWidgets('test TextSelectionGestureDetectorBuilder tap', (WidgetTester tester) async {
await pumpTextSelectionGestureDetectorBuilder(tester);
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch);
await gesture.up();
await tester.pumpAndSettle();
final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
expect(state.showToolbarCalled, isFalse);
expect(renderEditable.selectWordEdgeCalled, isTrue);
});
testWidgets('test TextSelectionGestureDetectorBuilder double tap', (WidgetTester tester) async {
await pumpTextSelectionGestureDetectorBuilder(tester);
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch);
await tester.pump(const Duration(milliseconds: 50));
await gesture.up();
await gesture.down(const Offset(200.0, 200.0));
await tester.pump(const Duration(milliseconds: 50));
await gesture.up();
await tester.pumpAndSettle();
final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
expect(state.showToolbarCalled, isTrue);
expect(renderEditable.selectWordCalled, isTrue);
});
testWidgets('test TextSelectionGestureDetectorBuilder forcePress enabled', (WidgetTester tester) async {
await pumpTextSelectionGestureDetectorBuilder(tester);
final TestGesture gesture = await tester.createGesture();
await gesture.downWithCustomEvent(
const Offset(200.0, 200.0),
const PointerDownEvent(
pointer: 0,
position: Offset(200.0, 200.0),
pressure: 3.0,
pressureMax: 6.0,
pressureMin: 0.0,
),
);
await gesture.updateWithCustomEvent(
const PointerUpEvent(
pointer: 0,
position: Offset(200.0, 200.0),
pressure: 0.0,
pressureMax: 6.0,
pressureMin: 0.0,
),
);
await tester.pump();
final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
expect(state.showToolbarCalled, isTrue);
expect(renderEditable.selectWordsInRangeCalled, isTrue);
});
testWidgets('test TextSelectionGestureDetectorBuilder selection disabled', (WidgetTester tester) async {
await pumpTextSelectionGestureDetectorBuilder(tester, selectionEnabled: false);
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0), pointer: 0, kind: PointerDeviceKind.touch);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pumpAndSettle();
final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
expect(state.showToolbarCalled, isTrue);
expect(renderEditable.selectWordsInRangeCalled, isFalse);
});
testWidgets('test TextSelectionGestureDetectorBuilder forcePress disabled', (WidgetTester tester) async {
await pumpTextSelectionGestureDetectorBuilder(tester, forcePressEnabled: false);
final TestGesture gesture = await tester.createGesture();
await gesture.downWithCustomEvent(
const Offset(200.0, 200.0),
const PointerDownEvent(
pointer: 0,
position: Offset(200.0, 200.0),
pressure: 3.0,
pressureMax: 6.0,
pressureMin: 0.0,
),
);
await gesture.up();
await tester.pump();
final FakeEditableTextState state = tester.state(find.byType(FakeEditableText));
final FakeRenderEditable renderEditable = tester.renderObject(find.byType(FakeEditable));
expect(state.showToolbarCalled, isFalse);
expect(renderEditable.selectWordsInRangeCalled, isFalse);
});
}
class FakeTextSelectionGestureDetectorBuilderDelegate implements TextSelectionGestureDetectorBuilderDelegate {
FakeTextSelectionGestureDetectorBuilderDelegate({
this.editableTextKey,
this.forcePressEnabled,
this.selectionEnabled,
});
@override
final GlobalKey<EditableTextState> editableTextKey;
@override
final bool forcePressEnabled;
@override
final bool selectionEnabled;
}
class FakeEditableText extends EditableText {
FakeEditableText({Key key}): super(
key: key,
controller: TextEditingController(),
focusNode: FocusNode(),
backgroundCursorColor: Colors.white,
cursorColor: Colors.white,
style: const TextStyle(),
);
@override
FakeEditableTextState createState() => FakeEditableTextState();
}
class FakeEditableTextState extends EditableTextState {
final GlobalKey _editableKey = GlobalKey();
bool showToolbarCalled = false;
@override
RenderEditable get renderEditable => _editableKey.currentContext.findRenderObject();
@override
bool showToolbar() {
showToolbarCalled = true;
return true;
}
@override
Widget build(BuildContext context) {
super.build(context);
return FakeEditable(this, key: _editableKey);
}
}
class FakeEditable extends LeafRenderObjectWidget {
const FakeEditable(
this.delegate, {
Key key,
}) : super(key: key);
final EditableTextState delegate;
@override
RenderEditable createRenderObject(BuildContext context) {
return FakeRenderEditable(delegate);
}
}
class FakeRenderEditable extends RenderEditable {
FakeRenderEditable(EditableTextState delegate) : super(
text: const TextSpan(
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
text: 'placeholder',
),
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
textAlign: TextAlign.start,
textDirection: TextDirection.ltr,
locale: const Locale('en', 'US'),
offset: ViewportOffset.fixed(10.0),
textSelectionDelegate: delegate,
selection: const TextSelection.collapsed(
offset: 0,
),
);
bool selectWordsInRangeCalled = false;
@override
void selectWordsInRange({ @required Offset from, Offset to, @required SelectionChangedCause cause }) {
selectWordsInRangeCalled = true;
}
bool selectWordEdgeCalled = false;
@override
void selectWordEdge({ @required SelectionChangedCause cause }) {
selectWordEdgeCalled = true;
}
bool selectPositionAtCalled = false;
@override
void selectPositionAt({ @required Offset from, Offset to, @required SelectionChangedCause cause }) {
selectPositionAtCalled = true;
}
bool selectWordCalled = false;
@override
void selectWord({ @required SelectionChangedCause cause }) {
selectWordCalled = true;
}
} }
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