mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add allowedButtonsFilter
to prevent Draggable from appearing with secondary click. (#111852)
* DragTarget part 1. [WIP] Change GestureRecognizer. Sorry. [WIP] Move from GestureRecognizer to MultiDragGestureRecognizer. Make it a `Set<int>?` Get bitwise operations working. Fix test. Rename to allowedInputPointers. Convert into a builder. Improve code with default funciton. Refactor everything again. Rename to buttonEventFilter. Use static function. Fix analyzer. Fix private reference. Use // in private method. * Fix Renzo request. * Add `allowedButtonsFilter` everywhere. * Refactor monoDrag for multi pointer support. * Fix tests? * Change default to always true. * Fix PR comments. * Completely refactor long press. * Add forgotten class. * Revert "Completely refactor long press." This reverts commit 5038e8603e250e8c928b0f1754fb794b7b75738b. * Add default value to LongPress * Refactor doubleTap. * Relax double tap. * Write comment in LongPress. * Use template.
This commit is contained in:
parent
9024a70f07
commit
0752af841e
@ -24,6 +24,7 @@ class EagerGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
)
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -133,6 +133,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
)
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
}) : assert(startPressure != null),
|
||||
assert(peakPressure != null),
|
||||
assert(interpolation != null),
|
||||
|
@ -247,6 +247,8 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
/// The [duration] argument can be used to overwrite the default duration
|
||||
/// after which the long press will be recognized.
|
||||
///
|
||||
/// {@macro flutter.gestures.tap.TapGestureRecognizer.allowedButtonsFilter}
|
||||
///
|
||||
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
|
||||
LongPressGestureRecognizer({
|
||||
Duration? duration,
|
||||
@ -258,6 +260,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.debugOwner,
|
||||
super.allowedButtonsFilter = _defaultButtonAcceptBehavior,
|
||||
}) : super(
|
||||
deadline: duration ?? kLongPressTimeout,
|
||||
);
|
||||
@ -268,6 +271,12 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
// different set of buttons, the gesture is canceled.
|
||||
int? _initialButtons;
|
||||
|
||||
// Accept the input if, and only if, a single button is pressed.
|
||||
static bool _defaultButtonAcceptBehavior(int buttons) =>
|
||||
buttons == kPrimaryButton ||
|
||||
buttons == kSecondaryButton ||
|
||||
buttons == kTertiaryButton;
|
||||
|
||||
/// Called when a pointer has contacted the screen at a particular location
|
||||
/// with a primary button, which might be the start of a long-press.
|
||||
///
|
||||
|
@ -59,9 +59,8 @@ typedef GestureVelocityTrackerBuilder = VelocityTracker Function(PointerEvent ev
|
||||
/// consider using one of its subclasses to recognize specific types for drag
|
||||
/// gestures.
|
||||
///
|
||||
/// [DragGestureRecognizer] competes on pointer events of [kPrimaryButton]
|
||||
/// only when it has at least one non-null callback. If it has no callbacks, it
|
||||
/// is a no-op.
|
||||
/// [DragGestureRecognizer] competes on pointer events only when it has at
|
||||
/// least one non-null callback. If it has no callbacks, it is a no-op.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
@ -84,10 +83,14 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.velocityTrackerBuilder = _defaultBuilder,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter = _defaultButtonAcceptBehavior,
|
||||
}) : assert(dragStartBehavior != null);
|
||||
|
||||
static VelocityTracker _defaultBuilder(PointerEvent event) => VelocityTracker.withKind(event.kind);
|
||||
|
||||
// Accept the input if, and only if, [kPrimaryButton] is pressed.
|
||||
static bool _defaultButtonAcceptBehavior(int buttons) => buttons == kPrimaryButton;
|
||||
|
||||
/// Configure the behavior of offsets passed to [onStart].
|
||||
///
|
||||
/// If set to [DragStartBehavior.start], the [onStart] callback will be called
|
||||
@ -122,7 +125,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [kPrimaryButton], the button this callback responds to.
|
||||
/// * [allowedButtonsFilter], which decides which button will be allowed.
|
||||
/// * [DragDownDetails], which is passed as an argument to this callback.
|
||||
GestureDragDownCallback? onDown;
|
||||
|
||||
@ -137,7 +140,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [kPrimaryButton], the button this callback responds to.
|
||||
/// * [allowedButtonsFilter], which decides which button will be allowed.
|
||||
/// * [DragStartDetails], which is passed as an argument to this callback.
|
||||
GestureDragStartCallback? onStart;
|
||||
|
||||
@ -151,7 +154,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [kPrimaryButton], the button this callback responds to.
|
||||
/// * [allowedButtonsFilter], which decides which button will be allowed.
|
||||
/// * [DragUpdateDetails], which is passed as an argument to this callback.
|
||||
GestureDragUpdateCallback? onUpdate;
|
||||
|
||||
@ -166,7 +169,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [kPrimaryButton], the button this callback responds to.
|
||||
/// * [allowedButtonsFilter], which decides which button will be allowed.
|
||||
/// * [DragEndDetails], which is passed as an argument to this callback.
|
||||
GestureDragEndCallback? onEnd;
|
||||
|
||||
@ -174,7 +177,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [kPrimaryButton], the button this callback responds to.
|
||||
/// * [allowedButtonsFilter], which decides which button will be allowed.
|
||||
GestureDragCancelCallback? onCancel;
|
||||
|
||||
/// The minimum distance an input pointer drag must have moved to
|
||||
@ -251,18 +254,12 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
@override
|
||||
bool isPointerAllowed(PointerEvent event) {
|
||||
if (_initialButtons == null) {
|
||||
switch (event.buttons) {
|
||||
case kPrimaryButton:
|
||||
if (onDown == null &&
|
||||
onStart == null &&
|
||||
onUpdate == null &&
|
||||
onEnd == null &&
|
||||
onCancel == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
if (onDown == null &&
|
||||
onStart == null &&
|
||||
onUpdate == null &&
|
||||
onEnd == null &&
|
||||
onCancel == null) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// There can be multiple drags simultaneously. Their effects are combined.
|
||||
@ -449,7 +446,6 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
}
|
||||
|
||||
void _checkDown() {
|
||||
assert(_initialButtons == kPrimaryButton);
|
||||
if (onDown != null) {
|
||||
final DragDownDetails details = DragDownDetails(
|
||||
globalPosition: _initialPosition.global,
|
||||
@ -460,7 +456,6 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
}
|
||||
|
||||
void _checkStart(Duration timestamp, int pointer) {
|
||||
assert(_initialButtons == kPrimaryButton);
|
||||
if (onStart != null) {
|
||||
final DragStartDetails details = DragStartDetails(
|
||||
sourceTimeStamp: timestamp,
|
||||
@ -479,7 +474,6 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
required Offset globalPosition,
|
||||
Offset? localPosition,
|
||||
}) {
|
||||
assert(_initialButtons == kPrimaryButton);
|
||||
if (onUpdate != null) {
|
||||
final DragUpdateDetails details = DragUpdateDetails(
|
||||
sourceTimeStamp: sourceTimeStamp,
|
||||
@ -493,7 +487,6 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
}
|
||||
|
||||
void _checkEnd(int pointer) {
|
||||
assert(_initialButtons == kPrimaryButton);
|
||||
if (onEnd == null) {
|
||||
return;
|
||||
}
|
||||
@ -530,7 +523,6 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
}
|
||||
|
||||
void _checkCancel() {
|
||||
assert(_initialButtons == kPrimaryButton);
|
||||
if (onCancel != null) {
|
||||
invokeCallback<void>('onCancel', onCancel!);
|
||||
}
|
||||
@ -570,6 +562,7 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
|
||||
)
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -616,6 +609,7 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
|
||||
)
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -654,6 +648,7 @@ class PanGestureRecognizer extends DragGestureRecognizer {
|
||||
PanGestureRecognizer({
|
||||
super.debugOwner,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -223,8 +223,12 @@ abstract class MultiDragGestureRecognizer extends GestureRecognizer {
|
||||
)
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter = _defaultButtonAcceptBehavior,
|
||||
});
|
||||
|
||||
// Accept the input if, and only if, [kPrimaryButton] is pressed.
|
||||
static bool _defaultButtonAcceptBehavior(int buttons) => buttons == kPrimaryButton;
|
||||
|
||||
/// Called when this class recognizes the start of a drag gesture.
|
||||
///
|
||||
/// The remaining notifications for this drag gesture are delivered to the
|
||||
@ -382,6 +386,7 @@ class ImmediateMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
|
||||
)
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -439,6 +444,7 @@ class HorizontalMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
|
||||
)
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -496,6 +502,7 @@ class VerticalMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
|
||||
)
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -606,6 +613,7 @@ class DelayedMultiDragGestureRecognizer extends MultiDragGestureRecognizer {
|
||||
)
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
}) : assert(delay != null);
|
||||
|
||||
/// The amount of time the pointer must remain in the same place for the drag
|
||||
|
@ -113,8 +113,8 @@ class _TapTracker {
|
||||
/// Recognizes when the user has tapped the screen at the same location twice in
|
||||
/// quick succession.
|
||||
///
|
||||
/// [DoubleTapGestureRecognizer] competes on pointer events of [kPrimaryButton]
|
||||
/// only when it has a non-null callback. If it has no callbacks, it is a no-op.
|
||||
/// [DoubleTapGestureRecognizer] competes on pointer events when it
|
||||
/// has a non-null callback. If it has no callbacks, it is a no-op.
|
||||
///
|
||||
class DoubleTapGestureRecognizer extends GestureRecognizer {
|
||||
/// Create a gesture recognizer for double taps.
|
||||
@ -128,8 +128,13 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
|
||||
)
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter = _defaultButtonAcceptBehavior,
|
||||
});
|
||||
|
||||
// The default value for [allowedButtonsFilter].
|
||||
// Accept the input if, and only if, [kPrimaryButton] is pressed.
|
||||
static bool _defaultButtonAcceptBehavior(int buttons) => buttons == kPrimaryButton;
|
||||
|
||||
// Implementation notes:
|
||||
//
|
||||
// The double tap recognizer can be in one of four states. There's no
|
||||
@ -165,7 +170,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [kPrimaryButton], the button this callback responds to.
|
||||
/// * [allowedButtonsFilter], which decides which button will be allowed.
|
||||
/// * [TapDownDetails], which is passed as an argument to this callback.
|
||||
/// * [GestureDetector.onDoubleTapDown], which exposes this callback.
|
||||
GestureTapDownCallback? onDoubleTapDown;
|
||||
@ -178,7 +183,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [kPrimaryButton], the button this callback responds to.
|
||||
/// * [allowedButtonsFilter], which decides which button will be allowed.
|
||||
/// * [GestureDetector.onDoubleTap], which exposes this callback.
|
||||
GestureDoubleTapCallback? onDoubleTap;
|
||||
|
||||
@ -192,7 +197,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [kPrimaryButton], the button this callback responds to.
|
||||
/// * [allowedButtonsFilter], which decides which button will be allowed.
|
||||
/// * [GestureDetector.onDoubleTapCancel], which exposes this callback.
|
||||
GestureTapCancelCallback? onDoubleTapCancel;
|
||||
|
||||
@ -203,19 +208,19 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
|
||||
@override
|
||||
bool isPointerAllowed(PointerDownEvent event) {
|
||||
if (_firstTap == null) {
|
||||
switch (event.buttons) {
|
||||
case kPrimaryButton:
|
||||
if (onDoubleTapDown == null &&
|
||||
onDoubleTap == null &&
|
||||
onDoubleTapCancel == null) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
if (onDoubleTapDown == null &&
|
||||
onDoubleTap == null &&
|
||||
onDoubleTapCancel == null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return super.isPointerAllowed(event);
|
||||
|
||||
// If second tap is not allowed, reset the state.
|
||||
final bool isPointerAllowed = super.isPointerAllowed(event);
|
||||
if (isPointerAllowed == false) {
|
||||
_reset();
|
||||
}
|
||||
return isPointerAllowed;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -367,7 +372,6 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
|
||||
}
|
||||
|
||||
void _checkUp(int buttons) {
|
||||
assert(buttons == kPrimaryButton);
|
||||
if (onDoubleTap != null) {
|
||||
invokeCallback<void>('onDoubleTap', onDoubleTap!);
|
||||
}
|
||||
@ -492,6 +496,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
|
||||
)
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
/// A pointer that might cause a tap has contacted the screen at a particular
|
||||
@ -813,6 +818,7 @@ class SerialTapGestureRecognizer extends GestureRecognizer {
|
||||
SerialTapGestureRecognizer({
|
||||
super.debugOwner,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
/// A pointer has contacted the screen at a particular location, which might
|
||||
|
@ -48,6 +48,11 @@ enum DragStartBehavior {
|
||||
start,
|
||||
}
|
||||
|
||||
/// Signature for `allowedButtonsFilter` in [GestureRecognizer].
|
||||
/// Used to filter the input buttons of incoming pointer events.
|
||||
/// The parameter `buttons` comes from [PointerEvent.buttons].
|
||||
typedef AllowedButtonsFilter = bool Function(int buttons);
|
||||
|
||||
/// The base class that all gesture recognizers inherit from.
|
||||
///
|
||||
/// Provides a basic API that can be used by classes that work with
|
||||
@ -79,8 +84,10 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
|
||||
)
|
||||
PointerDeviceKind? kind,
|
||||
Set<PointerDeviceKind>? supportedDevices,
|
||||
AllowedButtonsFilter? allowedButtonsFilter,
|
||||
}) : assert(kind == null || supportedDevices == null),
|
||||
_supportedDevices = kind == null ? supportedDevices : <PointerDeviceKind>{ kind };
|
||||
_supportedDevices = kind == null ? supportedDevices : <PointerDeviceKind>{ kind },
|
||||
_allowedButtonsFilter = allowedButtonsFilter ?? _defaultButtonAcceptBehavior;
|
||||
|
||||
/// The recognizer's owner.
|
||||
///
|
||||
@ -98,6 +105,29 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
|
||||
/// tracked and recognized.
|
||||
final Set<PointerDeviceKind>? _supportedDevices;
|
||||
|
||||
/// {@template flutter.gestures.multidrag._allowedButtonsFilter}
|
||||
/// Called when interaction starts. This limits the dragging behavior
|
||||
/// for custom clicks (such as scroll click). Its parameter comes
|
||||
/// from [PointerEvent.buttons].
|
||||
///
|
||||
/// Due to how [kPrimaryButton], [kSecondaryButton], etc., use integers,
|
||||
/// bitwise operations can help filter how buttons are pressed.
|
||||
/// For example, if someone simultaneously presses the primary and secondary
|
||||
/// buttons, the default behavior will return false. The following code
|
||||
/// accepts any button press with primary:
|
||||
/// `(int buttons) => buttons & kPrimaryButton != 0`.
|
||||
///
|
||||
/// When value is `(int buttons) => false`, allow no interactions.
|
||||
/// When value is `(int buttons) => true`, allow all interactions.
|
||||
///
|
||||
/// Defaults to all buttons.
|
||||
/// {@endtemplate}
|
||||
final AllowedButtonsFilter _allowedButtonsFilter;
|
||||
|
||||
// The default value for [allowedButtonsFilter].
|
||||
// Accept any input.
|
||||
static bool _defaultButtonAcceptBehavior(int buttons) => true;
|
||||
|
||||
/// Holds a mapping between pointer IDs and the kind of devices they are
|
||||
/// coming from.
|
||||
final Map<int, PointerDeviceKind> _pointerToKind = <int, PointerDeviceKind>{};
|
||||
@ -185,9 +215,9 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
|
||||
/// 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 _supportedDevices == null || _supportedDevices!.contains(event.kind);
|
||||
return (_supportedDevices == null ||
|
||||
_supportedDevices!.contains(event.kind)) &&
|
||||
_allowedButtonsFilter(event.buttons);
|
||||
}
|
||||
|
||||
/// Handles a pointer pan/zoom being added that's not allowed by this recognizer.
|
||||
@ -298,6 +328,7 @@ abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
|
||||
)
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
final Map<int, GestureArenaEntry> _entries = <int, GestureArenaEntry>{};
|
||||
@ -511,6 +542,7 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
|
||||
)
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
}) : assert(
|
||||
preAcceptSlopTolerance == null || preAcceptSlopTolerance >= 0,
|
||||
'The preAcceptSlopTolerance must be positive or null',
|
||||
|
@ -334,6 +334,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
)
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
this.dragStartBehavior = DragStartBehavior.down,
|
||||
this.trackpadScrollCausesScale = false,
|
||||
this.trackpadScrollToScaleFactor = kDefaultTrackpadScrollToScaleFactor,
|
||||
|
@ -149,7 +149,11 @@ abstract class BaseTapGestureRecognizer extends PrimaryPointerGestureRecognizer
|
||||
/// Creates a tap gesture recognizer.
|
||||
///
|
||||
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
|
||||
BaseTapGestureRecognizer({ super.debugOwner, super.supportedDevices })
|
||||
BaseTapGestureRecognizer({
|
||||
super.debugOwner,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
})
|
||||
: super(deadline: kPressTimeout);
|
||||
|
||||
bool _sentTapDown = false;
|
||||
@ -354,6 +358,16 @@ abstract class BaseTapGestureRecognizer extends PrimaryPointerGestureRecognizer
|
||||
/// one non-null `onTertiaryTap*` callback. If it has no callbacks, it is a
|
||||
/// no-op.
|
||||
///
|
||||
/// {@template flutter.gestures.tap.TapGestureRecognizer.allowedButtonsFilter}
|
||||
/// The [allowedButtonsFilter] argument only gives this recognizer the
|
||||
/// ability to limit the buttons it accepts. It does not provide the
|
||||
/// ability to recognize any buttons beyond the ones it already accepts:
|
||||
/// kPrimaryButton, kSecondaryButton or kTertiaryButton. Therefore, a
|
||||
/// combined value of `kPrimaryButton & kSecondaryButton` would be ignored,
|
||||
/// but `kPrimaryButton | kSecondaryButton` would be allowed, as long as
|
||||
/// only one of them is selected at a time.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [GestureDetector.onTap], which uses this recognizer.
|
||||
@ -362,7 +376,11 @@ class TapGestureRecognizer extends BaseTapGestureRecognizer {
|
||||
/// Creates a tap gesture recognizer.
|
||||
///
|
||||
/// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
|
||||
TapGestureRecognizer({ super.debugOwner, super.supportedDevices });
|
||||
TapGestureRecognizer({
|
||||
super.debugOwner,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
/// {@template flutter.gestures.tap.TapGestureRecognizer.onTapDown}
|
||||
/// A pointer has contacted the screen at a particular location with a primary
|
||||
|
@ -179,6 +179,7 @@ class Draggable<T extends Object> extends StatefulWidget {
|
||||
this.ignoringFeedbackPointer = true,
|
||||
this.rootOverlay = false,
|
||||
this.hitTestBehavior = HitTestBehavior.deferToChild,
|
||||
this.allowedButtonsFilter,
|
||||
}) : assert(child != null),
|
||||
assert(feedback != null),
|
||||
assert(ignoringFeedbackSemantics != null),
|
||||
@ -359,6 +360,9 @@ class Draggable<T extends Object> extends StatefulWidget {
|
||||
/// Defaults to [HitTestBehavior.deferToChild].
|
||||
final HitTestBehavior hitTestBehavior;
|
||||
|
||||
/// {@macro flutter.gestures.multidrag._allowedButtonsFilter}
|
||||
final AllowedButtonsFilter? allowedButtonsFilter;
|
||||
|
||||
/// Creates a gesture recognizer that recognizes the start of the drag.
|
||||
///
|
||||
/// Subclasses can override this function to customize when they start
|
||||
@ -367,11 +371,11 @@ class Draggable<T extends Object> extends StatefulWidget {
|
||||
MultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
|
||||
switch (affinity) {
|
||||
case Axis.horizontal:
|
||||
return HorizontalMultiDragGestureRecognizer()..onStart = onStart;
|
||||
return HorizontalMultiDragGestureRecognizer(allowedButtonsFilter: allowedButtonsFilter)..onStart = onStart;
|
||||
case Axis.vertical:
|
||||
return VerticalMultiDragGestureRecognizer()..onStart = onStart;
|
||||
return VerticalMultiDragGestureRecognizer(allowedButtonsFilter: allowedButtonsFilter)..onStart = onStart;
|
||||
case null:
|
||||
return ImmediateMultiDragGestureRecognizer()..onStart = onStart;
|
||||
return ImmediateMultiDragGestureRecognizer(allowedButtonsFilter: allowedButtonsFilter)..onStart = onStart;
|
||||
}
|
||||
}
|
||||
|
||||
@ -409,6 +413,7 @@ class LongPressDraggable<T extends Object> extends Draggable<T> {
|
||||
super.ignoringFeedbackSemantics,
|
||||
super.ignoringFeedbackPointer,
|
||||
this.delay = kLongPressTimeout,
|
||||
super.allowedButtonsFilter,
|
||||
});
|
||||
|
||||
/// Whether haptic feedback should be triggered on drag start.
|
||||
@ -421,7 +426,7 @@ class LongPressDraggable<T extends Object> extends Draggable<T> {
|
||||
|
||||
@override
|
||||
DelayedMultiDragGestureRecognizer createRecognizer(GestureMultiDragStartCallback onStart) {
|
||||
return DelayedMultiDragGestureRecognizer(delay: delay)
|
||||
return DelayedMultiDragGestureRecognizer(delay: delay, allowedButtonsFilter: allowedButtonsFilter)
|
||||
..onStart = (Offset position) {
|
||||
final Drag? result = onStart(position);
|
||||
if (result != null && hapticFeedbackOnStart) {
|
||||
|
@ -702,6 +702,7 @@ class TapAndDragGestureRecognizer extends OneSequenceGestureRecognizer with _Tap
|
||||
super.debugOwner,
|
||||
super.kind,
|
||||
super.supportedDevices,
|
||||
super.allowedButtonsFilter,
|
||||
}) : _deadline = kPressTimeout,
|
||||
dragStartBehavior = DragStartBehavior.start,
|
||||
slopTolerance = kTouchSlop;
|
||||
|
@ -152,6 +152,54 @@ void main() {
|
||||
expect(doubleTapCanceled, isFalse);
|
||||
});
|
||||
|
||||
testGesture('Should recognize double tap with secondaryButton', (GestureTester tester) {
|
||||
final DoubleTapGestureRecognizer tapSecondary = DoubleTapGestureRecognizer(
|
||||
allowedButtonsFilter: (int buttons) => buttons == kSecondaryButton,
|
||||
);
|
||||
tapSecondary.onDoubleTap = () {
|
||||
doubleTapRecognized = true;
|
||||
};
|
||||
tapSecondary.onDoubleTapDown = (TapDownDetails details) {
|
||||
doubleTapDownDetails = details;
|
||||
};
|
||||
tapSecondary.onDoubleTapCancel = () {
|
||||
doubleTapCanceled = true;
|
||||
};
|
||||
|
||||
// Down/up pair 7: normal tap sequence close to pair 6
|
||||
const PointerDownEvent down7 = PointerDownEvent(
|
||||
pointer: 7,
|
||||
position: Offset(10.0, 10.0),
|
||||
buttons: kSecondaryMouseButton,
|
||||
);
|
||||
|
||||
const PointerUpEvent up7 = PointerUpEvent(
|
||||
pointer: 7,
|
||||
position: Offset(11.0, 9.0),
|
||||
);
|
||||
|
||||
tapSecondary.addPointer(down6);
|
||||
tester.closeArena(6);
|
||||
tester.route(down6);
|
||||
tester.route(up6);
|
||||
GestureBinding.instance.gestureArena.sweep(6);
|
||||
expect(doubleTapDownDetails, isNull);
|
||||
|
||||
tester.async.elapse(const Duration(milliseconds: 100));
|
||||
tapSecondary.addPointer(down7);
|
||||
tester.closeArena(7);
|
||||
expect(doubleTapDownDetails, isNotNull);
|
||||
expect(doubleTapDownDetails!.globalPosition, down7.position);
|
||||
expect(doubleTapDownDetails!.localPosition, down7.localPosition);
|
||||
tester.route(down7);
|
||||
expect(doubleTapRecognized, isFalse);
|
||||
|
||||
tester.route(up7);
|
||||
expect(doubleTapRecognized, isTrue);
|
||||
GestureBinding.instance.gestureArena.sweep(2);
|
||||
expect(doubleTapCanceled, isFalse);
|
||||
});
|
||||
|
||||
testGesture('Inter-tap distance cancels double tap', (GestureTester tester) {
|
||||
tap.addPointer(down1);
|
||||
tester.closeArena(1);
|
||||
@ -493,6 +541,56 @@ void main() {
|
||||
expect(doubleTapCanceled, isFalse);
|
||||
});
|
||||
|
||||
testGesture('Button change with allowedButtonsFilter should interrupt existing sequence', (GestureTester tester) {
|
||||
final DoubleTapGestureRecognizer tapPrimary = DoubleTapGestureRecognizer(
|
||||
allowedButtonsFilter: (int buttons) => buttons == kPrimaryButton,
|
||||
);
|
||||
tapPrimary.onDoubleTap = () {
|
||||
doubleTapRecognized = true;
|
||||
};
|
||||
tapPrimary.onDoubleTapDown = (TapDownDetails details) {
|
||||
doubleTapDownDetails = details;
|
||||
};
|
||||
tapPrimary.onDoubleTapCancel = () {
|
||||
doubleTapCanceled = true;
|
||||
};
|
||||
|
||||
// Down1 -> down6 (different button from 1) -> down2 (same button as 1)
|
||||
// Down1 and down2 could've been a double tap, but is interrupted by down 6.
|
||||
// Down6 gets ignored because it's not a primary button. Regardless, the state
|
||||
// is reset.
|
||||
const Duration interval = Duration(milliseconds: 100);
|
||||
assert(interval * 2 < kDoubleTapTimeout);
|
||||
assert(interval > kDoubleTapMinTime);
|
||||
|
||||
tapPrimary.addPointer(down1);
|
||||
tester.closeArena(1);
|
||||
tester.route(down1);
|
||||
tester.route(up1);
|
||||
GestureBinding.instance.gestureArena.sweep(1);
|
||||
|
||||
tester.async.elapse(interval);
|
||||
|
||||
tapPrimary.addPointer(down6);
|
||||
tester.closeArena(6);
|
||||
tester.route(down6);
|
||||
tester.route(up6);
|
||||
GestureBinding.instance.gestureArena.sweep(6);
|
||||
|
||||
tester.async.elapse(interval);
|
||||
expect(doubleTapRecognized, isFalse);
|
||||
|
||||
tapPrimary.addPointer(down2);
|
||||
tester.closeArena(2);
|
||||
tester.route(down2);
|
||||
tester.route(up2);
|
||||
GestureBinding.instance.gestureArena.sweep(2);
|
||||
|
||||
expect(doubleTapRecognized, isFalse);
|
||||
expect(doubleTapDownDetails, isNull);
|
||||
expect(doubleTapCanceled, isFalse);
|
||||
});
|
||||
|
||||
testGesture('Button change should start a valid sequence', (GestureTester tester) {
|
||||
// Down6 -> down1 (different button from 6) -> down2 (same button as 1)
|
||||
|
||||
@ -624,6 +722,44 @@ void main() {
|
||||
doubleTap.dispose();
|
||||
});
|
||||
|
||||
testGesture('Buttons filter should cancel invalid taps', (GestureTester tester) {
|
||||
final List<String> recognized = <String>[];
|
||||
final DoubleTapGestureRecognizer doubleTap = DoubleTapGestureRecognizer(
|
||||
allowedButtonsFilter: (int buttons) => false,
|
||||
)
|
||||
..onDoubleTap = () {
|
||||
recognized.add('primary');
|
||||
};
|
||||
|
||||
// Down/up pair 7: normal tap sequence close to pair 6
|
||||
const PointerDownEvent down7 = PointerDownEvent(
|
||||
pointer: 7,
|
||||
position: Offset(10.0, 10.0),
|
||||
);
|
||||
|
||||
const PointerUpEvent up7 = PointerUpEvent(
|
||||
pointer: 7,
|
||||
position: Offset(11.0, 9.0),
|
||||
);
|
||||
|
||||
doubleTap.addPointer(down7);
|
||||
tester.closeArena(7);
|
||||
tester.route(down7);
|
||||
tester.route(up7);
|
||||
GestureBinding.instance.gestureArena.sweep(7);
|
||||
|
||||
tester.async.elapse(const Duration(milliseconds: 100));
|
||||
doubleTap.addPointer(down6);
|
||||
tester.closeArena(6);
|
||||
tester.route(down6);
|
||||
tester.route(up6);
|
||||
|
||||
expect(recognized, <String>[]);
|
||||
|
||||
recognized.clear();
|
||||
doubleTap.dispose();
|
||||
});
|
||||
|
||||
// Regression test for https://github.com/flutter/flutter/issues/73667
|
||||
testGesture('Unfinished DoubleTap does not prevent competing Tap', (GestureTester tester) {
|
||||
int tapCount = 0;
|
||||
|
@ -78,6 +78,57 @@ void main() {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
group('Recognizers on different button filters:', () {
|
||||
final List<String> recognized = <String>[];
|
||||
late HorizontalDragGestureRecognizer primaryRecognizer;
|
||||
late HorizontalDragGestureRecognizer secondaryRecognizer;
|
||||
setUp(() {
|
||||
primaryRecognizer = HorizontalDragGestureRecognizer(
|
||||
allowedButtonsFilter: (int buttons) => kPrimaryButton == buttons)
|
||||
..onStart = (DragStartDetails details) {
|
||||
recognized.add('onStartPrimary');
|
||||
};
|
||||
secondaryRecognizer = HorizontalDragGestureRecognizer(
|
||||
allowedButtonsFilter: (int buttons) => kSecondaryButton == buttons)
|
||||
..onStart = (DragStartDetails details) {
|
||||
recognized.add('onStartSecondary');
|
||||
};
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
recognized.clear();
|
||||
primaryRecognizer.dispose();
|
||||
secondaryRecognizer.dispose();
|
||||
});
|
||||
|
||||
testGesture('Primary button works', (GestureTester tester) {
|
||||
const PointerDownEvent down1 = PointerDownEvent(
|
||||
pointer: 6,
|
||||
position: Offset(10.0, 10.0),
|
||||
);
|
||||
|
||||
primaryRecognizer.addPointer(down1);
|
||||
secondaryRecognizer.addPointer(down1);
|
||||
tester.closeArena(down1.pointer);
|
||||
tester.route(down1);
|
||||
expect(recognized, <String>['onStartPrimary']);
|
||||
});
|
||||
|
||||
testGesture('Secondary button works', (GestureTester tester) {
|
||||
const PointerDownEvent down1 = PointerDownEvent(
|
||||
pointer: 6,
|
||||
position: Offset(10.0, 10.0),
|
||||
buttons: kSecondaryMouseButton,
|
||||
);
|
||||
|
||||
primaryRecognizer.addPointer(down1);
|
||||
secondaryRecognizer.addPointer(down1);
|
||||
tester.closeArena(down1.pointer);
|
||||
tester.route(down1);
|
||||
expect(recognized, <String>['onStartSecondary']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class MockHitTestTarget implements HitTestTarget {
|
||||
|
@ -3195,6 +3195,48 @@ void main() {
|
||||
expect(const LongPressDraggable<int>(feedback: widget2, child: widget1).feedback, widget2);
|
||||
expect(LongPressDraggable<int>(feedback: widget2, dragAnchorStrategy: dummyStrategy, child: widget1).dragAnchorStrategy, dummyStrategy);
|
||||
});
|
||||
|
||||
testWidgets('Test allowedButtonsFilter', (WidgetTester tester) async {
|
||||
Widget build(bool Function(int buttons)? allowedButtonsFilter) {
|
||||
return MaterialApp(
|
||||
home: Draggable<int>(
|
||||
key: UniqueKey(),
|
||||
allowedButtonsFilter: allowedButtonsFilter,
|
||||
feedback: const Text('Dragging'),
|
||||
child: const Text('Source'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(build(null));
|
||||
final Offset firstLocation = tester.getCenter(find.text('Source'));
|
||||
expect(find.text('Dragging'), findsNothing);
|
||||
final TestGesture gesture = await tester.startGesture(firstLocation, pointer: 7);
|
||||
await tester.pump();
|
||||
expect(find.text('Dragging'), findsOneWidget);
|
||||
await gesture.up();
|
||||
|
||||
await tester.pumpWidget(build((int buttons) => buttons == kSecondaryButton));
|
||||
expect(find.text('Dragging'), findsNothing);
|
||||
final TestGesture gesture1 = await tester.startGesture(firstLocation, pointer: 8);
|
||||
await tester.pump();
|
||||
expect(find.text('Dragging'), findsNothing);
|
||||
await gesture1.up();
|
||||
|
||||
await tester.pumpWidget(build((int buttons) => buttons & kTertiaryButton != 0 || buttons & kPrimaryButton != 0));
|
||||
expect(find.text('Dragging'), findsNothing);
|
||||
final TestGesture gesture2 = await tester.startGesture(firstLocation, pointer: 8);
|
||||
await tester.pump();
|
||||
expect(find.text('Dragging'), findsOneWidget);
|
||||
await gesture2.up();
|
||||
|
||||
await tester.pumpWidget(build((int buttons) => false));
|
||||
expect(find.text('Dragging'), findsNothing);
|
||||
final TestGesture gesture3 = await tester.startGesture(firstLocation, pointer: 8);
|
||||
await tester.pump();
|
||||
expect(find.text('Dragging'), findsNothing);
|
||||
await gesture3.up();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _testLongPressDraggableHapticFeedback({ required WidgetTester tester, required bool hapticFeedbackOnStart, required int expectedHapticFeedbackCount }) async {
|
||||
|
Loading…
Reference in New Issue
Block a user