Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions packages/flutter/lib/src/gestures/monodrag.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
PointerDeviceKind? kind,
this.dragStartBehavior = DragStartBehavior.start,
this.velocityTrackerBuilder = _defaultBuilder,
this.supportedDevices = _kAllPointerDeviceKinds

@tvolkert tvolkert May 4, 2021 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's weird that that there's a kind argument here too. You should document the interplay between these two (that either can be used to disallow a pointer type). Maybe even use an example, like what happens if they specify:

kind: PointerDeviceKind.touch,
supportedDevices: <PointerDeviceKind>{PointerDeviceKind.mouse},

Might also make sense to add an assert that they're not specifying a combo that doesn't make any sense.

You could even try to change GestureRecognizer.kind to be "kinds" and a Set (ref: #78853), but then we're in breaking change territory.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kinds is definitely odd. If I had the chance I would probably re-do how we do touch slop entirely.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having kinds and supportedDevices on here is odd. The docs for 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.

and supportedDevices also read pretty similar in what they do. A little more documentation contrasting the two is a good idea.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering whether we should deprecate kind altogether now and redirect people to supportedDevices to simplify the API.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I was thinking, we should deprecate kind. I can write a dart fix and a migration guide to help. :) I already wrote a really similar one for some other recent changes to ScrollBehavior.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Touched base with @jonahwilliams. I'll open a follow up change to see what this will look like. :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up is in #81858

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @tvolkert for the FYI.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just coming back around to close the loop on this, flutter/website#5771 has a migration guide for those seeking to restore the prior behavior. :)

}) : assert(dragStartBehavior != null),
super(debugOwner: debugOwner, kind: kind);

Expand Down Expand Up @@ -200,6 +201,11 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// match the native behavior on that platform.
GestureVelocityTrackerBuilder velocityTrackerBuilder;

/// The device types that this gesture recognizer will accept drags from.
///
/// If not specified, defaults to all pointer kinds.
Set<PointerDeviceKind> supportedDevices;

_DragState _state = _DragState.ready;
late OffsetPair _initialPosition;
late OffsetPair _pendingDragOffset;
Expand Down Expand Up @@ -230,6 +236,9 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {

@override
bool isPointerAllowed(PointerEvent event) {
if (!supportedDevices.contains(event.kind)) {
return false;
}
if (_initialButtons == null) {
switch (event.buttons) {
case kPrimaryButton:
Expand Down Expand Up @@ -508,7 +517,8 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
VerticalDragGestureRecognizer({
Object? debugOwner,
PointerDeviceKind? kind,
}) : super(debugOwner: debugOwner, kind: kind);
Set<PointerDeviceKind> supportedDevices = _kAllPointerDeviceKinds,
}) : super(debugOwner: debugOwner, kind: kind, supportedDevices: supportedDevices);

@override
bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
Expand All @@ -532,6 +542,10 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
String get debugDescription => 'vertical drag';
}

const Set<PointerDeviceKind> _kAllPointerDeviceKinds = <PointerDeviceKind>{
...PointerDeviceKind.values,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know we could do this!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤩

};

/// Recognizes movement in the horizontal direction.
///
/// Used for horizontal scrolling.
Expand All @@ -549,7 +563,8 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
HorizontalDragGestureRecognizer({
Object? debugOwner,
PointerDeviceKind? kind,
}) : super(debugOwner: debugOwner, kind: kind);
Set<PointerDeviceKind> supportedDevices = _kAllPointerDeviceKinds,
}) : super(debugOwner: debugOwner, kind: kind, supportedDevices: supportedDevices);

@override
bool isFlingGesture(VelocityEstimate estimate, PointerDeviceKind kind) {
Expand Down
27 changes: 26 additions & 1 deletion packages/flutter/lib/src/widgets/scroll_configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ import 'scrollbar.dart';

const Color _kDefaultGlowColor = Color(0xFFFFFFFF);

/// Device types that scrollables should accept drag gestures from by default.
const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
PointerDeviceKind.touch,
PointerDeviceKind.stylus,
PointerDeviceKind.invertedStylus,
};

