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.
|
/// all touch events inside the view bounds to the embedded Android view.
|
||||||
/// See [AndroidView.gestureRecognizers] for more details.
|
/// See [AndroidView.gestureRecognizers] for more details.
|
||||||
class EagerGestureRecognizer extends OneSequenceGestureRecognizer {
|
class EagerGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
|
/// Create an eager gesture recognizer.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.gestures.gestureRecognizer.kind}
|
||||||
|
EagerGestureRecognizer({ PointerDeviceKind kind }): super(kind: kind);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addPointer(PointerDownEvent event) {
|
void addAllowedPointer(PointerDownEvent event) {
|
||||||
// We call startTrackingPointer as this is where OneSequenceGestureRecognizer joins the arena.
|
// We call startTrackingPointer as this is where OneSequenceGestureRecognizer joins the arena.
|
||||||
startTrackingPointer(event.pointer);
|
startTrackingPointer(event.pointer);
|
||||||
resolve(GestureDisposition.accepted);
|
resolve(GestureDisposition.accepted);
|
||||||
|
@ -116,16 +116,19 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
/// The [interpolation] callback must always return a value in the range 0.0
|
/// 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
|
/// to 1.0 for values of `pressure` that are between `pressureMin` and
|
||||||
/// `pressureMax`.
|
/// `pressureMax`.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.gestures.gestureRecognizer.kind}
|
||||||
ForcePressGestureRecognizer({
|
ForcePressGestureRecognizer({
|
||||||
this.startPressure = 0.4,
|
this.startPressure = 0.4,
|
||||||
this.peakPressure = 0.85,
|
this.peakPressure = 0.85,
|
||||||
this.interpolation = _inverseLerp,
|
this.interpolation = _inverseLerp,
|
||||||
Object debugOwner,
|
Object debugOwner,
|
||||||
|
PointerDeviceKind kind,
|
||||||
}) : assert(startPressure != null),
|
}) : assert(startPressure != null),
|
||||||
assert(peakPressure != null),
|
assert(peakPressure != null),
|
||||||
assert(interpolation != null),
|
assert(interpolation != null),
|
||||||
assert(peakPressure > startPressure),
|
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
|
/// A pointer is in contact with the screen and has just pressed with a force
|
||||||
/// exceeding the [startPressure]. Consequently, if there were other gesture
|
/// exceeding the [startPressure]. Consequently, if there were other gesture
|
||||||
@ -205,7 +208,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
_ForceState _state = _ForceState.ready;
|
_ForceState _state = _ForceState.ready;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addPointer(PointerEvent event) {
|
void addAllowedPointer(PointerEvent event) {
|
||||||
// If the device has a maximum pressure of less than or equal to 1, it
|
// 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
|
// doesn't have touch pressure sensing capabilities. Do not participate
|
||||||
// in the gesture arena.
|
// in the gesture arena.
|
||||||
|
@ -123,10 +123,12 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
|||||||
/// can be moved without limit once the long press is accepted.
|
/// can be moved without limit once the long press is accepted.
|
||||||
LongPressGestureRecognizer({
|
LongPressGestureRecognizer({
|
||||||
double postAcceptSlopTolerance,
|
double postAcceptSlopTolerance,
|
||||||
|
PointerDeviceKind kind,
|
||||||
Object debugOwner,
|
Object debugOwner,
|
||||||
}) : super(
|
}) : super(
|
||||||
deadline: kLongPressTimeout,
|
deadline: kLongPressTimeout,
|
||||||
postAcceptSlopTolerance: postAcceptSlopTolerance,
|
postAcceptSlopTolerance: postAcceptSlopTolerance,
|
||||||
|
kind: kind,
|
||||||
debugOwner: debugOwner,
|
debugOwner: debugOwner,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -52,11 +52,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
/// Initialize the object.
|
/// Initialize the object.
|
||||||
///
|
///
|
||||||
/// [dragStartBehavior] must not be null.
|
/// [dragStartBehavior] must not be null.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.gestures.gestureRecognizer.kind}
|
||||||
DragGestureRecognizer({
|
DragGestureRecognizer({
|
||||||
Object debugOwner,
|
Object debugOwner,
|
||||||
|
PointerDeviceKind kind,
|
||||||
this.dragStartBehavior = DragStartBehavior.start,
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
}) : assert(dragStartBehavior != null),
|
}) : assert(dragStartBehavior != null),
|
||||||
super(debugOwner: debugOwner);
|
super(debugOwner: debugOwner, kind: kind);
|
||||||
|
|
||||||
/// Configure the behavior of offsets sent to [onStart].
|
/// Configure the behavior of offsets sent to [onStart].
|
||||||
///
|
///
|
||||||
@ -147,7 +150,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
|
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addPointer(PointerEvent event) {
|
void addAllowedPointer(PointerEvent event) {
|
||||||
startTrackingPointer(event.pointer);
|
startTrackingPointer(event.pointer);
|
||||||
_velocityTrackers[event.pointer] = VelocityTracker();
|
_velocityTrackers[event.pointer] = VelocityTracker();
|
||||||
if (_state == _DragState.ready) {
|
if (_state == _DragState.ready) {
|
||||||
@ -296,7 +299,12 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
/// track each touch point independently.
|
/// track each touch point independently.
|
||||||
class VerticalDragGestureRecognizer extends DragGestureRecognizer {
|
class VerticalDragGestureRecognizer extends DragGestureRecognizer {
|
||||||
/// Create a gesture recognizer for interactions in the vertical axis.
|
/// 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
|
@override
|
||||||
bool _isFlingGesture(VelocityEstimate estimate) {
|
bool _isFlingGesture(VelocityEstimate estimate) {
|
||||||
@ -330,7 +338,12 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
|
|||||||
/// track each touch point independently.
|
/// track each touch point independently.
|
||||||
class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
|
class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
|
||||||
/// Create a gesture recognizer for interactions in the horizontal axis.
|
/// 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
|
@override
|
||||||
bool _isFlingGesture(VelocityEstimate estimate) {
|
bool _isFlingGesture(VelocityEstimate estimate) {
|
||||||
|
@ -189,7 +189,10 @@ abstract class MultiDragPointerState {
|
|||||||
/// start after a long-press gesture.
|
/// start after a long-press gesture.
|
||||||
abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> extends GestureRecognizer {
|
abstract class MultiDragGestureRecognizer<T extends MultiDragPointerState> extends GestureRecognizer {
|
||||||
/// Initialize the object.
|
/// 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.
|
/// 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>{};
|
Map<int, T> _pointers = <int, T>{};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addPointer(PointerDownEvent event) {
|
void addAllowedPointer(PointerDownEvent event) {
|
||||||
assert(_pointers != null);
|
assert(_pointers != null);
|
||||||
assert(event.pointer != null);
|
assert(event.pointer != null);
|
||||||
assert(event.position != null);
|
assert(event.position != null);
|
||||||
@ -334,7 +337,10 @@ class _ImmediatePointerState extends MultiDragPointerState {
|
|||||||
/// start after a long-press gesture.
|
/// start after a long-press gesture.
|
||||||
class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_ImmediatePointerState> {
|
class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_ImmediatePointerState> {
|
||||||
/// Create a gesture recognizer for tracking multiple pointers at once.
|
/// 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
|
@override
|
||||||
_ImmediatePointerState createNewPointerState(PointerDownEvent event) {
|
_ImmediatePointerState createNewPointerState(PointerDownEvent event) {
|
||||||
@ -380,7 +386,10 @@ class _HorizontalPointerState extends MultiDragPointerState {
|
|||||||
class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_HorizontalPointerState> {
|
class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_HorizontalPointerState> {
|
||||||
/// Create a gesture recognizer for tracking multiple pointers at once
|
/// Create a gesture recognizer for tracking multiple pointers at once
|
||||||
/// but only if they first move horizontally.
|
/// but only if they first move horizontally.
|
||||||
HorizontalMultiDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
|
HorizontalMultiDragGestureRecognizer({
|
||||||
|
Object debugOwner,
|
||||||
|
PointerDeviceKind kind,
|
||||||
|
}) : super(debugOwner: debugOwner, kind: kind);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_HorizontalPointerState createNewPointerState(PointerDownEvent event) {
|
_HorizontalPointerState createNewPointerState(PointerDownEvent event) {
|
||||||
@ -426,7 +435,10 @@ class _VerticalPointerState extends MultiDragPointerState {
|
|||||||
class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_VerticalPointerState> {
|
class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_VerticalPointerState> {
|
||||||
/// Create a gesture recognizer for tracking multiple pointers at once
|
/// Create a gesture recognizer for tracking multiple pointers at once
|
||||||
/// but only if they first move vertically.
|
/// but only if they first move vertically.
|
||||||
VerticalMultiDragGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);
|
VerticalMultiDragGestureRecognizer({
|
||||||
|
Object debugOwner,
|
||||||
|
PointerDeviceKind kind,
|
||||||
|
}) : super(debugOwner: debugOwner, kind: kind);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_VerticalPointerState createNewPointerState(PointerDownEvent event) {
|
_VerticalPointerState createNewPointerState(PointerDownEvent event) {
|
||||||
@ -528,8 +540,9 @@ class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer<_Dela
|
|||||||
DelayedMultiDragGestureRecognizer({
|
DelayedMultiDragGestureRecognizer({
|
||||||
this.delay = kLongPressTimeout,
|
this.delay = kLongPressTimeout,
|
||||||
Object debugOwner,
|
Object debugOwner,
|
||||||
|
PointerDeviceKind kind,
|
||||||
}) : assert(delay != null),
|
}) : 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
|
/// The amount of time the pointer must remain in the same place for the drag
|
||||||
/// to be recognized.
|
/// to be recognized.
|
||||||
|
@ -69,7 +69,12 @@ class _TapTracker {
|
|||||||
/// quick succession.
|
/// quick succession.
|
||||||
class DoubleTapGestureRecognizer extends GestureRecognizer {
|
class DoubleTapGestureRecognizer extends GestureRecognizer {
|
||||||
/// Create a gesture recognizer for double taps.
|
/// 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:
|
// Implementation notes:
|
||||||
// The double tap recognizer can be in one of four states. There's no
|
// 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>{};
|
final Map<int, _TapTracker> _trackers = <int, _TapTracker>{};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addPointer(PointerEvent event) {
|
void addAllowedPointer(PointerEvent event) {
|
||||||
// Ignore out-of-bounds second taps.
|
// Ignore out-of-bounds second taps.
|
||||||
if (_firstTap != null &&
|
if (_firstTap != null &&
|
||||||
!_firstTap.isWithinTolerance(event, kDoubleTapSlop))
|
!_firstTap.isWithinTolerance(event, kDoubleTapSlop))
|
||||||
@ -318,7 +323,8 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
|
|||||||
MultiTapGestureRecognizer({
|
MultiTapGestureRecognizer({
|
||||||
this.longTapDelay = Duration.zero,
|
this.longTapDelay = Duration.zero,
|
||||||
Object debugOwner,
|
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
|
/// A pointer that might cause a tap has contacted the screen at a particular
|
||||||
/// location.
|
/// location.
|
||||||
@ -345,7 +351,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
|
|||||||
final Map<int, _TapGesture> _gestureMap = <int, _TapGesture>{};
|
final Map<int, _TapGesture> _gestureMap = <int, _TapGesture>{};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addPointer(PointerEvent event) {
|
void addAllowedPointer(PointerEvent event) {
|
||||||
assert(!_gestureMap.containsKey(event.pointer));
|
assert(!_gestureMap.containsKey(event.pointer));
|
||||||
_gestureMap[event.pointer] = _TapGesture(
|
_gestureMap[event.pointer] = _TapGesture(
|
||||||
gestureRecognizer: this,
|
gestureRecognizer: this,
|
||||||
|
@ -33,7 +33,7 @@ typedef RecognizerCallback<T> = T Function();
|
|||||||
///
|
///
|
||||||
/// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
|
/// * [DragGestureRecognizer.dragStartBehavior], which gives an example for the different behaviors.
|
||||||
enum DragStartBehavior {
|
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.
|
/// detected.
|
||||||
down,
|
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
|
/// The argument is optional and is only used for debug purposes (e.g. in the
|
||||||
/// [toString] serialization).
|
/// [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.
|
/// The recognizer's owner.
|
||||||
///
|
///
|
||||||
@ -66,6 +72,10 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
|
|||||||
/// this gesture recognizer was created, to aid in debugging.
|
/// this gesture recognizer was created, to aid in debugging.
|
||||||
final Object debugOwner;
|
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
|
/// Registers a new pointer that might be relevant to this gesture
|
||||||
/// detector.
|
/// detector.
|
||||||
///
|
///
|
||||||
@ -78,7 +88,43 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
|
|||||||
/// subsequent events for this pointer, and to add the pointer to
|
/// subsequent events for this pointer, and to add the pointer to
|
||||||
/// the global gesture arena manager (see [GestureArenaManager]) to track
|
/// the global gesture arena manager (see [GestureArenaManager]) to track
|
||||||
/// that pointer.
|
/// 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.
|
/// 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.
|
/// simultaneous touches to each result in a separate tap.
|
||||||
abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
|
abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
|
||||||
/// Initialize the object.
|
/// 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 Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
|
||||||
final Set<int> _trackedPointers = HashSet<int>();
|
final Set<int> _trackedPointers = HashSet<int>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void handleNonAllowedPointer(PointerDownEvent event) {
|
||||||
|
resolve(GestureDisposition.rejected);
|
||||||
|
}
|
||||||
|
|
||||||
/// Called when a pointer event is routed to this recognizer.
|
/// Called when a pointer event is routed to this recognizer.
|
||||||
@protected
|
@protected
|
||||||
void handleEvent(PointerEvent event);
|
void handleEvent(PointerEvent event);
|
||||||
@ -291,11 +347,14 @@ enum GestureRecognizerState {
|
|||||||
/// in the gesture arena, the gesture will be rejected.
|
/// in the gesture arena, the gesture will be rejected.
|
||||||
abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer {
|
abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
/// Initializes the [deadline] field during construction of subclasses.
|
/// Initializes the [deadline] field during construction of subclasses.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.gestures.gestureRecognizer.kind}
|
||||||
PrimaryPointerGestureRecognizer({
|
PrimaryPointerGestureRecognizer({
|
||||||
this.deadline,
|
this.deadline,
|
||||||
this.preAcceptSlopTolerance = kTouchSlop,
|
this.preAcceptSlopTolerance = kTouchSlop,
|
||||||
this.postAcceptSlopTolerance = kTouchSlop,
|
this.postAcceptSlopTolerance = kTouchSlop,
|
||||||
Object debugOwner,
|
Object debugOwner,
|
||||||
|
PointerDeviceKind kind,
|
||||||
}) : assert(
|
}) : assert(
|
||||||
preAcceptSlopTolerance == null || preAcceptSlopTolerance >= 0,
|
preAcceptSlopTolerance == null || preAcceptSlopTolerance >= 0,
|
||||||
'The preAcceptSlopTolerance must be positive or null',
|
'The preAcceptSlopTolerance must be positive or null',
|
||||||
@ -304,7 +363,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
|
|||||||
postAcceptSlopTolerance == null || postAcceptSlopTolerance >= 0,
|
postAcceptSlopTolerance == null || postAcceptSlopTolerance >= 0,
|
||||||
'The postAcceptSlopTolerance must be positive or null',
|
'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
|
/// If non-null, the recognizer will call [didExceedDeadline] after this
|
||||||
/// amount of time has elapsed since starting to track the primary pointer.
|
/// amount of time has elapsed since starting to track the primary pointer.
|
||||||
@ -346,7 +405,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
|
|||||||
Timer _timer;
|
Timer _timer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addPointer(PointerDownEvent event) {
|
void addAllowedPointer(PointerDownEvent event) {
|
||||||
startTrackingPointer(event.pointer);
|
startTrackingPointer(event.pointer);
|
||||||
if (state == GestureRecognizerState.ready) {
|
if (state == GestureRecognizerState.ready) {
|
||||||
state = GestureRecognizerState.possible;
|
state = GestureRecognizerState.possible;
|
||||||
|
@ -183,7 +183,12 @@ class _LineBetweenPointers{
|
|||||||
/// are no longer in contact with the screen, the recognizer calls [onEnd].
|
/// are no longer in contact with the screen, the recognizer calls [onEnd].
|
||||||
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
/// Create a gesture recognizer for interactions intended for scaling content.
|
/// 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
|
/// The pointers in contact with the screen have established a focal point and
|
||||||
/// initial scale of 1.0.
|
/// initial scale of 1.0.
|
||||||
@ -239,7 +244,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addPointer(PointerEvent event) {
|
void addAllowedPointer(PointerEvent event) {
|
||||||
startTrackingPointer(event.pointer);
|
startTrackingPointer(event.pointer);
|
||||||
_velocityTrackers[event.pointer] = VelocityTracker();
|
_velocityTrackers[event.pointer] = VelocityTracker();
|
||||||
if (_state == _ScaleState.ready) {
|
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 MaterialInkController inkController = Material.of(context);
|
||||||
final ThemeData themeData = Theme.of(context);
|
final ThemeData themeData = Theme.of(context);
|
||||||
final BuildContext editableContext = _editableTextKey.currentContext;
|
final BuildContext editableContext = _editableTextKey.currentContext;
|
||||||
final RenderBox referenceBox = InputDecorator.containerOf(editableContext) ?? editableContext.findRenderObject();
|
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;
|
final Color color = themeData.splashColor;
|
||||||
|
|
||||||
InteractiveInkFeature splash;
|
InteractiveInkFeature splash;
|
||||||
@ -637,7 +637,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
|||||||
|
|
||||||
void _handleTapDown(TapDownDetails details) {
|
void _handleTapDown(TapDownDetails details) {
|
||||||
_renderEditable.handleTapDown(details);
|
_renderEditable.handleTapDown(details);
|
||||||
_startSplash(details);
|
_startSplash(details.globalPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleForcePressStarted(ForcePressDetails details) {
|
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)
|
if (_effectiveFocusNode.hasFocus)
|
||||||
return;
|
return;
|
||||||
final InteractiveInkFeature splash = _createInkFeature(details);
|
final InteractiveInkFeature splash = _createInkFeature(globalPosition);
|
||||||
_splashes ??= HashSet<InteractiveInkFeature>();
|
_splashes ??= HashSet<InteractiveInkFeature>();
|
||||||
_splashes.add(splash);
|
_splashes.add(splash);
|
||||||
_currentSplash = splash;
|
_currentSplash = splash;
|
||||||
@ -888,6 +907,8 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
|||||||
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
|
onSingleLongTapMoveUpdate: _handleSingleLongTapMoveUpdate,
|
||||||
onSingleLongTapEnd: _handleSingleLongTapEnd,
|
onSingleLongTapEnd: _handleSingleLongTapEnd,
|
||||||
onDoubleTapDown: _handleDoubleTapDown,
|
onDoubleTapDown: _handleDoubleTapDown,
|
||||||
|
onDragSelectionStart: _handleDragSelectionStart,
|
||||||
|
onDragSelectionUpdate: _handleDragSelectionUpdate,
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
|
@ -55,6 +55,10 @@ enum SelectionChangedCause {
|
|||||||
/// Keyboard-triggered selection changes may be caused by the IME as well as
|
/// Keyboard-triggered selection changes may be caused by the IME as well as
|
||||||
/// by accessibility tools (e.g. TalkBack on Android).
|
/// by accessibility tools (e.g. TalkBack on Android).
|
||||||
keyboard,
|
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.
|
/// 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
|
/// 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.
|
/// callback.
|
||||||
///
|
///
|
||||||
/// When [ignorePointer] is true, an ancestor widget must respond to long
|
/// 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
|
// When the team wins a gesture the recognizer notifies the engine that it should release
|
||||||
// the touch sequence to the embedded UIView.
|
// the touch sequence to the embedded UIView.
|
||||||
class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer {
|
class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
_UiKitViewGestureRecognizer(this.controller, this.gestureRecognizerFactories) {
|
_UiKitViewGestureRecognizer(this.controller, this.gestureRecognizerFactories, {
|
||||||
|
PointerDeviceKind kind,
|
||||||
|
}): super(kind: kind) {
|
||||||
team = GestureArenaTeam();
|
team = GestureArenaTeam();
|
||||||
team.captain = this;
|
team.captain = this;
|
||||||
_gestureRecognizers = gestureRecognizerFactories.map(
|
_gestureRecognizers = gestureRecognizerFactories.map(
|
||||||
@ -387,7 +389,7 @@ class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
final UiKitViewController controller;
|
final UiKitViewController controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addPointer(PointerDownEvent event) {
|
void addAllowedPointer(PointerDownEvent event) {
|
||||||
startTrackingPointer(event.pointer);
|
startTrackingPointer(event.pointer);
|
||||||
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
|
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
|
||||||
recognizer.addPointer(event);
|
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
|
// 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.
|
// sets itself to a "forwarding mode" where it will forward any new pointer event to the Android view.
|
||||||
class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
|
class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||||
_AndroidViewGestureRecognizer(this.dispatcher, this.gestureRecognizerFactories) {
|
_AndroidViewGestureRecognizer(this.dispatcher, this.gestureRecognizerFactories, {
|
||||||
|
PointerDeviceKind kind,
|
||||||
|
}): super(kind: kind) {
|
||||||
team = GestureArenaTeam();
|
team = GestureArenaTeam();
|
||||||
team.captain = this;
|
team.captain = this;
|
||||||
_gestureRecognizers = gestureRecognizerFactories.map(
|
_gestureRecognizers = gestureRecognizerFactories.map(
|
||||||
@ -456,7 +460,7 @@ class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
Set<OneSequenceGestureRecognizer> _gestureRecognizers;
|
Set<OneSequenceGestureRecognizer> _gestureRecognizers;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void addPointer(PointerDownEvent event) {
|
void addAllowedPointer(PointerDownEvent event) {
|
||||||
startTrackingPointer(event.pointer);
|
startTrackingPointer(event.pointer);
|
||||||
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
|
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
|
||||||
recognizer.addPointer(event);
|
recognizer.addPointer(event);
|
||||||
|
@ -8,7 +8,7 @@ import 'package:flutter/gestures.dart' show kDoubleTapTimeout, kDoubleTapSlop;
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/gestures.dart' show DragStartBehavior;
|
import 'package:flutter/gestures.dart';
|
||||||
|
|
||||||
import 'basic.dart';
|
import 'basic.dart';
|
||||||
import 'container.dart';
|
import 'container.dart';
|
||||||
@ -20,6 +20,10 @@ import 'transitions.dart';
|
|||||||
|
|
||||||
export 'package:flutter/services.dart' show TextSelectionDelegate;
|
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.
|
/// Which type of selection handle to be displayed.
|
||||||
///
|
///
|
||||||
/// With mixed-direction text, both handles may be the same type. Examples:
|
/// With mixed-direction text, both handles may be the same type. Examples:
|
||||||
@ -64,6 +68,19 @@ enum _TextSelectionHandlePosition { start, end }
|
|||||||
/// Used by [TextSelectionOverlay.onSelectionOverlayChanged].
|
/// Used by [TextSelectionOverlay.onSelectionOverlayChanged].
|
||||||
typedef TextSelectionOverlayChanged = void Function(TextEditingValue value, Rect caretRect);
|
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
|
/// An interface for building the selection UI, to be provided by the
|
||||||
/// implementor of the toolbar widget.
|
/// implementor of the toolbar widget.
|
||||||
///
|
///
|
||||||
@ -620,6 +637,9 @@ class TextSelectionGestureDetector extends StatefulWidget {
|
|||||||
this.onSingleLongTapMoveUpdate,
|
this.onSingleLongTapMoveUpdate,
|
||||||
this.onSingleLongTapEnd,
|
this.onSingleLongTapEnd,
|
||||||
this.onDoubleTapDown,
|
this.onDoubleTapDown,
|
||||||
|
this.onDragSelectionStart,
|
||||||
|
this.onDragSelectionUpdate,
|
||||||
|
this.onDragSelectionEnd,
|
||||||
this.behavior,
|
this.behavior,
|
||||||
@required this.child,
|
@required this.child,
|
||||||
}) : assert(child != null),
|
}) : assert(child != null),
|
||||||
@ -664,6 +684,19 @@ class TextSelectionGestureDetector extends StatefulWidget {
|
|||||||
/// time (within [kDoubleTapTimeout]) to a previous short tap.
|
/// time (within [kDoubleTapTimeout]) to a previous short tap.
|
||||||
final GestureTapDownCallback onDoubleTapDown;
|
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.
|
/// How this gesture detector should behave during hit testing.
|
||||||
///
|
///
|
||||||
/// This defaults to [HitTestBehavior.deferToChild].
|
/// This defaults to [HitTestBehavior.deferToChild].
|
||||||
@ -687,6 +720,7 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_doubleTapTimer?.cancel();
|
_doubleTapTimer?.cancel();
|
||||||
|
_dragUpdateThrottleTimer?.cancel();
|
||||||
super.dispose();
|
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) {
|
void _forcePressStarted(ForcePressDetails details) {
|
||||||
_doubleTapTimer?.cancel();
|
_doubleTapTimer?.cancel();
|
||||||
_doubleTapTimer = null;
|
_doubleTapTimer = null;
|
||||||
@ -754,7 +838,7 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleLongPressUp(LongPressEndDetails details) {
|
void _handleLongPressEnd(LongPressEndDetails details) {
|
||||||
if (!_isDoubleTap && widget.onSingleLongTapEnd != null) {
|
if (!_isDoubleTap && widget.onSingleLongTapEnd != null) {
|
||||||
widget.onSingleLongTapEnd(details);
|
widget.onSingleLongTapEnd(details);
|
||||||
}
|
}
|
||||||
@ -778,15 +862,64 @@ class _TextSelectionGestureDetectorState extends State<TextSelectionGestureDetec
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
|
||||||
onTapDown: _handleTapDown,
|
|
||||||
onTapUp: _handleTapUp,
|
gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
|
||||||
onForcePressStart: widget.onForcePressStart != null ? _forcePressStarted : null,
|
() => TapGestureRecognizer(debugOwner: this),
|
||||||
onForcePressEnd: widget.onForcePressEnd != null ? _forcePressEnded : null,
|
(TapGestureRecognizer instance) {
|
||||||
onTapCancel: _handleTapCancel,
|
instance
|
||||||
onLongPressStart: _handleLongPressStart,
|
..onTapDown = _handleTapDown
|
||||||
onLongPressMoveUpdate: _handleLongPressMoveUpdate,
|
..onTapUp = _handleTapUp
|
||||||
onLongPressEnd: _handleLongPressUp,
|
..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,
|
excludeFromSemantics: true,
|
||||||
behavior: widget.behavior,
|
behavior: widget.behavior,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
|
@ -517,4 +517,83 @@ void main() {
|
|||||||
tester.route(pointer.up());
|
tester.route(pointer.up());
|
||||||
drag.dispose();
|
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();
|
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);
|
expect(didStartDrag, isTrue);
|
||||||
drag.dispose();
|
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();
|
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();
|
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) {
|
testGesture('Scale gesture competes with drag', (GestureTester tester) {
|
||||||
final ScaleGestureRecognizer scale = ScaleGestureRecognizer();
|
final ScaleGestureRecognizer scale = ScaleGestureRecognizer();
|
||||||
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
|
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
|
||||||
|
@ -12,7 +12,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/foundation.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 '../widgets/semantics_tester.dart';
|
||||||
import 'feedback_tester.dart';
|
import 'feedback_tester.dart';
|
||||||
@ -533,6 +533,37 @@ void main() {
|
|||||||
expect(controller.selection.extentOffset, testValue.indexOf('f')+1);
|
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 {
|
testWidgets('enableInteractiveSelection = false, long-press', (WidgetTester tester) async {
|
||||||
final TextEditingController controller = TextEditingController();
|
final TextEditingController controller = TextEditingController();
|
||||||
|
|
||||||
@ -564,6 +595,69 @@ void main() {
|
|||||||
expect(controller.selection.extentOffset, -1);
|
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 {
|
testWidgets('Can drag handles to change selection', (WidgetTester tester) async {
|
||||||
final TextEditingController controller = TextEditingController();
|
final TextEditingController controller = TextEditingController();
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter/gestures.dart' show PointerDeviceKind;
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@ -13,9 +14,11 @@ void main() {
|
|||||||
int doubleTapDownCount;
|
int doubleTapDownCount;
|
||||||
int forcePressStartCount;
|
int forcePressStartCount;
|
||||||
int forcePressEndCount;
|
int forcePressEndCount;
|
||||||
|
int dragStartCount;
|
||||||
|
int dragUpdateCount;
|
||||||
|
int dragEndCount;
|
||||||
const Offset forcePressOffset = Offset(400.0, 50.0);
|
const Offset forcePressOffset = Offset(400.0, 50.0);
|
||||||
|
|
||||||
|
|
||||||
void _handleTapDown(TapDownDetails details) { tapCount++; }
|
void _handleTapDown(TapDownDetails details) { tapCount++; }
|
||||||
void _handleSingleTapUp(TapUpDetails details) { singleTapUpCount++; }
|
void _handleSingleTapUp(TapUpDetails details) { singleTapUpCount++; }
|
||||||
void _handleSingleTapCancel() { singleTapCancelCount++; }
|
void _handleSingleTapCancel() { singleTapCancelCount++; }
|
||||||
@ -23,6 +26,9 @@ void main() {
|
|||||||
void _handleDoubleTapDown(TapDownDetails details) { doubleTapDownCount++; }
|
void _handleDoubleTapDown(TapDownDetails details) { doubleTapDownCount++; }
|
||||||
void _handleForcePressStart(ForcePressDetails details) { forcePressStartCount++; }
|
void _handleForcePressStart(ForcePressDetails details) { forcePressStartCount++; }
|
||||||
void _handleForcePressEnd(ForcePressDetails details) { forcePressEndCount++; }
|
void _handleForcePressEnd(ForcePressDetails details) { forcePressEndCount++; }
|
||||||
|
void _handleDragSelectionStart(DragStartDetails details) { dragStartCount++; }
|
||||||
|
void _handleDragSelectionUpdate(DragStartDetails _, DragUpdateDetails details) { dragUpdateCount++; }
|
||||||
|
void _handleDragSelectionEnd(DragEndDetails details) { dragEndCount++; }
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
tapCount = 0;
|
tapCount = 0;
|
||||||
@ -32,6 +38,9 @@ void main() {
|
|||||||
doubleTapDownCount = 0;
|
doubleTapDownCount = 0;
|
||||||
forcePressStartCount = 0;
|
forcePressStartCount = 0;
|
||||||
forcePressEndCount = 0;
|
forcePressEndCount = 0;
|
||||||
|
dragStartCount = 0;
|
||||||
|
dragUpdateCount = 0;
|
||||||
|
dragEndCount = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> pumpGestureDetector(WidgetTester tester) async {
|
Future<void> pumpGestureDetector(WidgetTester tester) async {
|
||||||
@ -45,6 +54,9 @@ void main() {
|
|||||||
onDoubleTapDown: _handleDoubleTapDown,
|
onDoubleTapDown: _handleDoubleTapDown,
|
||||||
onForcePressStart: _handleForcePressStart,
|
onForcePressStart: _handleForcePressStart,
|
||||||
onForcePressEnd: _handleForcePressEnd,
|
onForcePressEnd: _handleForcePressEnd,
|
||||||
|
onDragSelectionStart: _handleDragSelectionStart,
|
||||||
|
onDragSelectionUpdate: _handleDragSelectionUpdate,
|
||||||
|
onDragSelectionEnd: _handleDragSelectionEnd,
|
||||||
child: Container(),
|
child: Container(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -275,4 +287,89 @@ void main() {
|
|||||||
expect(forcePressEndCount, 1);
|
expect(forcePressEndCount, 1);
|
||||||
expect(doubleTapDownCount, 0);
|
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
|
/// You can use [createGesture] if your gesture doesn't begin with an initial
|
||||||
/// down gesture.
|
/// down gesture.
|
||||||
Future<TestGesture> startGesture(Offset downLocation, {int pointer}) async {
|
Future<TestGesture> startGesture(
|
||||||
final TestGesture result = await createGesture(pointer: pointer);
|
Offset downLocation, {
|
||||||
|
int pointer,
|
||||||
|
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||||
|
}) async {
|
||||||
|
final TestGesture result = await createGesture(pointer: pointer, kind: kind);
|
||||||
await result.down(downLocation);
|
await result.down(downLocation);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user