Unverified Commit 09e400ea authored by Callum Moffat's avatar Callum Moffat Committed by GitHub

Don't disable pointer interaction during trackpad scroll (#106890)

parent 9a4a9f7e
...@@ -1382,6 +1382,7 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele ...@@ -1382,6 +1382,7 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
metrics, metrics,
simulation, simulation,
context.vsync, context.vsync,
activity?.shouldIgnorePointer ?? true,
); );
case _NestedBallisticScrollActivityMode.inner: case _NestedBallisticScrollActivityMode.inner:
return _NestedInnerBallisticScrollActivity( return _NestedInnerBallisticScrollActivity(
...@@ -1389,9 +1390,10 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele ...@@ -1389,9 +1390,10 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
this, this,
simulation, simulation,
context.vsync, context.vsync,
activity?.shouldIgnorePointer ?? true,
); );
case _NestedBallisticScrollActivityMode.independent: case _NestedBallisticScrollActivityMode.independent:
return BallisticScrollActivity(this, simulation, context.vsync); return BallisticScrollActivity(this, simulation, context.vsync, activity?.shouldIgnorePointer ?? true);
} }
} }
...@@ -1463,7 +1465,8 @@ class _NestedInnerBallisticScrollActivity extends BallisticScrollActivity { ...@@ -1463,7 +1465,8 @@ class _NestedInnerBallisticScrollActivity extends BallisticScrollActivity {
_NestedScrollPosition position, _NestedScrollPosition position,
Simulation simulation, Simulation simulation,
TickerProvider vsync, TickerProvider vsync,
) : super(position, simulation, vsync); bool shouldIgnorePointer,
) : super(position, simulation, vsync, shouldIgnorePointer);
final _NestedScrollCoordinator coordinator; final _NestedScrollCoordinator coordinator;
...@@ -1499,9 +1502,10 @@ class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity { ...@@ -1499,9 +1502,10 @@ class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity {
this.metrics, this.metrics,
Simulation simulation, Simulation simulation,
TickerProvider vsync, TickerProvider vsync,
bool shouldIgnorePointer,
) : assert(metrics.minRange != metrics.maxRange), ) : assert(metrics.minRange != metrics.maxRange),
assert(metrics.maxRange > metrics.minRange), assert(metrics.maxRange > metrics.minRange),
super(position, simulation, vsync); super(position, simulation, vsync, shouldIgnorePointer);
final _NestedScrollCoordinator coordinator; final _NestedScrollCoordinator coordinator;
final _NestedScrollMetrics metrics; final _NestedScrollMetrics metrics;
......
...@@ -244,6 +244,7 @@ class ScrollDragController implements Drag { ...@@ -244,6 +244,7 @@ class ScrollDragController implements Drag {
_lastDetails = details, _lastDetails = details,
_retainMomentum = carriedVelocity != null && carriedVelocity != 0.0, _retainMomentum = carriedVelocity != null && carriedVelocity != 0.0,
_lastNonStationaryTimestamp = details.sourceTimeStamp, _lastNonStationaryTimestamp = details.sourceTimeStamp,
_kind = details.kind,
_offsetSinceLastStop = motionStartDistanceThreshold == null ? null : 0.0; _offsetSinceLastStop = motionStartDistanceThreshold == null ? null : 0.0;
/// The object that will actuate the scroll view as the user drags. /// The object that will actuate the scroll view as the user drags.
...@@ -424,6 +425,8 @@ class ScrollDragController implements Drag { ...@@ -424,6 +425,8 @@ class ScrollDragController implements Drag {
onDragCanceled?.call(); onDragCanceled?.call();
} }
/// The type of input device driving the drag.
final PointerDeviceKind? _kind;
/// The most recently observed [DragStartDetails], [DragUpdateDetails], or /// The most recently observed [DragStartDetails], [DragUpdateDetails], or
/// [DragEndDetails] object. /// [DragEndDetails] object.
dynamic get lastDetails => _lastDetails; dynamic get lastDetails => _lastDetails;
...@@ -483,7 +486,7 @@ class DragScrollActivity extends ScrollActivity { ...@@ -483,7 +486,7 @@ class DragScrollActivity extends ScrollActivity {
} }
@override @override
bool get shouldIgnorePointer => true; bool get shouldIgnorePointer => _controller?._kind != PointerDeviceKind.trackpad;
@override @override
bool get isScrolling => true; bool get isScrolling => true;
...@@ -526,6 +529,7 @@ class BallisticScrollActivity extends ScrollActivity { ...@@ -526,6 +529,7 @@ class BallisticScrollActivity extends ScrollActivity {
super.delegate, super.delegate,
Simulation simulation, Simulation simulation,
TickerProvider vsync, TickerProvider vsync,
this.shouldIgnorePointer,
) { ) {
_controller = AnimationController.unbounded( _controller = AnimationController.unbounded(
debugLabel: kDebugMode ? objectRuntimeType(this, 'BallisticScrollActivity') : null, debugLabel: kDebugMode ? objectRuntimeType(this, 'BallisticScrollActivity') : null,
...@@ -576,7 +580,7 @@ class BallisticScrollActivity extends ScrollActivity { ...@@ -576,7 +580,7 @@ class BallisticScrollActivity extends ScrollActivity {
} }
@override @override
bool get shouldIgnorePointer => true; final bool shouldIgnorePointer;
@override @override
bool get isScrolling => true; bool get isScrolling => true;
......
...@@ -141,7 +141,7 @@ class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollAc ...@@ -141,7 +141,7 @@ class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollAc
assert(hasPixels); assert(hasPixels);
final Simulation? simulation = physics.createBallisticSimulation(this, velocity); final Simulation? simulation = physics.createBallisticSimulation(this, velocity);
if (simulation != null) { if (simulation != null) {
beginActivity(BallisticScrollActivity(this, simulation, context.vsync)); beginActivity(BallisticScrollActivity(this, simulation, context.vsync, activity?.shouldIgnorePointer ?? true));
} else { } else {
goIdle(); goIdle();
} }
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -127,6 +128,86 @@ void main() { ...@@ -127,6 +128,86 @@ void main() {
await tester.pump(); await tester.pump();
expect(find.text('Page 9'), findsOneWidget); expect(find.text('Page 9'), findsOneWidget);
}); });
testWidgets('Pointer is not ignored during trackpad scrolling.', (WidgetTester tester) async {
final ScrollController controller = ScrollController();
int? lastTapped;
int? lastHovered;
await tester.pumpWidget(MaterialApp(
home: ListView(
controller: controller,
children: List<Widget>.generate(30, (int i) {
return SizedBox(height: 100.0, child: MouseRegion(
onHover: (PointerHoverEvent event) {
lastHovered = i;
},
child: GestureDetector(
onTap: () {
lastTapped = i;
},
child: Text('$i')
)
));
})
)
));
final TestGesture touchGesture = await tester.createGesture(kind: PointerDeviceKind.touch); // ignore: avoid_redundant_argument_values
// Try mouse hovering while scrolling by touch
await touchGesture.down(tester.getCenter(find.byType(ListView)));
await tester.pump();
await touchGesture.moveBy(const Offset(0, 200));
await tester.pump();
final TestGesture hoverGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await hoverGesture.addPointer(
location: tester.getCenter(find.text('3'))
);
await hoverGesture.moveBy(const Offset(1, 1));
await hoverGesture.removePointer(
location: tester.getCenter(find.text('3'))
);
await tester.pumpAndSettle();
expect(controller.position.activity?.shouldIgnorePointer, isTrue); // Pointer is ignored for touch scrolling.
expect(lastHovered, isNull);
await touchGesture.up();
await tester.pump();
// Try mouse clicking during inertia after scrolling by touch
await tester.fling(find.byType(ListView), const Offset(0, -200), 1000);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(controller.position.activity?.shouldIgnorePointer, isTrue); // Pointer is ignored following touch scrolling.
await tester.tap(find.text('3'), warnIfMissed: false);
expect(lastTapped, isNull);
await tester.pumpAndSettle();
controller.jumpTo(0);
await tester.pump();
final TestGesture trackpadGesture = await tester.createGesture(kind: PointerDeviceKind.trackpad);
// Try mouse hovering while scrolling with a trackpad
await trackpadGesture.panZoomStart(tester.getCenter(find.byType(ListView)));
await tester.pump();
await trackpadGesture.panZoomUpdate(tester.getCenter(find.byType(ListView)), pan: const Offset(0, 200));
await tester.pump();
await hoverGesture.addPointer(
location: tester.getCenter(find.text('3'))
);
await hoverGesture.moveBy(const Offset(1, 1));
await hoverGesture.removePointer(
location: tester.getCenter(find.text('3'))
);
await tester.pumpAndSettle();
expect(controller.position.activity?.shouldIgnorePointer, isFalse); // Pointer is not ignored for trackpad scrolling.
expect(lastHovered, equals(3));
await trackpadGesture.panZoomEnd();
await tester.pump();
// Try mouse clicking during inertia after scrolling with a trackpad
await tester.trackpadFling(find.byType(ListView), const Offset(0, -200), 1000);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(controller.position.activity?.shouldIgnorePointer, isFalse); // Pointer is not ignored following trackpad scrolling.
await tester.tap(find.text('3'));
expect(lastTapped, equals(3));
await tester.pumpAndSettle();
});
} }
class PageView62209 extends StatefulWidget { class PageView62209 extends StatefulWidget {
......
...@@ -388,6 +388,7 @@ abstract class WidgetController { ...@@ -388,6 +388,7 @@ abstract class WidgetController {
Offset initialOffset = Offset.zero, Offset initialOffset = Offset.zero,
Duration initialOffsetDelay = const Duration(seconds: 1), Duration initialOffsetDelay = const Duration(seconds: 1),
bool warnIfMissed = true, bool warnIfMissed = true,
PointerDeviceKind deviceKind = PointerDeviceKind.touch,
}) { }) {
return flingFrom( return flingFrom(
getCenter(finder, warnIfMissed: warnIfMissed, callee: 'fling'), getCenter(finder, warnIfMissed: warnIfMissed, callee: 'fling'),
...@@ -398,6 +399,7 @@ abstract class WidgetController { ...@@ -398,6 +399,7 @@ abstract class WidgetController {
frameInterval: frameInterval, frameInterval: frameInterval,
initialOffset: initialOffset, initialOffset: initialOffset,
initialOffsetDelay: initialOffsetDelay, initialOffsetDelay: initialOffsetDelay,
deviceKind: deviceKind,
); );
} }
...@@ -417,11 +419,12 @@ abstract class WidgetController { ...@@ -417,11 +419,12 @@ abstract class WidgetController {
Duration frameInterval = const Duration(milliseconds: 16), Duration frameInterval = const Duration(milliseconds: 16),
Offset initialOffset = Offset.zero, Offset initialOffset = Offset.zero,
Duration initialOffsetDelay = const Duration(seconds: 1), Duration initialOffsetDelay = const Duration(seconds: 1),
PointerDeviceKind deviceKind = PointerDeviceKind.touch,
}) { }) {
assert(offset.distance > 0.0); assert(offset.distance > 0.0);
assert(speed > 0.0); // speed is pixels/second assert(speed > 0.0); // speed is pixels/second
return TestAsyncUtils.guard<void>(() async { return TestAsyncUtils.guard<void>(() async {
final TestPointer testPointer = TestPointer(pointer ?? _getNextPointer(), PointerDeviceKind.touch, null, buttons); final TestPointer testPointer = TestPointer(pointer ?? _getNextPointer(), deviceKind, null, buttons);
const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
final double timeStampDelta = 1000000.0 * offset.distance / (kMoveCount * speed); final double timeStampDelta = 1000000.0 * offset.distance / (kMoveCount * speed);
double timeStamp = 0.0; double timeStamp = 0.0;
...@@ -445,6 +448,84 @@ abstract class WidgetController { ...@@ -445,6 +448,84 @@ abstract class WidgetController {
}); });
} }
/// Attempts a trackpad fling gesture starting from the center of the given
/// widget, moving the given distance, reaching the given speed. A trackpad
/// fling sends PointerPanZoom events instead of a sequence of touch events.
///
/// {@macro flutter.flutter_test.WidgetController.tap.warnIfMissed}
///
/// {@macro flutter.flutter_test.WidgetController.fling}
///
/// A fling is essentially a drag that ends at a particular speed. If you
/// just want to drag and end without a fling, use [drag].
Future<void> trackpadFling(
Finder finder,
Offset offset,
double speed, {
int? pointer,
int buttons = kPrimaryButton,
Duration frameInterval = const Duration(milliseconds: 16),
Offset initialOffset = Offset.zero,
Duration initialOffsetDelay = const Duration(seconds: 1),
bool warnIfMissed = true,
}) {
return trackpadFlingFrom(
getCenter(finder, warnIfMissed: warnIfMissed, callee: 'fling'),
offset,
speed,
pointer: pointer,
buttons: buttons,
frameInterval: frameInterval,
initialOffset: initialOffset,
initialOffsetDelay: initialOffsetDelay,
);
}
/// Attempts a fling gesture starting from the given location, moving the
/// given distance, reaching the given speed. A trackpad fling sends
/// PointerPanZoom events instead of a sequence of touch events.
///
/// {@macro flutter.flutter_test.WidgetController.fling}
///
/// A fling is essentially a drag that ends at a particular speed. If you
/// just want to drag and end without a fling, use [dragFrom].
Future<void> trackpadFlingFrom(
Offset startLocation,
Offset offset,
double speed, {
int? pointer,
int buttons = kPrimaryButton,
Duration frameInterval = const Duration(milliseconds: 16),
Offset initialOffset = Offset.zero,
Duration initialOffsetDelay = const Duration(seconds: 1),
}) {
assert(offset.distance > 0.0);
assert(speed > 0.0); // speed is pixels/second
return TestAsyncUtils.guard<void>(() async {
final TestPointer testPointer = TestPointer(pointer ?? _getNextPointer(), PointerDeviceKind.trackpad, null, buttons);
const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
final double timeStampDelta = 1000000.0 * offset.distance / (kMoveCount * speed);
double timeStamp = 0.0;
double lastTimeStamp = timeStamp;
await sendEventToBinding(testPointer.panZoomStart(startLocation, timeStamp: Duration(microseconds: timeStamp.round())));
if (initialOffset.distance > 0.0) {
await sendEventToBinding(testPointer.panZoomUpdate(startLocation, pan: initialOffset, timeStamp: Duration(microseconds: timeStamp.round())));
timeStamp += initialOffsetDelay.inMicroseconds;
await pump(initialOffsetDelay);
}
for (int i = 0; i <= kMoveCount; i += 1) {
final Offset pan = initialOffset + Offset.lerp(Offset.zero, offset, i / kMoveCount)!;
await sendEventToBinding(testPointer.panZoomUpdate(startLocation, pan: pan, timeStamp: Duration(microseconds: timeStamp.round())));
timeStamp += timeStampDelta;
if (timeStamp - lastTimeStamp > frameInterval.inMicroseconds) {
await pump(Duration(microseconds: (timeStamp - lastTimeStamp).truncate()));
lastTimeStamp = timeStamp;
}
}
await sendEventToBinding(testPointer.panZoomEnd(timeStamp: Duration(microseconds: timeStamp.round())));
});
}
/// A simulator of how the framework handles a series of [PointerEvent]s /// A simulator of how the framework handles a series of [PointerEvent]s
/// received from the Flutter engine. /// received from the Flutter engine.
/// ///
......
...@@ -530,6 +530,43 @@ class TestGesture { ...@@ -530,6 +530,43 @@ class TestGesture {
assert(!_pointer._isDown); assert(!_pointer._isDown);
}); });
} }
/// Dispatch a pointer pan zoom start event at the given `location`, caching the
/// hit test result.
Future<void> panZoomStart(Offset location, { Duration timeStamp = Duration.zero }) async {
return TestAsyncUtils.guard<void>(() async {
return _dispatcher(_pointer.panZoomStart(location, timeStamp: timeStamp));
});
}
/// Dispatch a pointer pan zoom update event at the given `location`, caching the
/// hit test result.
Future<void> panZoomUpdate(Offset location, {
Offset pan = Offset.zero,
double scale = 1,
double rotation = 0,
Duration timeStamp = Duration.zero
}) async {
return TestAsyncUtils.guard<void>(() async {
return _dispatcher(_pointer.panZoomUpdate(location,
pan: pan,
scale: scale,
rotation: rotation,
timeStamp: timeStamp
));
});
}
/// Dispatch a pointer pan zoom end event, caching the hit test result.
Future<void> panZoomEnd({
Duration timeStamp = Duration.zero
}) async {
return TestAsyncUtils.guard<void>(() async {
return _dispatcher(_pointer.panZoomEnd(
timeStamp: timeStamp
));
});
}
} }
/// A record of input [PointerEvent] list with the timeStamp of when it is /// A record of input [PointerEvent] list with the timeStamp of when it is
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment