Unverified Commit 05097916 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Fix mouse hover to not schedule a frame for every mouse move. (#41014)

This fixes the mouse hover code to not schedule frames with every mouse move.

Before this, it would schedule a post frame callback, and then schedule a frame immediately, even if there was nothing that needed to be updated. Now it will schedule checks for mouse position updates synchronously, unless there's a new annotation, and skip scheduling a new frame in all cases. It has to be async in the case of a new annotation (i.e. a new MouseRegion is added), since when the annotation is added, it hasn't yet painted, and it can't hit test against the new layer until after the paint, so in that case it schedules a post frame callback, but since it's already building a frame when it does that, it doesn't need to schedule a frame.

The code also used to do mouse position checks for all mice if only one mouse changed position. I fixed this part too, so that it will only check position for the mouse that changed.
parent 75552684
......@@ -92,12 +92,21 @@ class MouseTracker extends ChangeNotifier {
/// Creates a mouse tracker to keep track of mouse locations.
///
/// All of the parameters must not be null.
MouseTracker(PointerRouter router, this.annotationFinder)
: assert(router != null),
MouseTracker(this._router, this.annotationFinder)
: assert(_router != null),
assert(annotationFinder != null) {
router.addGlobalRoute(_handleEvent);
_router.addGlobalRoute(_handleEvent);
}
@override
void dispose() {
super.dispose();
_router.removeGlobalRoute(_handleEvent);
}
// The pointer router that the mouse tracker listens to for events.
final PointerRouter _router;
/// Used to find annotations at a given logical coordinate.
final MouseDetectorAnnotationFinder annotationFinder;
......@@ -111,16 +120,19 @@ class MouseTracker extends ChangeNotifier {
/// annotation has been added to the layer tree.
void attachAnnotation(MouseTrackerAnnotation annotation) {
_trackedAnnotations[annotation] = _TrackedAnnotation(annotation);
// Schedule a check so that we test this new annotation to see if the mouse
// is currently inside its region.
// Schedule a check so that we test this new annotation to see if any mouse
// is currently inside its region. It has to happen after the frame is
// complete so that the annotation layer has been added before the check.
if (mouseIsConnected) {
_scheduleMousePositionCheck();
}
}
/// Stops tracking an annotation, indicating that it has been removed from the
/// layer tree.
///
/// If the associated layer is not removed, and receives a hit, then
/// [collectMousePositions] will assert the next time it is called.
/// [sendMouseNotifications] will assert the next time it is called.
void detachAnnotation(MouseTrackerAnnotation annotation) {
final _TrackedAnnotation trackedAnnotation = _findAnnotation(annotation);
for (int deviceId in trackedAnnotation.activeDevices) {
......@@ -133,18 +145,19 @@ class MouseTracker extends ChangeNotifier {
_trackedAnnotations.remove(annotation);
}
bool _postFrameCheckScheduled = false;
bool _scheduledPostFramePositionCheck = false;
// Schedules a position check at the end of this frame for those annotations
// that have been added.
void _scheduleMousePositionCheck() {
// If we're not tracking anything, then there is no point in registering a
// frame callback or scheduling a frame. By definition there are no active
// annotations that need exiting, either.
if (_trackedAnnotations.isNotEmpty && !_postFrameCheckScheduled) {
_postFrameCheckScheduled = true;
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
_postFrameCheckScheduled = false;
collectMousePositions();
if (_trackedAnnotations.isNotEmpty && !_scheduledPostFramePositionCheck) {
_scheduledPostFramePositionCheck = true;
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
sendMouseNotifications(_lastMouseEvent.keys);
_scheduledPostFramePositionCheck = false;
});
SchedulerBinding.instance.scheduleFrame();
}
}
......@@ -158,21 +171,24 @@ class MouseTracker extends ChangeNotifier {
// If we are adding the device again, then we're not removing it anymore.
_pendingRemovals.remove(deviceId);
_addMouseEvent(deviceId, event);
sendMouseNotifications(<int>{deviceId});
return;
}
if (event is PointerRemovedEvent) {
_removeMouseEvent(deviceId, event);
// If the mouse was removed, then we need to schedule one more check to
// exit any annotations that were active.
_scheduleMousePositionCheck();
sendMouseNotifications(<int>{deviceId});
} else {
if (event is PointerMoveEvent || event is PointerHoverEvent || event is PointerDownEvent) {
if (!_lastMouseEvent.containsKey(deviceId) || _lastMouseEvent[deviceId].position != event.position) {
final PointerEvent lastEvent = _lastMouseEvent[deviceId];
_addMouseEvent(deviceId, event);
if (lastEvent == null ||
lastEvent is PointerAddedEvent || lastEvent.position != event.position) {
// Only schedule a frame if we have our first event, or if the
// location of the mouse has changed, and only if there are tracked annotations.
_scheduleMousePositionCheck();
sendMouseNotifications(<int>{deviceId});
}
_addMouseEvent(deviceId, event);
}
}
}
......@@ -206,14 +222,18 @@ class MouseTracker extends ChangeNotifier {
/// This function is only public to allow for proper testing of the
/// MouseTracker. Do not call in other contexts.
@visibleForTesting
void collectMousePositions() {
void sendMouseNotifications(Iterable<int> deviceIds) {
if (_trackedAnnotations.isEmpty) {
return;
}
void exitAnnotation(_TrackedAnnotation trackedAnnotation, int deviceId) {
if (trackedAnnotation.annotation?.onExit != null && trackedAnnotation.activeDevices.contains(deviceId)) {
final PointerEvent event = _lastMouseEvent[deviceId] ?? _pendingRemovals[deviceId];
assert(event != null);
trackedAnnotation.annotation.onExit(PointerExitEvent.fromMouseEvent(event));
trackedAnnotation.activeDevices.remove(deviceId);
}
trackedAnnotation.activeDevices.remove(deviceId);
}
void exitAllDevices(_TrackedAnnotation trackedAnnotation) {
......@@ -234,8 +254,9 @@ class MouseTracker extends ChangeNotifier {
return;
}
for (int deviceId in _lastMouseEvent.keys) {
for (int deviceId in deviceIds) {
final PointerEvent lastEvent = _lastMouseEvent[deviceId];
assert(lastEvent != null);
final Iterable<MouseTrackerAnnotation> hits = annotationFinder(lastEvent.position);
// No annotations were found at this position for this deviceId, so send an
......@@ -311,7 +332,6 @@ class MouseTracker extends ChangeNotifier {
/// The most recent mouse event observed for each mouse device ID observed.
///
/// May be null if no mouse is connected, or hasn't produced an event yet.
/// Will not be updated unless there is at least one tracked annotation.
final Map<int, PointerEvent> _lastMouseEvent = <int, PointerEvent>{};
/// Whether or not a mouse is connected and has produced events.
......
......@@ -43,7 +43,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
_handleSemanticsEnabledChanged();
assert(renderView != null);
addPersistentFrameCallback(_handlePersistentFrameCallback);
_mouseTracker = _createMouseTracker();
initMouseTracker();
}
/// The current [RendererBinding], if one has been created.
......@@ -238,10 +238,14 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture
SemanticsHandle _semanticsHandle;
// Creates a [MouseTracker] which manages state about currently connected
// mice, for hover notification.
MouseTracker _createMouseTracker() {
return MouseTracker(pointerRouter, renderView.hitTestMouseTrackers);
/// Creates a [MouseTracker] which manages state about currently connected
/// mice, for hover notification.
///
/// Used by testing framework to reinitialize the mouse tracker between tests.
@visibleForTesting
void initMouseTracker([MouseTracker tracker]) {
_mouseTracker?.dispose();
_mouseTracker = tracker ?? MouseTracker(pointerRouter, renderView.hitTestMouseTrackers);
}
void _handleSemanticsEnabledChanged() {
......
......@@ -5833,7 +5833,7 @@ class MouseRegion extends SingleChildRenderObjectWidget {
final PointerExitEventListener onExit;
@override
_ListenerElement createElement() => _ListenerElement(this);
_MouseRegionElement createElement() => _MouseRegionElement(this);
@override
RenderMouseRegion createRenderObject(BuildContext context) {
......@@ -5866,20 +5866,20 @@ class MouseRegion extends SingleChildRenderObjectWidget {
}
}
class _ListenerElement extends SingleChildRenderObjectElement {
_ListenerElement(SingleChildRenderObjectWidget widget) : super(widget);
class _MouseRegionElement extends SingleChildRenderObjectElement {
_MouseRegionElement(SingleChildRenderObjectWidget widget) : super(widget);
@override
void activate() {
super.activate();
final RenderMouseRegion renderMouseListener = renderObject;
renderMouseListener.postActivate();
final RenderMouseRegion renderMouseRegion = renderObject;
renderMouseRegion.postActivate();
}
@override
void deactivate() {
final RenderMouseRegion renderMouseListener = renderObject;
renderMouseListener.preDeactivate();
final RenderMouseRegion renderMouseRegion = renderObject;
renderMouseRegion.preDeactivate();
super.deactivate();
}
}
......
......@@ -4,7 +4,9 @@
import 'dart:ui' as ui;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
......@@ -13,7 +15,7 @@ import '../flutter_test_alternative.dart';
typedef HandleEventCallback = void Function(PointerEvent event);
class TestGestureFlutterBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding {
class TestGestureFlutterBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, RendererBinding {
HandleEventCallback callback;
@override
......@@ -35,7 +37,6 @@ void ensureTestGestureBinding() {
void main() {
setUp(ensureTestGestureBinding);
group(MouseTracker, () {
final List<PointerEnterEvent> enter = <PointerEnterEvent>[];
final List<PointerHoverEvent> move = <PointerHoverEvent>[];
final List<PointerExitEvent> exit = <PointerExitEvent>[];
......@@ -51,7 +52,6 @@ void main() {
);
bool isInHitRegionOne;
bool isInHitRegionTwo;
MouseTracker tracker;
void clear() {
enter.clear();
......@@ -63,21 +63,25 @@ void main() {
clear();
isInHitRegionOne = true;
isInHitRegionTwo = false;
tracker = MouseTracker(
RendererBinding.instance.initMouseTracker(
MouseTracker(
GestureBinding.instance.pointerRouter,
(Offset _) sync* {
(Offset position) sync* {
if (isInHitRegionOne)
yield annotation;
else if (isInHitRegionTwo)
else if (isInHitRegionTwo) {
yield partialAnnotation;
}
},
),
);
PointerEventConverter.clearPointers();
});
test('receives and processes mouse hover events', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
change: ui.PointerChange.hover,
change: ui.PointerChange.hover, // Will implicitly also add a PointerAdded event.
physicalX: 0.0 * ui.window.devicePixelRatio,
physicalY: 0.0 * ui.window.devicePixelRatio,
kind: PointerDeviceKind.mouse,
......@@ -116,10 +120,10 @@ void main() {
device: 1,
),
]);
tracker.attachAnnotation(annotation);
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
RendererBinding.instance.mouseTracker.sendMouseNotifications(<int>{0});
isInHitRegionOne = true;
ui.window.onPointerDataPacket(packet1);
tracker.collectMousePositions();
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(0.0, 0.0)));
expect(enter.first.device, equals(0));
......@@ -132,7 +136,6 @@ void main() {
clear();
ui.window.onPointerDataPacket(packet2);
tracker.collectMousePositions();
expect(enter.length, equals(0), reason: 'enter contains $enter');
expect(exit.length, equals(0), reason: 'exit contains $exit');
expect(move.length, equals(1), reason: 'move contains $move');
......@@ -142,9 +145,11 @@ void main() {
clear();
ui.window.onPointerDataPacket(packet3);
tracker.collectMousePositions();
expect(enter.length, equals(0), reason: 'enter contains $enter');
expect(move.length, equals(0), reason: 'move contains $move');
expect(move.length, equals(1), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(1.0, 201.0)));
expect(move.first.device, equals(0));
expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(exit.length, equals(1), reason: 'exit contains $exit');
expect(exit.first.position, equals(const Offset(1.0, 201.0)));
expect(exit.first.device, equals(0));
......@@ -152,7 +157,6 @@ void main() {
clear();
ui.window.onPointerDataPacket(packet4);
tracker.collectMousePositions();
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(1.0, 301.0)));
expect(enter.first.device, equals(0));
......@@ -166,20 +170,21 @@ void main() {
// add in a second mouse simultaneously.
clear();
ui.window.onPointerDataPacket(packet5);
tracker.collectMousePositions();
RendererBinding.instance.mouseTracker.sendMouseNotifications(<int>{1});
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(1.0, 401.0)));
expect(enter.first.device, equals(1));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(exit.length, equals(0), reason: 'exit contains $exit');
expect(move.length, equals(2), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(1.0, 301.0)));
expect(move.first.device, equals(0));
expect(move.first.position, equals(const Offset(1.0, 401.0)));
expect(move.first.device, equals(1));
expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(move.last.position, equals(const Offset(1.0, 401.0)));
expect(move.last.device, equals(1));
expect(move.last.runtimeType, equals(PointerHoverEvent));
});
test('detects exit when annotated layer no longer hit', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
......@@ -204,25 +209,28 @@ void main() {
),
]);
isInHitRegionOne = true;
tracker.attachAnnotation(annotation);
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
RendererBinding.instance.mouseTracker.sendMouseNotifications(<int>{0});
ui.window.onPointerDataPacket(packet1);
tracker.collectMousePositions();
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(1.0, 101.0)));
expect(enter.first.position, equals(const Offset(0.0, 0.0)));
expect(enter.first.device, equals(0));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(move.length, equals(1), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(1.0, 101.0)));
expect(move.length, equals(2), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(0.0, 0.0)));
expect(move.first.device, equals(0));
expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(move.last.position, equals(const Offset(1.0, 101.0)));
expect(move.last.device, equals(0));
expect(move.last.runtimeType, equals(PointerHoverEvent));
expect(exit.length, equals(0), reason: 'exit contains $exit');
// Simulate layer going away by detaching it.
clear();
isInHitRegionOne = false;
ui.window.onPointerDataPacket(packet2);
tracker.collectMousePositions();
expect(enter.length, equals(0), reason: 'enter contains $enter');
expect(move.length, equals(0), reason: 'enter contains $move');
expect(exit.length, equals(1), reason: 'enter contains $exit');
......@@ -231,12 +239,11 @@ void main() {
expect(exit.first.runtimeType, equals(PointerExitEvent));
// Actually detach annotation. Shouldn't receive hit.
tracker.detachAnnotation(annotation);
RendererBinding.instance.mouseTracker.detachAnnotation(annotation);
clear();
isInHitRegionOne = false;
ui.window.onPointerDataPacket(packet2);
tracker.collectMousePositions();
expect(enter.length, equals(0), reason: 'enter contains $enter');
expect(move.length, equals(0), reason: 'enter contains $move');
expect(exit.length, equals(0), reason: 'enter contains $exit');
......@@ -254,13 +261,13 @@ void main() {
isInHitRegionOne = false;
isInHitRegionTwo = true;
tracker.attachAnnotation(partialAnnotation);
RendererBinding.instance.mouseTracker.attachAnnotation(partialAnnotation);
ui.window.onPointerDataPacket(packet);
tracker.collectMousePositions();
tracker.detachAnnotation(partialAnnotation);
RendererBinding.instance.mouseTracker.detachAnnotation(partialAnnotation);
isInHitRegionTwo = false;
});
test('detects exit when mouse goes away', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
......@@ -285,27 +292,35 @@ void main() {
),
]);
isInHitRegionOne = true;
tracker.attachAnnotation(annotation);
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
RendererBinding.instance.mouseTracker.sendMouseNotifications(<int>{0});
ui.window.onPointerDataPacket(packet1);
tracker.collectMousePositions();
ui.window.onPointerDataPacket(packet2);
tracker.collectMousePositions();
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(1.0, 101.0)));
expect(enter.first.delta, equals(const Offset(1.0, 101.0)));
expect(enter.first.position, equals(const Offset(0.0, 0.0)));
expect(enter.first.delta, equals(const Offset(0.0, 0.0)));
expect(enter.first.device, equals(0));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(move.length, equals(1), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(1.0, 101.0)));
expect(move.first.delta, equals(const Offset(1.0, 101.0)));
expect(move.first.device, equals(0));
expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(move.length, equals(3), reason: 'move contains $move');
expect(move[0].position, equals(const Offset(0.0, 0.0)));
expect(move[0].delta, equals(const Offset(0.0, 0.0)));
expect(move[0].device, equals(0));
expect(move[0].runtimeType, equals(PointerHoverEvent));
expect(move[1].position, equals(const Offset(1.0, 101.0)));
expect(move[1].delta, equals(const Offset(1.0, 101.0)));
expect(move[1].device, equals(0));
expect(move[1].runtimeType, equals(PointerHoverEvent));
expect(move[2].position, equals(const Offset(1.0, 201.0)));
expect(move[2].delta, equals(const Offset(0.0, 100.0)));
expect(move[2].device, equals(0));
expect(move[2].runtimeType, equals(PointerHoverEvent));
expect(exit.length, equals(1), reason: 'exit contains $exit');
expect(exit.first.position, equals(const Offset(1.0, 201.0)));
expect(exit.first.delta, equals(const Offset(0.0, 0.0)));
expect(exit.first.device, equals(0));
expect(exit.first.runtimeType, equals(PointerExitEvent));
});
test('handles mouse down and move', () {
final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
ui.PointerData(
......@@ -336,20 +351,21 @@ void main() {
),
]);
isInHitRegionOne = true;
tracker.attachAnnotation(annotation);
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
RendererBinding.instance.mouseTracker.sendMouseNotifications(<int>{0});
ui.window.onPointerDataPacket(packet1);
tracker.collectMousePositions();
ui.window.onPointerDataPacket(packet2);
tracker.collectMousePositions();
expect(enter.length, equals(1), reason: 'enter contains $enter');
expect(enter.first.position, equals(const Offset(1.0, 101.0)));
expect(enter.first.position, equals(const Offset(0.0, 0.0)));
expect(enter.first.device, equals(0));
expect(enter.first.runtimeType, equals(PointerEnterEvent));
expect(move.length, equals(1), reason: 'move contains $move');
expect(move.first.position, equals(const Offset(1.0, 101.0)));
expect(move.first.device, equals(0));
expect(move.first.runtimeType, equals(PointerHoverEvent));
expect(move.length, equals(2), reason: 'move contains $move');
expect(move[0].position, equals(const Offset(0.0, 0.0)));
expect(move[0].device, equals(0));
expect(move[0].runtimeType, equals(PointerHoverEvent));
expect(move[1].position, equals(const Offset(1.0, 101.0)));
expect(move[1].device, equals(0));
expect(move[1].runtimeType, equals(PointerHoverEvent));
expect(exit.length, equals(0), reason: 'exit contains $exit');
});
});
}
......@@ -388,7 +388,7 @@ void main() {
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
);
await gesture.addPointer();
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await gesture.moveTo(center);
await tester.pumpAndSettle();
......
......@@ -147,6 +147,7 @@ void main() {
TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(() => gesture?.removePointer());
await tester.pumpAndSettle();
await gesture.moveTo(tester.getCenter(find.byType(FloatingActionButton)));
await tester.pumpAndSettle();
......
......@@ -8,7 +8,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/gestures.dart';
// The tests in this file are moved from listener_test.dart, which tests several
// deprecated APIs. The file should be removed once these parameters are.
......@@ -82,7 +81,7 @@ void main() {
// onPointer{Enter,Hover,Exit} are removed. They were kept for compatibility,
// and the tests have been copied to mouse_region_test.
// https://github.com/flutter/flutter/issues/36085
setUp((){
setUp(() {
HoverClientState.numExits = 0;
HoverClientState.numEntries = 0;
});
......@@ -91,7 +90,8 @@ void main() {
PointerEnterEvent enter;
PointerHoverEvent move;
PointerExitEvent exit;
await tester.pumpWidget(Center(
await tester.pumpWidget(
Center(
child: Listener(
child: Container(
color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00),
......@@ -102,8 +102,10 @@ void main() {
onPointerHover: (PointerHoverEvent details) => move = details,
onPointerExit: (PointerExitEvent details) => exit = details,
),
));
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 300.0));
await tester.pump();
......@@ -117,7 +119,8 @@ void main() {
PointerEnterEvent enter;
PointerHoverEvent move;
PointerExitEvent exit;
await tester.pumpWidget(Center(
await tester.pumpWidget(
Center(
child: Listener(
child: Container(
width: 100.0,
......@@ -127,10 +130,11 @@ void main() {
onPointerHover: (PointerHoverEvent details) => move = details,
onPointerExit: (PointerExitEvent details) => exit = details,
),
));
),
);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(400.0, 300.0));
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 300.0));
await tester.pump();
move = null;
enter = null;
......@@ -145,7 +149,8 @@ void main() {
PointerEnterEvent enter;
PointerHoverEvent move;
PointerExitEvent exit;
await tester.pumpWidget(Center(
await tester.pumpWidget(
Center(
child: Listener(
child: Container(
width: 100.0,
......@@ -155,14 +160,14 @@ void main() {
onPointerHover: (PointerHoverEvent details) => move = details,
onPointerExit: (PointerExitEvent details) => exit = details,
),
));
),
);
final RenderMouseRegion renderListener = tester.renderObject(find.byType(MouseRegion));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(400.0, 300.0));
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 300.0));
await tester.pump();
expect(move, isNotNull);
expect(move.position, equals(const Offset(400.0, 300.0)));
expect(move, isNull);
expect(enter, isNotNull);
expect(enter.position, equals(const Offset(400.0, 300.0)));
expect(exit, isNull);
......@@ -197,7 +202,7 @@ void main() {
await tester.pumpWidget(Container());
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 0.0));
await gesture.addPointer(location: const Offset(400.0, 0.0));
await tester.pump();
await tester.pumpWidget(
Column(
......@@ -430,9 +435,8 @@ void main() {
expect(bottomLeft.dy - topLeft.dy, scaleFactor * localHeight);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.addPointer(location: topLeft - const Offset(1, 1));
addTearDown(gesture.removePointer);
await gesture.moveTo(topLeft - const Offset(1, 1));
await tester.pump();
expect(events, isEmpty);
......@@ -458,7 +462,7 @@ void main() {
testWidgets('needsCompositing updates correctly and is respected', (WidgetTester tester) async {
// Pretend that we have a mouse connected.
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await tester.pumpWidget(
......@@ -507,7 +511,7 @@ void main() {
testWidgets("Callbacks aren't called during build", (WidgetTester tester) async {
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await tester.pumpWidget(
......@@ -538,7 +542,7 @@ void main() {
testWidgets("Listener activate/deactivate don't duplicate annotations", (WidgetTester tester) async {
final GlobalKey feedbackKey = GlobalKey();
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await tester.pumpWidget(
......@@ -586,11 +590,8 @@ void main() {
// Plug-in a mouse and move it to the center of the container.
TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(() async {
if (gesture != null)
return gesture.removePointer();
});
await gesture.addPointer(location: Offset.zero);
addTearDown(() => gesture?.removePointer());
await gesture.moveTo(tester.getCenter(find.byType(Container)));
await tester.pumpAndSettle();
......@@ -613,7 +614,7 @@ void main() {
expect(hover.length, 0);
expect(exit.length, 1);
expect(exit.single.position, const Offset(400.0, 300.0));
expect(exit.single.delta, const Offset(0.0, 0.0));
expect(exit.single.delta, Offset.zero);
});
});
}
......@@ -7,7 +7,6 @@ import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
class HoverClient extends StatefulWidget {
const HoverClient({
Key key,
......@@ -27,18 +26,19 @@ class HoverClient extends StatefulWidget {
}
class HoverClientState extends State<HoverClient> {
void _onExit(PointerExitEvent details) {
if (widget.onExit != null)
if (widget.onExit != null) {
widget.onExit();
}
if (widget.onHover != null) {
widget.onHover(false);
}
}
void _onEnter(PointerEnterEvent details) {
if (widget.onEnter != null)
if (widget.onEnter != null) {
widget.onEnter();
}
if (widget.onHover != null) {
widget.onHover(true);
}
......@@ -82,7 +82,6 @@ class _HoverFeedbackState extends State<HoverFeedback> {
}
void main() {
group('MouseRegion hover detection', () {
testWidgets('detects pointer enter', (WidgetTester tester) async {
PointerEnterEvent enter;
PointerHoverEvent move;
......@@ -100,6 +99,7 @@ void main() {
),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 300.0));
await tester.pump();
......@@ -109,6 +109,7 @@ void main() {
expect(enter.position, equals(const Offset(400.0, 300.0)));
expect(exit, isNull);
});
testWidgets('detects pointer exiting', (WidgetTester tester) async {
PointerEnterEvent enter;
PointerHoverEvent move;
......@@ -125,6 +126,7 @@ void main() {
),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 300.0));
await tester.pump();
......@@ -137,6 +139,7 @@ void main() {
expect(exit, isNotNull);
expect(exit.position, equals(const Offset(1.0, 1.0)));
});
testWidgets('detects pointer exit when widget disappears', (WidgetTester tester) async {
PointerEnterEvent enter;
PointerHoverEvent move;
......@@ -154,6 +157,7 @@ void main() {
));
final RenderMouseRegion renderListener = tester.renderObject(find.byType(MouseRegion));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 300.0));
await tester.pump();
......@@ -172,6 +176,7 @@ void main() {
expect(exit.position, equals(const Offset(400.0, 300.0)));
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener.hoverAnnotation), isFalse);
});
testWidgets('Hover works with nested listeners', (WidgetTester tester) async {
final UniqueKey key1 = UniqueKey();
final UniqueKey key2 = UniqueKey();
......@@ -254,6 +259,7 @@ void main() {
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists();
});
testWidgets('Hover transfers between two listeners', (WidgetTester tester) async {
final UniqueKey key1 = UniqueKey();
final UniqueKey key2 = UniqueKey();
......@@ -276,7 +282,6 @@ void main() {
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.moveTo(const Offset(400.0, 0.0));
addTearDown(gesture.removePointer);
await tester.pump();
await tester.pumpWidget(
Column(
......@@ -360,6 +365,63 @@ void main() {
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isFalse);
});
testWidgets('MouseRegion uses updated callbacks', (WidgetTester tester) async {
final List<String> logs = <String>[];
Widget hoverableContainer({
PointerEnterEventListener onEnter,
PointerHoverEventListener onHover,
PointerExitEventListener onExit,
}) {
return Container(
alignment: Alignment.topLeft,
child: MouseRegion(
child: Container(
color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00),
width: 100.0,
height: 100.0,
),
onEnter: onEnter,
onHover: onHover,
onExit: onExit,
),
);
}
await tester.pumpWidget(hoverableContainer(
onEnter: (PointerEnterEvent details) => logs.add('enter1'),
onHover: (PointerHoverEvent details) => logs.add('hover1'),
onExit: (PointerExitEvent details) => logs.add('exit1'),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
// Start outside, move inside, then move outside
await gesture.moveTo(const Offset(150.0, 150.0));
await tester.pump();
await gesture.moveTo(const Offset(50.0, 50.0));
await tester.pump();
await gesture.moveTo(const Offset(150.0, 150.0));
await tester.pump();
expect(logs, <String>['enter1', 'hover1', 'exit1']);
logs.clear();
// Same tests but with updated callbacks
await tester.pumpWidget(hoverableContainer(
onEnter: (PointerEnterEvent details) => logs.add('enter2'),
onHover: (PointerHoverEvent details) => logs.add('hover2'),
onExit: (PointerExitEvent details) => logs.add('exit2'),
));
await gesture.moveTo(const Offset(150.0, 150.0));
await tester.pump();
await gesture.moveTo(const Offset(50.0, 50.0));
await tester.pump();
await gesture.moveTo(const Offset(150.0, 150.0));
await tester.pump();
expect(logs, <String>['enter2', 'hover2', 'exit2']);
});
testWidgets('needsCompositing set when parent class needsCompositing is set', (WidgetTester tester) async {
await tester.pumpWidget(
MouseRegion(
......@@ -476,7 +538,7 @@ void main() {
Transform.scale(
scale: 2.0,
child: MouseRegion(
onHover: (PointerHoverEvent _) { },
onHover: (PointerHoverEvent _) {},
),
),
);
......@@ -488,8 +550,7 @@ void main() {
await tester.pumpWidget(
Transform.scale(
scale: 2.0,
child: const MouseRegion(
),
child: const MouseRegion(),
),
);
expect(listener.needsCompositing, isFalse);
......@@ -501,14 +562,14 @@ void main() {
testWidgets("Callbacks aren't called during build", (WidgetTester tester) async {
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
addTearDown(gesture.removePointer);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await gesture.addPointer(location: Offset.zero);
int numEntries = 0;
int numExits = 0;
await tester.pumpWidget(
Center(child: HoverFeedback(
Center(
child: HoverFeedback(
onEnter: () => numEntries++,
onExit: () => numExits++,
)),
......@@ -528,7 +589,8 @@ void main() {
expect(numExits, equals(1));
await tester.pumpWidget(
Center(child: HoverFeedback(
Center(
child: HoverFeedback(
onEnter: () => numEntries++,
onExit: () => numExits++,
)),
......@@ -549,7 +611,8 @@ void main() {
int numExits = 0;
await tester.pumpWidget(
Center(child: HoverFeedback(
Center(
child: HoverFeedback(
key: feedbackKey,
onEnter: () => numEntries++,
onExit: () => numExits++,
......@@ -563,7 +626,9 @@ void main() {
expect(find.text('HOVERING'), findsOneWidget);
await tester.pumpWidget(
Center(child: Container(child: HoverFeedback(
Center(
child: Container(
child: HoverFeedback(
key: feedbackKey,
onEnter: () => numEntries++,
onExit: () => numExits++,
......@@ -601,7 +666,7 @@ void main() {
// Plug-in a mouse and move it to the center of the container.
TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
await gesture.addPointer(location: Offset.zero);
addTearDown(() => gesture?.removePointer());
await gesture.moveTo(tester.getCenter(find.byType(Container)));
await tester.pumpAndSettle();
......@@ -625,7 +690,7 @@ void main() {
expect(hover.length, 0);
expect(exit.length, 1);
expect(exit.single.position, const Offset(400.0, 300.0));
expect(exit.single.delta, const Offset(0.0, 0.0));
expect(exit.single.delta, Offset.zero);
});
testWidgets('detects pointer enter with closure arguments', (WidgetTester tester) async {
......@@ -645,10 +710,8 @@ void main() {
await tester.pumpAndSettle();
expect(find.text('HOVERING'), findsOneWidget);
});
});
group('MouseRegion paints child once and only once', () {
testWidgets('When MouseRegion is inactive', (WidgetTester tester) async {
testWidgets('MouseRegion paints child once and only once when MouseRegion is inactive', (WidgetTester tester) async {
int paintCount = 0;
await tester.pumpWidget(
Directionality(
......@@ -666,11 +729,12 @@ void main() {
expect(paintCount, 1);
});
testWidgets('When MouseRegion is active', (WidgetTester tester) async {
testWidgets('MouseRegion paints child once and only once when MouseRegion is active', (WidgetTester tester) async {
int paintCount = 0;
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await tester.pumpWidget(
Directionality(
......@@ -686,18 +750,13 @@ void main() {
);
expect(paintCount, 1);
await gesture.removePointer();
});
});
testWidgets('RenderMouseRegion\'s debugFillProperties when default', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
RenderMouseRegion().debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
final List<String> description = builder.properties.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)).map((DiagnosticsNode node) => node.toString()).toList();
expect(description, <String>[
'parentData: MISSING',
......@@ -716,10 +775,7 @@ void main() {
child: RenderErrorBox(),
).debugFillProperties(builder);
final List<String> description = builder.properties
.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
.map((DiagnosticsNode node) => node.toString())
.toList();
final List<String> description = builder.properties.where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)).map((DiagnosticsNode node) => node.toString()).toList();
expect(description, <String>[
'parentData: MISSING',
......@@ -728,6 +784,39 @@ void main() {
'listeners: enter, hover, exit',
]);
});
testWidgets('No new frames are scheduled when mouse moves without triggering callbacks', (WidgetTester tester) async {
await tester.pumpWidget(Center(
child: MouseRegion(
child: Container(
width: 100.0,
height: 100.0,
),
onEnter: (PointerEnterEvent details) {},
onHover: (PointerHoverEvent details) {},
onExit: (PointerExitEvent details) {},
),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(400.0, 300.0));
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
await gesture.moveBy(const Offset(10.0, 10.0));
expect(tester.binding.hasScheduledFrame, isFalse);
});
testWidgets("MouseTracker's attachAnnotation doesn't schedule any frames", (WidgetTester tester) async {
// This test is here because MouseTracker can't use testWidgets.
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
onEnter: (PointerEnterEvent event) {},
onHover: (PointerHoverEvent event) {},
onExit: (PointerExitEvent event) {},
);
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
expect(tester.binding.hasScheduledFrame, isFalse);
expect(RendererBinding.instance.mouseTracker.isAnnotationAttached(annotation), isTrue);
RendererBinding.instance.mouseTracker.detachAnnotation(annotation);
});
}
// This widget allows you to send a callback that is called during `onPaint.
......@@ -748,8 +837,7 @@ class _PaintDelegateWidget extends SingleChildRenderObjectWidget {
@override
void updateRenderObject(BuildContext context, _PaintCallbackObject renderObject) {
renderObject
..onPaint = onPaint?.callback;
renderObject..onPaint = onPaint?.callback;
}
}
......@@ -769,8 +857,9 @@ class _PaintCallbackObject extends RenderProxyBox {
@override
void paint(PaintingContext context, Offset offset) {
if (onPaint != null)
if (onPaint != null) {
onPaint();
}
super.paint(context, offset);
}
}
......@@ -781,7 +870,6 @@ class _HoverClientWithClosures extends StatefulWidget {
}
class _HoverClientWithClosuresState extends State<_HoverClientWithClosures> {
bool _hovering = false;
@override
......@@ -789,8 +877,16 @@ class _HoverClientWithClosuresState extends State<_HoverClientWithClosures> {
return Directionality(
textDirection: TextDirection.ltr,
child: MouseRegion(
onEnter: (PointerEnterEvent _) { setState(() { _hovering = true; }); },
onExit: (PointerExitEvent _) { setState(() { _hovering = false; }); },
onEnter: (PointerEnterEvent _) {
setState(() {
_hovering = true;
});
},
onExit: (PointerExitEvent _) {
setState(() {
_hovering = false;
});
},
child: Text(_hovering ? 'HOVERING' : 'not hovering'),
),
);
......
......@@ -805,6 +805,10 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
'The MouseTracker thinks that there is still a mouse connected, which indicates that a '
'test has not removed the mouse pointer which it added. Call removePointer on the '
'active mouse gesture to remove the mouse pointer.');
// ignore: invalid_use_of_visible_for_testing_member
RendererBinding.instance.initMouseTracker();
// ignore: invalid_use_of_visible_for_testing_member
PointerEventConverter.clearPointers();
}
}
......
......@@ -373,16 +373,16 @@ class TestGesture {
}
/// In a test, send a pointer add event for this pointer.
Future<void> addPointer({ Duration timeStamp = Duration.zero }) {
Future<void> addPointer({ Duration timeStamp = Duration.zero, Offset location }) {
return TestAsyncUtils.guard<void>(() {
return _dispatcher(_pointer.addPointer(timeStamp: timeStamp, location: _pointer.location), null);
return _dispatcher(_pointer.addPointer(timeStamp: timeStamp, location: location ?? _pointer.location), null);
});
}
/// In a test, send a pointer remove event for this pointer.
Future<void> removePointer({ Duration timeStamp = Duration.zero}) {
Future<void> removePointer({ Duration timeStamp = Duration.zero, Offset location }) {
return TestAsyncUtils.guard<void>(() {
return _dispatcher(_pointer.removePointer(timeStamp: timeStamp, location: _pointer.location), null);
return _dispatcher(_pointer.removePointer(timeStamp: timeStamp, location: location ?? _pointer.location), null);
});
}
......
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