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
metrics,
simulation,
context.vsync,
activity?.shouldIgnorePointer ?? true,
);
case _NestedBallisticScrollActivityMode.inner:
return _NestedInnerBallisticScrollActivity(
......@@ -1389,9 +1390,10 @@ class _NestedScrollPosition extends ScrollPosition implements ScrollActivityDele
this,
simulation,
context.vsync,
activity?.shouldIgnorePointer ?? true,
);
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 {
_NestedScrollPosition position,
Simulation simulation,
TickerProvider vsync,
) : super(position, simulation, vsync);
bool shouldIgnorePointer,
) : super(position, simulation, vsync, shouldIgnorePointer);
final _NestedScrollCoordinator coordinator;
......@@ -1499,9 +1502,10 @@ class _NestedOuterBallisticScrollActivity extends BallisticScrollActivity {
this.metrics,
Simulation simulation,
TickerProvider vsync,
bool shouldIgnorePointer,
) : assert(metrics.minRange != metrics.maxRange),
assert(metrics.maxRange > metrics.minRange),
super(position, simulation, vsync);
super(position, simulation, vsync, shouldIgnorePointer);
final _NestedScrollCoordinator coordinator;
final _NestedScrollMetrics metrics;
......
......@@ -244,6 +244,7 @@ class ScrollDragController implements Drag {
_lastDetails = details,
_retainMomentum = carriedVelocity != null && carriedVelocity != 0.0,
_lastNonStationaryTimestamp = details.sourceTimeStamp,
_kind = details.kind,
_offsetSinceLastStop = motionStartDistanceThreshold == null ? null : 0.0;
/// The object that will actuate the scroll view as the user drags.
......@@ -424,6 +425,8 @@ class ScrollDragController implements Drag {
onDragCanceled?.call();
}
/// The type of input device driving the drag.
final PointerDeviceKind? _kind;
/// The most recently observed [DragStartDetails], [DragUpdateDetails], or
/// [DragEndDetails] object.
dynamic get lastDetails => _lastDetails;
......@@ -483,7 +486,7 @@ class DragScrollActivity extends ScrollActivity {
}
@override
bool get shouldIgnorePointer => true;
bool get shouldIgnorePointer => _controller?._kind != PointerDeviceKind.trackpad;
@override
bool get isScrolling => true;
......@@ -526,6 +529,7 @@ class BallisticScrollActivity extends ScrollActivity {
super.delegate,
Simulation simulation,
TickerProvider vsync,
this.shouldIgnorePointer,
) {
_controller = AnimationController.unbounded(
debugLabel: kDebugMode ? objectRuntimeType(this, 'BallisticScrollActivity') : null,
......@@ -576,7 +580,7 @@ class BallisticScrollActivity extends ScrollActivity {
}
@override
bool get shouldIgnorePointer => true;
final bool shouldIgnorePointer;
@override
bool get isScrolling => true;
......
......@@ -141,7 +141,7 @@ class ScrollPositionWithSingleContext extends ScrollPosition implements ScrollAc
assert(hasPixels);
final Simulation? simulation = physics.createBallisticSimulation(this, velocity);
if (simulation != null) {
beginActivity(BallisticScrollActivity(this, simulation, context.vsync));
beginActivity(BallisticScrollActivity(this, simulation, context.vsync, activity?.shouldIgnorePointer ?? true));
} else {
goIdle();
}
......
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -127,6 +128,86 @@ void main() {
await tester.pump();
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 {
......
......@@ -388,6 +388,7 @@ abstract class WidgetController {
Offset initialOffset = Offset.zero,
Duration initialOffsetDelay = const Duration(seconds: 1),
bool warnIfMissed = true,
PointerDeviceKind deviceKind = PointerDeviceKind.touch,
}) {
return flingFrom(
getCenter(finder, warnIfMissed: warnIfMissed, callee: 'fling'),
......@@ -398,6 +399,7 @@ abstract class WidgetController {
frameInterval: frameInterval,
initialOffset: initialOffset,
initialOffsetDelay: initialOffsetDelay,
deviceKind: deviceKind,
);
}
......@@ -417,11 +419,12 @@ abstract class WidgetController {
Duration frameInterval = const Duration(milliseconds: 16),
Offset initialOffset = Offset.zero,
Duration initialOffsetDelay = const Duration(seconds: 1),
PointerDeviceKind deviceKind = PointerDeviceKind.touch,
}) {
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.touch, null, buttons);
final TestPointer testPointer = TestPointer(pointer ?? _getNextPointer(), deviceKind, 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;
......@@ -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
/// received from the Flutter engine.
///
......
......@@ -530,6 +530,43 @@ class TestGesture {
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
......
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