Text selection via mouse (#28290)

This commit is contained in:
Mouad Debbar 2019-03-01 13:57:10 -08:00 committed by GitHub
parent 1df28e8b7a
commit b94bf87c70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 833 additions and 51 deletions

View file

@ -12,8 +12,13 @@ import 'recognizer.dart';
/// all touch events inside the view bounds to the embedded Android view.
/// See [AndroidView.gestureRecognizers] for more details.
class EagerGestureRecognizer extends OneSequenceGestureRecognizer {
/// Create an eager gesture recognizer.
///
/// {@macro flutter.gestures.gestureRecognizer.kind}
EagerGestureRecognizer({ PointerDeviceKind kind }): super(kind: kind);
@override
void addPointer(PointerDownEvent event) {
void addAllowedPointer(PointerDownEvent event) {
// We call startTrackingPointer as this is where OneSequenceGestureRecognizer joins the arena.
startTrackingPointer(event.pointer);
resolve(GestureDisposition.accepted);

View file

@ -116,16 +116,19 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
/// The [interpolation] callback must always return a value in the range 0.0
/// to 1.0 for values of `pressure` that are between `pressureMin` and
/// `pressureMax`.
///
/// {@macro flutter.gestures.gestureRecognizer.kind}
ForcePressGestureRecognizer({
this.startPressure = 0.4,
this.peakPressure = 0.85,
this.interpolation = _inverseLerp,
Object debugOwner,
PointerDeviceKind kind,
}) : assert(startPressure != null),
assert(peakPressure != null),
assert(interpolation != null),
assert(peakPressure > startPressure),
super(debugOwner: debugOwner);
super(debugOwner: debugOwner, kind: kind);
/// A pointer is in contact with the screen and has just pressed with a force
/// exceeding the [startPressure]. Consequently, if there were other gesture
@ -205,7 +208,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
_ForceState _state = _ForceState.ready;
@override
void addPointer(PointerEvent event) {
void addAllowedPointer(PointerEvent event) {
// If the device has a maximum pressure of less than or equal to 1, it
// doesn't have touch pressure sensing capabilities. Do not participate
// in the gesture arena.

View file

@ -123,10 +123,12 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
/// can be moved without limit once the long press is accepted.
LongPressGestureRecognizer({
double postAcceptSlopTolerance,
PointerDeviceKind kind,
Object debugOwner,
}) : super(
deadline: kLongPressTimeout,
postAcceptSlopTolerance: postAcceptSlopTolerance,
kind: kind,
debugOwner: debugOwner,
);

View file

@ -52,11 +52,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// Initialize the object.
///
/// [dragStartBehavior] must not be null.
///
/// {@macro flutter.gestures.gestureRecognizer.kind}
DragGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
this.dragStartBehavior = DragStartBehavior.start,
}) : assert(dragStartBehavior != null),
super(debugOwner: debugOwner);
super(debugOwner: debugOwner, kind: kind);
/// Configure the behavior of offsets sent to [onStart].
///
@ -147,7 +150,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
@override
void addPointer(PointerEvent event) {
void addAllowedPointer(PointerEvent event) {
startTrackingPointer(event.pointer);
_velocityTrackers[event.pointer] = VelocityTracker();
if (_state == _DragState.ready) {
@ -296,7 +299,12 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// track each touch point independently.
class VerticalDragGestureRecognizer extends DragGestureRecognizer {
/// Create a gesture recognizer for interactions in the vertical axis.
VerticalDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
///
/// {@macro flutter.gestures.gestureRecognizer.kind}
VerticalDragGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
@override
bool _isFlingGesture(VelocityEstimate estimate) {
@ -330,7 +338,12 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
/// track each touch point independently.
class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
/// Create a gesture recognizer for interactions in the horizontal axis.
HorizontalDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
///
/// {@macro flutter.gestures.gestureRecognizer.kind}
HorizontalDragGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
@override
bool _isFlingGesture(VelocityEstimate estimate) {

View file

@ -189,7 +189,10 @@ abstract class MultiDragPointerState {
/// start after a long-press gesture.
abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> extends GestureRecognizer {
/// Initialize the object.
MultiDragGestureRecognizer({ @required Object debugOwner }) : super(debugOwner: debugOwner);
MultiDragGestureRecognizer({
@required Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
/// Called when this class recognizes the start of a drag gesture.
///
@ -200,7 +203,7 @@ abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> exten
Map<int, T> _pointers = <int, T>{};
@override
void addPointer(PointerDownEvent event) {
void addAllowedPointer(PointerDownEvent event) {
assert(_pointers != null);
assert(event.pointer != null);
assert(event.position != null);
@ -334,7 +337,10 @@ class _ImmediatePointerState extends MultiDragPointerState {
/// start after a long-press gesture.
class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_ImmediatePointerState> {
/// Create a gesture recognizer for tracking multiple pointers at once.
ImmediateMultiDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
ImmediateMultiDragGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
@override
_ImmediatePointerState createNewPointerState(PointerDownEvent event) {
@ -380,7 +386,10 @@ class _HorizontalPointerState extends MultiDragPointerState {
class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_HorizontalPointerState> {
/// Create a gesture recognizer for tracking multiple pointers at once
/// but only if they first move horizontally.
HorizontalMultiDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
HorizontalMultiDragGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
@override
_HorizontalPointerState createNewPointerState(PointerDownEvent event) {
@ -426,7 +435,10 @@ class _VerticalPointerState extends MultiDragPointerState {
class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_VerticalPointerState> {
/// Create a gesture recognizer for tracking multiple pointers at once
/// but only if they first move vertically.
VerticalMultiDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
VerticalMultiDragGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
@override
_VerticalPointerState createNewPointerState(PointerDownEvent event) {
@ -528,8 +540,9 @@ class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Dela
DelayedMultiDragGestureRecognizer({
this.delay = kLongPressTimeout,
Object debugOwner,
PointerDeviceKind kind,
}) : assert(delay != null),
super(debugOwner: debugOwner);
super(debugOwner: debugOwner, kind: kind);
/// The amount of time the pointer must remain in the same place for the drag
/// to be recognized.

View file

@ -69,7 +69,12 @@ class _TapTracker {
/// quick succession.
class DoubleTapGestureRecognizer extends GestureRecognizer {
/// Create a gesture recognizer for double taps.
DoubleTapGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
///
/// {@macro flutter.gestures.gestureRecognizer.kind}
DoubleTapGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
// Implementation notes:
// The double tap recognizer can be in one of four states. There's no
@ -100,7 +105,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
final Map<int, _TapTracker> _trackers = <int, _TapTracker>{};
@override
void addPointer(PointerEvent event) {
void addAllowedPointer(PointerEvent event) {
// Ignore out-of-bounds second taps.
if (_firstTap != null &&
!_firstTap.isWithinTolerance(event, kDoubleTapSlop))
@ -318,7 +323,8 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
MultiTapGestureRecognizer({
this.longTapDelay = Duration.zero,
Object debugOwner,
}) : super(debugOwner: debugOwner);
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
/// A pointer that might cause a tap has contacted the screen at a particular
/// location.
@ -345,7 +351,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
final Map<int, _TapGesture> _gestureMap = <int, _TapGesture>{};
@override
void addPointer(PointerEvent event) {
void addAllowedPointer(PointerEvent event) {
assert(!_gestureMap.containsKey(event.pointer));
_gestureMap[event.pointer] = _TapGesture(
gestureRecognizer: this,

View file

@ -33,7 +33,7 @@ typedef RecognizerCallback<T> = T Function();
///
/// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
enum DragStartBehavior {
/// Set the initial offset, at the position where the first down even was
/// Set the initial offset, at the position where the first down event was
/// detected.
down,
@ -58,7 +58,13 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
///
/// The argument is optional and is only used for debug purposes (e.g. in the
/// [toString] serialization).
GestureRecognizer({ this.debugOwner });
///
/// {@template flutter.gestures.gestureRecognizer.kind}
/// It's possible to limit this recognizer to a specific [PointerDeviceKind]
/// by providing the optional [kind] argument. If [kind] is null,
/// the recognizer will accept pointer events from all device kinds.
/// {@endtemplate}
GestureRecognizer({ this.debugOwner, PointerDeviceKind kind }) : _kind = kind;
/// The recognizer's owner.
///
@ -66,6 +72,10 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
/// this gesture recognizer was created, to aid in debugging.
final Object debugOwner;
/// The kind of device that's allowed to be recognized. If null, events from
/// all device kinds will be tracked and recognized.
final PointerDeviceKind _kind;
/// Registers a new pointer that might be relevant to this gesture
/// detector.
///
@ -78,7 +88,43 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
/// subsequent events for this pointer, and to add the pointer to
/// the global gesture arena manager (see [GestureArenaManager]) to track
/// that pointer.
void addPointer(PointerDownEvent event);
///
/// This method is called for each and all pointers being added. In
/// most cases, you want to override [addAllowedPointer] instead.
void addPointer(PointerDownEvent event) {
if (isPointerAllowed(event)) {
addAllowedPointer(event);
} else {
handleNonAllowedPointer(event);
}
}
/// Registers a new pointer that's been checked to be allowed by this gesture
/// recognizer.
///
/// Subclasses of [GestureRecognizer] are supposed to override this method
/// instead of [addPointer] because [addPointer] will be called for each
/// pointer being added while [addAllowedPointer] is only called for pointers
/// that are allowed by this recognizer.
@protected
void addAllowedPointer(PointerDownEvent event) { }
/// Handles a pointer being added that's not allowed by this recognizer.
///
/// Subclasses can override this method and reject the gesture.
///
/// See:
/// - [OneSequenceGestureRecognizer.handleNonAllowedPointer].
@protected
void handleNonAllowedPointer(PointerDownEvent event) { }
/// Checks whether or not a pointer is allowed to be tracked by this recognizer.
@protected
bool isPointerAllowed(PointerDownEvent event) {
// Currently, it only checks for device kind. But in the future we could check
// for other things e.g. mouse button.
return _kind == null || _kind == event.kind;
}
/// Releases any resources used by the object.
///
@ -151,11 +197,21 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
/// simultaneous touches to each result in a separate tap.
abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
/// Initialize the object.
OneSequenceGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
///
/// {@macro flutter.gestures.gestureRecognizer.kind}
OneSequenceGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
final Set<int> _trackedPointers = HashSet<int>();
@override
void handleNonAllowedPointer(PointerDownEvent event) {
resolve(GestureDisposition.rejected);
}
/// Called when a pointer event is routed to this recognizer.
@protected
void handleEvent(PointerEvent event);
@ -291,11 +347,14 @@ enum GestureRecognizerState {
/// in the gesture arena, the gesture will be rejected.
abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer {
/// Initializes the [deadline] field during construction of subclasses.
///
/// {@macro flutter.gestures.gestureRecognizer.kind}
PrimaryPointerGestureRecognizer({
this.deadline,
this.preAcceptSlopTolerance = kTouchSlop,
this.postAcceptSlopTolerance = kTouchSlop,
Object debugOwner,
PointerDeviceKind kind,
}) : assert(
preAcceptSlopTolerance == null || preAcceptSlopTolerance >= 0,
'The preAcceptSlopTolerance must be positive or null',
@ -304,7 +363,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
postAcceptSlopTolerance == null || postAcceptSlopTolerance >= 0,
'The postAcceptSlopTolerance must be positive or null',
),
super(debugOwner: debugOwner);
super(debugOwner: debugOwner, kind: kind);
/// If non-null, the recognizer will call [didExceedDeadline] after this
/// amount of time has elapsed since starting to track the primary pointer.
@ -346,7 +405,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
Timer _timer;
@override
void addPointer(PointerDownEvent event) {
void addAllowedPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer);
if (state == GestureRecognizerState.ready) {
state = GestureRecognizerState.possible;

View file

@ -183,7 +183,12 @@ class _LineBetweenPointers{
/// are no longer in contact with the screen, the recognizer calls [onEnd].
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
/// Create a gesture recognizer for interactions intended for scaling content.
ScaleGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
///
/// {@macro flutter.gestures.gestureRecognizer.kind}
ScaleGestureRecognizer({
Object debugOwner,
PointerDeviceKind kind,
}) : super(debugOwner: debugOwner, kind: kind);
/// The pointers in contact with the screen have established a focal point and
/// initial scale of 1.0.
@ -239,7 +244,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
}
@override
void addPointer(PointerEvent event) {
void addAllowedPointer(PointerEvent event) {
startTrackingPointer(event.pointer);
_velocityTrackers[event.pointer] = VelocityTracker();
if (_state == _ScaleState.ready) {

View file

@ -599,12 +599,12 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
}
}
InteractiveInkFeature _createInkFeature(TapDownDetails details) {
InteractiveInkFeature _createInkFeature(Offset globalPosition) {
final MaterialInkController inkController = Material.of(context);
final ThemeData themeData = Theme.of(context);
final BuildContext editableContext = _editableTextKey.currentContext;
final RenderBox referenceBox = InputDecorator.containerOf(editableContext) ?? editableContext.findRenderObject();
final Offset position = referenceBox.globalToLocal(details.globalPosition);
final Offset position = referenceBox.globalToLocal(globalPosition);
final Color color = themeData.splashColor;
InteractiveInkFeature splash;
@ -637,7 +637,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
void _handleTapDown(TapDownDetails details) {
_renderEditable.handleTapDown(details);
_startSplash(details);
_startSplash(details.globalPosition);
}
void _handleForcePressStarted(ForcePressDetails details) {
@ -723,10 +723,29 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
}
}
void _startSplash(TapDownDetails details) {
void _handleDragSelectionStart(DragStartDetails details) {
_renderEditable.selectPositionAt(
from: details.globalPosition,
cause: SelectionChangedCause.drag,
);
_startSplash(details.globalPosition);
}
void _handleDragSelectionUpdate(
DragStartDetails startDetails,
DragUpdateDetails updateDetails,
) {
_renderEditable.selectPositionAt(
from: startDetails.globalPosition,
to: updateDetails.globalPosition,
cause: SelectionChangedCause.drag,
);
}
void _startSplash(Offset globalPosition) {
if (_effectiveFocusNode.hasFocus)
return;
final InteractiveInkFeature splash = _createInkFeature(details);
final InteractiveInkFeature splash = _createInkFeature(globalPosition);
_splashes ??= HashSet<InteractiveInkFeature>();
_splashes.add(splash);
_currentSplash = splash;
@ -888,6 +907,8 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
onSingleLongTapEnd: _handleSingleLongTapEnd,
onDoubleTapDown: _handleDoubleTapDown,
onDragSelectionStart: _handleDragSelectionStart,
onDragSelectionUpdate: _handleDragSelectionUpdate,
behavior: HitTestBehavior.translucent,
child: child,
),

View file

@ -55,6 +55,10 @@ enum SelectionChangedCause {
/// Keyboard-triggered selection changes may be caused by the IME as well as
/// by accessibility tools (e.g. TalkBack on Android).
keyboard,
/// The user used the mouse to change the selection by dragging over a piece
/// of text.
drag,
}
/// Signature for the callback that reports when the caret location changes.
@ -1235,7 +1239,7 @@ class RenderEditable extends RenderBox {
}
/// If [ignorePointer] is false (the default) then this method is called by
/// the internal gesture recognizer's [LongPressRecognizer.onLongPress]
/// the internal gesture recognizer's [LongPressGestureRecognizer.onLongPress]
/// callback.
///
/// When [ignorePointer] is true, an ancestor widget must respond to long

View file

@ -367,7 +367,9 @@ class RenderUiKitView extends RenderBox {
// When the team wins a gesture the recognizer notifies the engine that it should release
// the touch sequence to the embedded UIView.
class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer {
_UiKitViewGestureRecognizer(this.controller, this.gestureRecognizerFactories) {
_UiKitViewGestureRecognizer(this.controller, this.gestureRecognizerFactories, {
PointerDeviceKind kind,
}): super(kind: kind) {
team = GestureArenaTeam();
team.captain = this;
_gestureRecognizers = gestureRecognizerFactories.map(
@ -387,7 +389,7 @@ class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer {
final UiKitViewController controller;
@override
void addPointer(PointerDownEvent event) {
void addAllowedPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer);
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
recognizer.addPointer(event);
@ -427,7 +429,9 @@ class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer {
// When the team wins the recognizer sends all the cached point events to the embedded Android view, and
// sets itself to a "forwarding mode" where it will forward any new pointer event to the Android view.
class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
_AndroidViewGestureRecognizer(this.dispatcher, this.gestureRecognizerFactories) {
_AndroidViewGestureRecognizer(this.dispatcher, this.gestureRecognizerFactories, {
PointerDeviceKind kind,
}): super(kind: kind) {
team = GestureArenaTeam();
team.captain = this;
_gestureRecognizers = gestureRecognizerFactories.map(
@ -456,7 +460,7 @@ class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
Set<OneSequenceGestureRecognizer> _gestureRecognizers;
@override
void addPointer(PointerDownEvent event) {
void addAllowedPointer(PointerDownEvent event) {
startTrackingPointer(event.pointer);
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
recognizer.addPointer(event);

View file

@ -8,7 +8,7 @@ import 'package:flutter/gestures.dart' show kDoubleTapTimeout, kDoubleTapSlop;
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/gestures.dart';
import 'basic.dart';
import 'container.dart';
@ -20,6 +20,10 @@ import 'transitions.dart';
export 'package:flutter/services.dart' show TextSelectionDelegate;
/// A duration that controls how often the drag selection update callback is
/// called.
const Duration _kDragSelectionUpdateThrottle = Duration(milliseconds: 50);
/// Which type of selection handle to be displayed.
///
/// With mixed-direction text, both handles may be the same type. Examples:
@ -64,6 +68,19 @@ enum _TextSelectionHandlePosition { start, end }
/// Used by [TextSelectionOverlay.onSelectionOverlayChanged].
typedef TextSelectionOverlayChanged = void Function(TextEditingValue value, Rect caretRect);
/// Signature for when a pointer that's dragging to select text has moved again.
///
/// The first argument [startDetails] contains the details of the event that
/// initiated the dragging.
///
/// The second argument [updateDetails] contains the details of the current
/// pointer movement. It's the same as the one passed to [DragGestureRecognizer.onUpdate].
///
/// This signature is different from [GestureDragUpdateCallback] to make it
/// easier for various text fields to use [TextSelectionGestureDetector] without
/// having to store the start position.
typedef DragSelectionUpdateCallback = void Function(DragStartDetails startDetails, DragUpdateDetails updateDetails);
/// An interface for building the selection UI, to be provided by the
/// implementor of the toolbar widget.
///
@ -620,6 +637,9 @@ class TextSelectionGestureDetector extends StatefulWidget {
this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd,
this.onDoubleTapDown,
this.onDragSelectionStart,
this.onDragSelectionUpdate,
this.onDragSelectionEnd,
this.behavior,
@required this.child,
}) : assert(child != null),
@ -664,6 +684,19 @@ class TextSelectionGestureDetector extends StatefulWidget {
/// time (within [kDoubleTapTimeout]) to a previous short tap.
final GestureTapDownCallback onDoubleTapDown;
/// Called when a mouse starts dragging to select text.
final GestureDragStartCallback onDragSelectionStart;
/// Called repeatedly as a mouse moves while dragging.
///
/// The frequency of calls is throttled to avoid excessive text layout
/// operations in text fields. The throttling is controlled by the constant
/// [_kDragSelectionUpdateThrottle].
final DragSelectionUpdateCallback onDragSelectionUpdate;
/// Called when a mouse that was previously dragging is released.
final GestureDragEndCallback onDragSelectionEnd;
/// How this gesture detector should behave during hit testing.
///
/// This defaults to [HitTestBehavior.deferToChild].
@ -687,6 +720,7 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
@override
void dispose() {
_doubleTapTimer?.cancel();
_dragUpdateThrottleTimer?.cancel();
super.dispose();
}
@ -730,6 +764,56 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
}
}
DragStartDetails _lastDragStartDetails;
DragUpdateDetails _lastDragUpdateDetails;
Timer _dragUpdateThrottleTimer;
void _handleDragStart(DragStartDetails details) {
assert(_lastDragStartDetails == null);
_lastDragStartDetails = details;
if (widget.onDragSelectionStart != null) {
widget.onDragSelectionStart(details);
}
}
void _handleDragUpdate(DragUpdateDetails details) {
_lastDragUpdateDetails = details;
// Only schedule a new timer if there's no one pending.
_dragUpdateThrottleTimer ??= Timer(_kDragSelectionUpdateThrottle, _handleDragUpdateThrottled);
}
/// Drag updates are being throttled to avoid excessive text layouts in text
/// fields. The frequency of invocations is controlled by the constant
/// [_kDragSelectionUpdateThrottle].
///
/// Once the drag gesture ends, any pending drag update will be fired
/// immediately. See [_handleDragEnd].
void _handleDragUpdateThrottled() {
assert(_lastDragStartDetails != null);
assert(_lastDragUpdateDetails != null);
if (widget.onDragSelectionUpdate != null) {
widget.onDragSelectionUpdate(_lastDragStartDetails, _lastDragUpdateDetails);
}
_dragUpdateThrottleTimer = null;
_lastDragUpdateDetails = null;
}
void _handleDragEnd(DragEndDetails details) {
assert(_lastDragStartDetails != null);
if (_dragUpdateThrottleTimer != null) {
// If there's already an update scheduled, trigger it immediately and
// cancel the timer.
_dragUpdateThrottleTimer.cancel();
_handleDragUpdateThrottled();
}
if (widget.onDragSelectionEnd != null) {
widget.onDragSelectionEnd(details);
}
_dragUpdateThrottleTimer = null;
_lastDragStartDetails = null;
_lastDragUpdateDetails = null;
}
void _forcePressStarted(ForcePressDetails details) {
_doubleTapTimer?.cancel();
_doubleTapTimer = null;
@ -754,7 +838,7 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
}
}
void _handleLongPressUp(LongPressEndDetails details) {
void _handleLongPressEnd(LongPressEndDetails details) {
if (!_isDoubleTap && widget.onSingleLongTapEnd != null) {
widget.onSingleLongTapEnd(details);
}
@ -778,15 +862,64 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: _handleTapDown,
onTapUp: _handleTapUp,
onForcePressStart: widget.onForcePressStart != null ? _forcePressStarted : null,
onForcePressEnd: widget.onForcePressEnd != null ? _forcePressEnded : null,
onTapCancel: _handleTapCancel,
onLongPressStart: _handleLongPressStart,
onLongPressMoveUpdate: _handleLongPressMoveUpdate,
onLongPressEnd: _handleLongPressUp,
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
instance
..onTapDown = _handleTapDown
..onTapUp = _handleTapUp
..onTapCancel = _handleTapCancel;
},
);
if (widget.onSingleLongTapStart != null ||
widget.onSingleLongTapMoveUpdate != null ||
widget.onSingleLongTapEnd != null) {
gestures[LongPressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
() => LongPressGestureRecognizer(debugOwner: this, kind: PointerDeviceKind.touch),
(LongPressGestureRecognizer instance) {
instance
..onLongPressStart = _handleLongPressStart
..onLongPressMoveUpdate = _handleLongPressMoveUpdate
..onLongPressEnd = _handleLongPressEnd;
},
);
}
if (widget.onDragSelectionStart != null ||
widget.onDragSelectionUpdate != null ||
widget.onDragSelectionEnd != null) {
// TODO(mdebbar): Support dragging in any direction (for multiline text).
// https://github.com/flutter/flutter/issues/28676
gestures[HorizontalDragGestureRecognizer] = GestureRecognizerFactoryWithHandlers<HorizontalDragGestureRecognizer>(
() => HorizontalDragGestureRecognizer(debugOwner: this, kind: PointerDeviceKind.mouse),
(HorizontalDragGestureRecognizer instance) {
instance
// Text selection should start from the position of the first pointer
// down event.
..dragStartBehavior = DragStartBehavior.down
..onStart = _handleDragStart
..onUpdate = _handleDragUpdate
..onEnd = _handleDragEnd;
},
);
}
if (widget.onForcePressStart != null || widget.onForcePressEnd != null) {
gestures[ForcePressGestureRecognizer] = GestureRecognizerFactoryWithHandlers<ForcePressGestureRecognizer>(
() => ForcePressGestureRecognizer(debugOwner: this),
(ForcePressGestureRecognizer instance) {
instance
..onStart = widget.onForcePressStart != null ? _forcePressStarted : null
..onEnd = widget.onForcePressEnd != null ? _forcePressEnded : null;
},
);
}
return RawGestureDetector(
gestures: gestures,
excludeFromSemantics: true,
behavior: widget.behavior,
child: widget.child,

View file

@ -517,4 +517,83 @@ void main() {
tester.route(pointer.up());
drag.dispose();
});
}
testGesture('Can filter drags based on device kind', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag =
HorizontalDragGestureRecognizer(
kind: PointerDeviceKind.mouse,
)
..dragStartBehavior = DragStartBehavior.down;
bool didStartDrag = false;
drag.onStart = (_) {
didStartDrag = true;
};
double updatedDelta;
drag.onUpdate = (DragUpdateDetails details) {
updatedDelta = details.primaryDelta;
};
bool didEndDrag = false;
drag.onEnd = (DragEndDetails details) {
didEndDrag = true;
};
// Using a touch pointer to drag shouldn't be recognized.
final TestPointer touchPointer = TestPointer(5, PointerDeviceKind.touch);
final PointerDownEvent touchDown = touchPointer.down(const Offset(10.0, 10.0));
drag.addPointer(touchDown);
tester.closeArena(5);
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(touchDown);
// Still doesn't recognize the drag because it's coming from a touch pointer.
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(touchPointer.move(const Offset(20.0, 25.0)));
// Still doesn't recognize the drag because it's coming from a touch pointer.
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(touchPointer.up());
// Still doesn't recognize the drag because it's coming from a touch pointer.
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
// Using a mouse pointer to drag should be recognized.
final TestPointer mousePointer = TestPointer(5, PointerDeviceKind.mouse);
final PointerDownEvent mouseDown = mousePointer.down(const Offset(10.0, 10.0));
drag.addPointer(mouseDown);
tester.closeArena(5);
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(mouseDown);
expect(didStartDrag, isTrue);
didStartDrag = false;
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
tester.route(mousePointer.move(const Offset(20.0, 25.0)));
expect(didStartDrag, isFalse);
expect(updatedDelta, 10.0);
updatedDelta = null;
expect(didEndDrag, isFalse);
tester.route(mousePointer.up());
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isTrue);
didEndDrag = false;
drag.dispose();
});
}

View file

@ -278,4 +278,44 @@ void main() {
longPressDrag.dispose();
});
});
testGesture('Can filter long press based on device kind', (GestureTester tester) {
final LongPressGestureRecognizer mouseLongPress = LongPressGestureRecognizer(kind: PointerDeviceKind.mouse);
bool mouseLongPressDown = false;
mouseLongPress.onLongPress = () {
mouseLongPressDown = true;
};
const PointerDownEvent mouseDown = PointerDownEvent(
pointer: 5,
position: Offset(10, 10),
kind: PointerDeviceKind.mouse,
);
const PointerDownEvent touchDown = PointerDownEvent(
pointer: 5,
position: Offset(10, 10),
kind: PointerDeviceKind.touch,
);
// Touch events shouldn't be recognized.
mouseLongPress.addPointer(touchDown);
tester.closeArena(5);
expect(mouseLongPressDown, isFalse);
tester.route(touchDown);
expect(mouseLongPressDown, isFalse);
tester.async.elapse(const Duration(seconds: 2));
expect(mouseLongPressDown, isFalse);
// Mouse events are still recognized.
mouseLongPress.addPointer(mouseDown);
tester.closeArena(5);
expect(mouseLongPressDown, isFalse);
tester.route(mouseDown);
expect(mouseLongPressDown, isFalse);
tester.async.elapse(const Duration(seconds: 2));
expect(mouseLongPressDown, isTrue);
mouseLongPress.dispose();
});
}

View file

@ -61,4 +61,32 @@ void main() {
expect(didStartDrag, isTrue);
drag.dispose();
});
testGesture('MultiDrag: can filter based on device kind', (GestureTester tester) {
final DelayedMultiDragGestureRecognizer drag =
DelayedMultiDragGestureRecognizer(kind: PointerDeviceKind.touch);
bool didStartDrag = false;
drag.onStart = (Offset position) {
didStartDrag = true;
return TestDrag();
};
final TestPointer mousePointer = TestPointer(5, PointerDeviceKind.mouse);
final PointerDownEvent down = mousePointer.down(const Offset(10.0, 10.0));
drag.addPointer(down);
tester.closeArena(5);
expect(didStartDrag, isFalse);
tester.async.flushMicrotasks();
expect(didStartDrag, isFalse);
tester.route(mousePointer.move(const Offset(20.0, 20.0))); // move less than touch slop before delay expires
expect(didStartDrag, isFalse);
tester.async.elapse(kLongPressTimeout * 2); // expire delay
// Still false because it shouldn't recognize mouse events.
expect(didStartDrag, isFalse);
tester.route(mousePointer.move(const Offset(30.0, 70.0))); // move more than touch slop after delay expires
// And still false.
expect(didStartDrag, isFalse);
drag.dispose();
});
}

View file

@ -69,4 +69,81 @@ void main() {
tap.dispose();
});
testGesture('Can filter based on device kind', (GestureTester tester) {
final MultiTapGestureRecognizer tap =
MultiTapGestureRecognizer(
longTapDelay: kLongPressTimeout,
kind: PointerDeviceKind.touch,
);
final List<String> log = <String>[];
tap.onTapDown = (int pointer, TapDownDetails details) { log.add('tap-down $pointer'); };
tap.onTapUp = (int pointer, TapUpDetails details) { log.add('tap-up $pointer'); };
tap.onTap = (int pointer) { log.add('tap $pointer'); };
tap.onLongTapDown = (int pointer, TapDownDetails details) { log.add('long-tap-down $pointer'); };
tap.onTapCancel = (int pointer) { log.add('tap-cancel $pointer'); };
final TestPointer touchPointer5 = TestPointer(5, PointerDeviceKind.touch);
final PointerDownEvent down5 = touchPointer5.down(const Offset(10.0, 10.0));
tap.addPointer(down5);
tester.closeArena(5);
expect(log, <String>['tap-down 5']);
log.clear();
tester.route(down5);
expect(log, isEmpty);
final TestPointer mousePointer6 = TestPointer(6, PointerDeviceKind.mouse);
final PointerDownEvent down6 = mousePointer6.down(const Offset(20.0, 20.0));
tap.addPointer(down6);
tester.closeArena(6);
// Mouse down should be ignored by the recognizer.
expect(log, isEmpty);
final TestPointer touchPointer7 = TestPointer(7, PointerDeviceKind.touch);
final PointerDownEvent down7 = touchPointer7.down(const Offset(15.0, 15.0));
tap.addPointer(down7);
tester.closeArena(7);
expect(log, <String>['tap-down 7']);
log.clear();
tester.route(down7);
expect(log, isEmpty);
tester.route(touchPointer5.move(const Offset(11.0, 12.0)));
expect(log, isEmpty);
// Move within the [kTouchSlop] range.
tester.route(mousePointer6.move(const Offset(21.0, 18.0)));
// Move beyond the slop range.
tester.route(mousePointer6.move(const Offset(50.0, 40.0)));
// Neither triggers any event because they originate from a mouse.
expect(log, isEmpty);
tester.route(touchPointer7.move(const Offset(14.0, 13.0)));
expect(log, isEmpty);
tester.route(touchPointer5.up());
expect(log, <String>[
'tap-up 5',
'tap 5',
]);
log.clear();
// Mouse up should be ignored.
tester.route(mousePointer6.up());
expect(log, isEmpty);
tester.async.elapse(kLongPressTimeout + kPressTimeout);
// Only the touch pointer (7) triggers a long-tap, not the mouse pointer (6).
expect(log, <String>['long-tap-down 7']);
log.clear();
tester.route(touchPointer7.move(const Offset(40.0, 30.0))); // move more than kTouchSlop from 15.0,15.0
expect(log, <String>['tap-cancel 7']);
log.clear();
tap.dispose();
});
}

View file

@ -220,6 +220,101 @@ void main() {
tap.dispose();
});
testGesture('Rejects scale gestures from unallowed device kinds', (GestureTester tester) {
final ScaleGestureRecognizer scale = ScaleGestureRecognizer(kind: PointerDeviceKind.touch);
bool didStartScale = false;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
};
double updatedScale;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedScale = details.scale;
};
final TestPointer mousePointer = TestPointer(1, PointerDeviceKind.mouse);
final PointerDownEvent down = mousePointer.down(const Offset(0.0, 0.0));
scale.addPointer(down);
tester.closeArena(1);
// One-finger panning
tester.route(down);
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
// Using a mouse, the scale gesture shouldn't even start.
tester.route(mousePointer.move(const Offset(20.0, 30.0)));
expect(didStartScale, isFalse);
expect(updatedScale, isNull);
scale.dispose();
});
testGesture('Scale gestures starting from allowed device kinds cannot be ended from unallowed devices', (GestureTester tester) {
final ScaleGestureRecognizer scale = ScaleGestureRecognizer(kind: PointerDeviceKind.touch);
bool didStartScale = false;
Offset updatedFocalPoint;
scale.onStart = (ScaleStartDetails details) {
didStartScale = true;
updatedFocalPoint = details.focalPoint;
};
double updatedScale;
scale.onUpdate = (ScaleUpdateDetails details) {
updatedScale = details.scale;
updatedFocalPoint = details.focalPoint;
};
bool didEndScale = false;
scale.onEnd = (ScaleEndDetails details) {
didEndScale = true;
};
final TestPointer touchPointer = TestPointer(1, PointerDeviceKind.touch);
final PointerDownEvent down = touchPointer.down(const Offset(0.0, 0.0));
scale.addPointer(down);
tester.closeArena(1);
// One-finger panning
tester.route(down);
expect(didStartScale, isTrue);
didStartScale = false;
expect(updatedScale, isNull);
expect(updatedFocalPoint, const Offset(0.0, 0.0));
expect(didEndScale, isFalse);
// The gesture can start using one touch finger.
tester.route(touchPointer.move(const Offset(20.0, 30.0)));
expect(updatedFocalPoint, const Offset(20.0, 30.0));
updatedFocalPoint = null;
expect(updatedScale, 1.0);
updatedScale = null;
expect(didEndScale, isFalse);
// Two-finger scaling
final TestPointer mousePointer = TestPointer(2, PointerDeviceKind.mouse);
final PointerDownEvent down2 = mousePointer.down(const Offset(10.0, 20.0));
scale.addPointer(down2);
tester.closeArena(2);
tester.route(down2);
// Mouse-generated events are ignored.
expect(didEndScale, isFalse);
expect(updatedScale, isNull);
expect(didStartScale, isFalse);
// Zoom in using a mouse doesn't work either.
tester.route(mousePointer.move(const Offset(0.0, 10.0)));
expect(updatedScale, isNull);
expect(didEndScale, isFalse);
scale.dispose();
});
testGesture('Scale gesture competes with drag', (GestureTester tester) {
final ScaleGestureRecognizer scale = ScaleGestureRecognizer();
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();

View file

@ -12,7 +12,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/gestures.dart' show DragStartBehavior, PointerDeviceKind;
import '../widgets/semantics_tester.dart';
import 'feedback_tester.dart';
@ -533,6 +533,37 @@ void main() {
expect(controller.selection.extentOffset, testValue.indexOf('f')+1);
});
testWidgets('Mouse long press is just like a tap', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
controller: controller,
),
)
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
expect(controller.value.text, testValue);
await skipPastScrollingAnimation(tester);
expect(controller.selection.isCollapsed, true);
// Long press the 'e' using a mouse device.
final int eIndex = testValue.indexOf('e');
final Offset ePos = textOffsetToPosition(tester, eIndex);
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pump();
// The cursor is placed just like a regular tap.
expect(controller.selection.baseOffset, eIndex);
expect(controller.selection.extentOffset, eIndex);
});
testWidgets('enableInteractiveSelection = false, long-press', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
@ -564,6 +595,69 @@ void main() {
expect(controller.selection.extentOffset, -1);
});
testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await tester.pump();
await gesture.moveTo(gPos);
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(controller.selection.baseOffset, testValue.indexOf('e'));
expect(controller.selection.extentOffset, testValue.indexOf('g'));
});
testWidgets('Slow mouse dragging also selects text', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
final Offset ePos = textOffsetToPosition(tester, testValue.indexOf('e'));
final Offset gPos = textOffsetToPosition(tester, testValue.indexOf('g'));
final TestGesture gesture = await tester.startGesture(ePos, kind: PointerDeviceKind.mouse);
await tester.pump(const Duration(seconds: 2));
await gesture.moveTo(gPos);
await tester.pump();
await gesture.up();
expect(controller.selection.baseOffset, testValue.indexOf('e'));
expect(controller.selection.extentOffset, testValue.indexOf('g'));
});
testWidgets('Can drag handles to change selection', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController();

View file

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart' show PointerDeviceKind;
import 'package:flutter/widgets.dart';
void main() {
@ -13,9 +14,11 @@ void main() {
int doubleTapDownCount;
int forcePressStartCount;
int forcePressEndCount;
int dragStartCount;
int dragUpdateCount;
int dragEndCount;
const Offset forcePressOffset = Offset(400.0, 50.0);
void _handleTapDown(TapDownDetails details) { tapCount++; }
void _handleSingleTapUp(TapUpDetails details) { singleTapUpCount++; }
void _handleSingleTapCancel() { singleTapCancelCount++; }
@ -23,6 +26,9 @@ void main() {
void _handleDoubleTapDown(TapDownDetails details) { doubleTapDownCount++; }
void _handleForcePressStart(ForcePressDetails details) { forcePressStartCount++; }
void _handleForcePressEnd(ForcePressDetails details) { forcePressEndCount++; }
void _handleDragSelectionStart(DragStartDetails details) { dragStartCount++; }
void _handleDragSelectionUpdate(DragStartDetails _, DragUpdateDetails details) { dragUpdateCount++; }
void _handleDragSelectionEnd(DragEndDetails details) { dragEndCount++; }
setUp(() {
tapCount = 0;
@ -32,6 +38,9 @@ void main() {
doubleTapDownCount = 0;
forcePressStartCount = 0;
forcePressEndCount = 0;
dragStartCount = 0;
dragUpdateCount = 0;
dragEndCount = 0;
});
Future<void> pumpGestureDetector(WidgetTester tester) async {
@ -45,6 +54,9 @@ void main() {
onDoubleTapDown: _handleDoubleTapDown,
onForcePressStart: _handleForcePressStart,
onForcePressEnd: _handleForcePressEnd,
onDragSelectionStart: _handleDragSelectionStart,
onDragSelectionUpdate: _handleDragSelectionUpdate,
onDragSelectionEnd: _handleDragSelectionEnd,
child: Container(),
),
);
@ -275,4 +287,89 @@ void main() {
expect(forcePressEndCount, 1);
expect(doubleTapDownCount, 0);
});
testWidgets('a long press from a touch device is recognized as a long single tap', (WidgetTester tester) async {
await pumpGestureDetector(tester);
const int pointerValue = 1;
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0), pointer: pointerValue, kind: PointerDeviceKind.touch);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pumpAndSettle();
expect(tapCount, 1);
expect(singleTapUpCount, 0);
expect(singleLongTapStartCount, 1);
});
testWidgets('a long press from a mouse is just a tap', (WidgetTester tester) async {
await pumpGestureDetector(tester);
const int pointerValue = 1;
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0), pointer: pointerValue, kind: PointerDeviceKind.mouse);
await tester.pump(const Duration(seconds: 2));
await gesture.up();
await tester.pumpAndSettle();
expect(tapCount, 1);
expect(singleTapUpCount, 1);
expect(singleLongTapStartCount, 0);
});
testWidgets('a touch drag is not recognized for text selection', (WidgetTester tester) async {
await pumpGestureDetector(tester);
const int pointerValue = 1;
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0), pointer: pointerValue, kind: PointerDeviceKind.touch);
await tester.pump();
await gesture.moveBy(const Offset(210.0, 200.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(tapCount, 0);
expect(singleTapUpCount, 0);
expect(dragStartCount, 0);
expect(dragUpdateCount, 0);
expect(dragEndCount, 0);
});
testWidgets('a mouse drag is recognized for text selection', (WidgetTester tester) async {
await pumpGestureDetector(tester);
const int pointerValue = 1;
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0), pointer: pointerValue, kind: PointerDeviceKind.mouse);
await tester.pump();
await gesture.moveBy(const Offset(210.0, 200.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(tapCount, 0);
expect(singleTapUpCount, 0);
expect(dragStartCount, 1);
expect(dragUpdateCount, 1);
expect(dragEndCount, 1);
});
testWidgets('a slow mouse drag is still recognized for text selection', (WidgetTester tester) async {
await pumpGestureDetector(tester);
const int pointerValue = 1;
final TestGesture gesture =
await tester.startGesture(const Offset(200.0, 200.0), pointer: pointerValue, kind: PointerDeviceKind.mouse);
await tester.pump(const Duration(seconds: 2));
await gesture.moveBy(const Offset(210.0, 200.0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(dragStartCount, 1);
expect(dragUpdateCount, 1);
expect(dragEndCount, 1);
});
}

View file

@ -553,8 +553,12 @@ abstract class WidgetController {
///
/// You can use [createGesture] if your gesture doesn't begin with an initial
/// down gesture.
Future<TestGesture> startGesture(Offset downLocation, {int pointer}) async {
final TestGesture result = await createGesture(pointer: pointer);
Future<TestGesture> startGesture(
Offset downLocation, {
int pointer,
PointerDeviceKind kind = PointerDeviceKind.touch,
}) async {
final TestGesture result = await createGesture(pointer: pointer, kind: kind);
await result.down(downLocation);
return result;
}