/// Describes how [Scrollable] widgets should behave.
///
/// {@template flutter.widgets.scrollBehavior}
Expand Down Expand Up @@ -52,6 +59,7 @@ class ScrollBehavior {
ScrollBehavior copyWith({
bool scrollbars = true,
bool overscroll = true,
Set<PointerDeviceKind>? dragDevices,
ScrollPhysics? physics,
TargetPlatform? platform,
}) {
Expand All @@ -61,6 +69,7 @@ class ScrollBehavior {
overscrollIndicator: overscroll,
physics: physics,
platform: platform,
dragDevices: dragDevices,
);
}

Expand All @@ -69,6 +78,14 @@ class ScrollBehavior {
/// Defaults to the current platform.
TargetPlatform getPlatform(BuildContext context) => defaultTargetPlatform;

/// The device kinds that the scrollable will accept drag gestures from.
///
/// By default only [PointerDeviceKind.touch], [PointerDeviceKind.stylus], and
/// [PointerDeviceKind.invertedStylus] are configured to create drag gestures.
/// Enabling this for [PointerDeviceKind.mouse] will make it difficult or
/// impossible to select text in scrollable containers and is not recommended.
Set<PointerDeviceKind> get dragDevices => _kTouchLikeDeviceTypes;

/// Wraps the given widget, which scrolls in the given [AxisDirection].
///
/// For example, on Android, this method wraps the given widget with a
Expand Down Expand Up @@ -200,13 +217,18 @@ class _WrappedScrollBehavior implements ScrollBehavior {
this.overscrollIndicator = true,
this.physics,
this.platform,
});
Set<PointerDeviceKind>? dragDevices,
}) : _dragDevices = dragDevices;

final ScrollBehavior delegate;
final bool scrollbar;
final bool overscrollIndicator;
final ScrollPhysics? physics;
final TargetPlatform? platform;
final Set<PointerDeviceKind>? _dragDevices;

@override
Set<PointerDeviceKind> get dragDevices => _dragDevices ?? delegate.dragDevices;

@override
Widget buildOverscrollIndicator(BuildContext context, Widget child, ScrollableDetails details) {
Expand All @@ -233,12 +255,14 @@ class _WrappedScrollBehavior implements ScrollBehavior {
bool overscroll = true,
ScrollPhysics? physics,
TargetPlatform? platform,
Set<PointerDeviceKind>? dragDevices,
}) {
return delegate.copyWith(
scrollbars: scrollbars,
overscroll: overscroll,
physics: physics,
platform: platform,
dragDevices: dragDevices,
);
}

Expand All @@ -259,6 +283,7 @@ class _WrappedScrollBehavior implements ScrollBehavior {
|| oldDelegate.overscrollIndicator != overscrollIndicator
|| oldDelegate.physics != physics
|| oldDelegate.platform != platform
|| setEquals<PointerDeviceKind>(oldDelegate.dragDevices, dragDevices)
|| delegate.shouldNotify(oldDelegate.delegate);
}

Expand Down
6 changes: 4 additions & 2 deletions packages/flutter/lib/src/widgets/scrollable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
..minFlingVelocity = _physics?.minFlingVelocity
..maxFlingVelocity = _physics?.maxFlingVelocity
..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context)
..dragStartBehavior = widget.dragStartBehavior;
..dragStartBehavior = widget.dragStartBehavior
..supportedDevices = _configuration.dragDevices;
},
),
};
Expand All @@ -585,7 +586,8 @@ class ScrollableState extends State<Scrollable> with TickerProviderStateMixin, R
..minFlingVelocity = _physics?.minFlingVelocity
..maxFlingVelocity = _physics?.maxFlingVelocity
..velocityTrackerBuilder = _configuration.velocityTrackerBuilder(context)
..dragStartBehavior = widget.dragStartBehavior;
..dragStartBehavior = widget.dragStartBehavior
..supportedDevices = _configuration.dragDevices;
},
),
};
Expand Down
100 changes: 100 additions & 0 deletions packages/flutter/test/gestures/drag_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,106 @@ void main() {
didEndDrag = false;
});

testGesture('Should reject mouse drag when configured to ignore mouse pointers - Horizontal', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer(supportedDevices: <PointerDeviceKind>{
PointerDeviceKind.touch,
}) ..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);

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;
};

final TestPointer pointer = TestPointer(5, PointerDeviceKind.mouse);
final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0));
drag.addPointer(down);
tester.closeArena(5);
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);

tester.route(down);
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);

tester.route(pointer.move(const Offset(20.0, 25.0)));
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);

tester.route(pointer.move(const Offset(20.0, 25.0)));
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);

tester.route(pointer.up());
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
});

testGesture('Should reject mouse drag when configured to ignore mouse pointers - Vertical', (GestureTester tester) {
final VerticalDragGestureRecognizer drag = VerticalDragGestureRecognizer(supportedDevices: <PointerDeviceKind>{
PointerDeviceKind.touch,
})..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);

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;
};

final TestPointer pointer = TestPointer(5, PointerDeviceKind.mouse);
final PointerDownEvent down = pointer.down(const Offset(10.0, 10.0));
drag.addPointer(down);
tester.closeArena(5);
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);

tester.route(down);
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);

tester.route(pointer.move(const Offset(25.0, 20.0)));
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);

tester.route(pointer.move(const Offset(25.0, 20.0)));
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);

tester.route(pointer.up());
expect(didStartDrag, isFalse);
expect(updatedDelta, isNull);
expect(didEndDrag, isFalse);
});

testGesture('Should report original timestamps', (GestureTester tester) {
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer() ..dragStartBehavior = DragStartBehavior.down;
addTearDown(drag.dispose);
Expand Down
46 changes: 45 additions & 1 deletion packages/flutter/test/widgets/scrollable_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ Future<void> pumpTest(
bool scrollable = true,
bool reverse = false,
ScrollController? controller,
bool enableMouseDrag = true,
}) async {
await tester.pumpWidget(MaterialApp(
scrollBehavior: const NoScrollbarBehavior(),
scrollBehavior: const NoScrollbarBehavior().copyWith(dragDevices: enableMouseDrag
? <ui.PointerDeviceKind>{...ui.PointerDeviceKind.values}
: null,
),
theme: ThemeData(
platform: platform,
),
Expand Down Expand Up @@ -1269,6 +1273,46 @@ void main() {

expect(tester.takeException(), null);
});

testWidgets('Does not scroll with mouse pointer drag when behavior is configured to ignore them', (WidgetTester tester) async {
await pumpTest(tester, debugDefaultTargetPlatformOverride, enableMouseDrag: false);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true), kind: ui.PointerDeviceKind.mouse);

await gesture.moveBy(const Offset(0.0, -200));
await tester.pump();
await tester.pumpAndSettle();

expect(getScrollOffset(tester), 0.0);

await gesture.moveBy(const Offset(0.0, 200));
await tester.pump();
await tester.pumpAndSettle();

expect(getScrollOffset(tester), 0.0);

await gesture.removePointer();
await tester.pump();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS, TargetPlatform.android }));

testWidgets('Does scroll with mouse pointer drag when behavior is not configured to ignore them', (WidgetTester tester) async {
await pumpTest(tester, debugDefaultTargetPlatformOverride, enableMouseDrag: true);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byType(Scrollable), warnIfMissed: true), kind: ui.PointerDeviceKind.mouse);

await gesture.moveBy(const Offset(0.0, -200));
await tester.pump();
await tester.pumpAndSettle();

expect(getScrollOffset(tester), 200.0);

await gesture.moveBy(const Offset(0.0, 200));
await tester.pump();
await tester.pumpAndSettle();

expect(getScrollOffset(tester), 0.0);

await gesture.removePointer();
await tester.pump();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS, TargetPlatform.android }));
}

// ignore: must_be_immutable
Expand Down
5 changes: 4 additions & 1 deletion packages/flutter_test/lib/src/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ abstract class WidgetController {
double touchSlopX = kDragSlopDefault,
double touchSlopY = kDragSlopDefault,
bool warnIfMissed = true,
PointerDeviceKind kind = PointerDeviceKind.touch,
}) {
return dragFrom(
getCenter(finder, warnIfMissed: warnIfMissed, callee: 'drag'),
Expand All @@ -538,6 +539,7 @@ abstract class WidgetController {
buttons: buttons,
touchSlopX: touchSlopX,
touchSlopY: touchSlopY,
kind: kind,
);
}

Expand All @@ -559,10 +561,11 @@ abstract class WidgetController {
int buttons = kPrimaryButton,
double touchSlopX = kDragSlopDefault,
double touchSlopY = kDragSlopDefault,
PointerDeviceKind kind = PointerDeviceKind.touch,
}) {
assert(kDragSlopDefault > kTouchSlop);
return TestAsyncUtils.guard<void>(() async {
final TestGesture gesture = await startGesture(startLocation, pointer: pointer, buttons: buttons);
final TestGesture gesture = await startGesture(startLocation, pointer: pointer, buttons: buttons, kind: kind);
assert(gesture != null);

final double xSign = offset.dx.sign;
Expand Down