mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[flutter] reject mouse drags by default in scrollables (#81569)
This commit is contained in:
parent
7b3ce8c003
commit
fff8ecfb94
@ -69,6 +69,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
PointerDeviceKind? kind,
|
||||
this.dragStartBehavior = DragStartBehavior.start,
|
||||
this.velocityTrackerBuilder = _defaultBuilder,
|
||||
this.supportedDevices = _kAllPointerDeviceKinds
|
||||
}) : assert(dragStartBehavior != null),
|
||||
super(debugOwner: debugOwner, kind: kind);
|
||||
|
||||
@ -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;
|
||||
@ -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:
|
||||
@ -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) {
|
||||
@ -532,6 +542,10 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
|
||||
String get debugDescription => 'vertical drag';
|
||||
}
|
||||
|
||||
const Set<PointerDeviceKind> _kAllPointerDeviceKinds = <PointerDeviceKind>{
|
||||
...PointerDeviceKind.values,
|
||||
};
|
||||
|
||||
/// Recognizes movement in the horizontal direction.
|
||||
///
|
||||
/// Used for horizontal scrolling.
|
||||
@ -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) {
|
||||
|
||||
@ -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}
|
||||
@ -52,6 +59,7 @@ class ScrollBehavior {
|
||||
ScrollBehavior copyWith({
|
||||
bool scrollbars = true,
|
||||
bool overscroll = true,
|
||||
Set<PointerDeviceKind>? dragDevices,
|
||||
ScrollPhysics? physics,
|
||||
TargetPlatform? platform,
|
||||
}) {
|
||||
@ -61,6 +69,7 @@ class ScrollBehavior {
|
||||
overscrollIndicator: overscroll,
|
||||
physics: physics,
|
||||
platform: platform,
|
||||
dragDevices: dragDevices,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -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) {
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
},
|
||||
),
|
||||
};
|
||||
@ -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;
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
),
|
||||
@ -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
|
||||
|
||||
@ -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'),
|
||||
@ -538,6 +539,7 @@ abstract class WidgetController {
|
||||
buttons: buttons,
|
||||
touchSlopX: touchSlopX,
|
||||
touchSlopY: touchSlopY,
|
||||
kind: kind,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user