mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
Text selection via mouse (#28290)
This commit is contained in:
parent
1df28e8b7a
commit
b94bf87c70
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue