From 0450e7c0ff7d6a24dde56fee432781a2344f506c Mon Sep 17 00:00:00 2001 From: Tong Mu Date: Fri, 24 Jan 2020 10:33:02 -0800 Subject: [PATCH] Trigger MouseRegion.toHover only on hover events (#47403) --- .../lib/src/gestures/mouse_tracking.dart | 26 +-- .../widgets/listener_deprecated_test.dart | 4 +- .../test/widgets/mouse_region_test.dart | 208 +++++++++++++++++- 3 files changed, 214 insertions(+), 24 deletions(-) diff --git a/packages/flutter/lib/src/gestures/mouse_tracking.dart b/packages/flutter/lib/src/gestures/mouse_tracking.dart index 5e169ee64dd..0094ce8217e 100644 --- a/packages/flutter/lib/src/gestures/mouse_tracking.dart +++ b/packages/flutter/lib/src/gestures/mouse_tracking.dart @@ -289,7 +289,6 @@ class MouseTracker extends ChangeNotifier { return; final PointerEvent previousEvent = existingState?.latestEvent; - final Offset lastHoverPosition = previousEvent is! PointerHoverEvent ? null : previousEvent.position; _updateDevices( targetEvent: event, handleUpdatedDevice: (_MouseState mouseState, LinkedHashSet previousAnnotations) { @@ -297,7 +296,7 @@ class MouseTracker extends ChangeNotifier { _dispatchDeviceCallbacks( lastAnnotations: previousAnnotations, nextAnnotations: mouseState.annotations, - lastHoverPosition: lastHoverPosition, + previousEvent: previousEvent, unhandledEvent: event, trackedAnnotations: _trackedAnnotations, ); @@ -328,13 +327,11 @@ class MouseTracker extends ChangeNotifier { void _updateAllDevices() { _updateDevices( handleUpdatedDevice: (_MouseState mouseState, LinkedHashSet previousAnnotations) { - final PointerEvent latestEvent = mouseState.latestEvent; - final Offset lastHoverPosition = latestEvent is PointerHoverEvent ? latestEvent.position : null; _dispatchDeviceCallbacks( lastAnnotations: previousAnnotations, nextAnnotations: mouseState.annotations, - lastHoverPosition: lastHoverPosition, - unhandledEvent: mouseState.latestEvent, + previousEvent: mouseState.latestEvent, + unhandledEvent: null, trackedAnnotations: _trackedAnnotations, ); } @@ -427,20 +424,22 @@ class MouseTracker extends ChangeNotifier { // Dispatch callbacks related to a device after all necessary information // has been collected. // - // The `lastHoverPosition` can be null, which means the last event is not a - // hover. Other arguments must not be null. + // The `previousEvent` is the latest event before `unhandledEvent`. It might be + // null, which means the update is triggered by a new event. + // The `unhandledEvent` can be null, which means the update is not triggered + // by an event. static void _dispatchDeviceCallbacks({ @required LinkedHashSet lastAnnotations, @required LinkedHashSet nextAnnotations, - @required Offset lastHoverPosition, + @required PointerEvent previousEvent, @required PointerEvent unhandledEvent, @required Set trackedAnnotations, }) { assert(lastAnnotations != null); assert(nextAnnotations != null); - // lastHoverPosition can be null - assert(unhandledEvent != null); assert(trackedAnnotations != null); + final PointerEvent latestEvent = unhandledEvent ?? previousEvent; + assert(latestEvent != null); // Order is important for mouse event callbacks. The `findAnnotations` // returns annotations in the visual order from front to back. We call // it the "visual order", and the opposite one "reverse visual order". @@ -456,7 +455,7 @@ class MouseTracker extends ChangeNotifier { // trigger may cause exceptions and has safer alternatives. See // [MouseRegion.onExit] for details. if (annotation.onExit != null && attached) { - annotation.onExit(PointerExitEvent.fromMouseEvent(unhandledEvent)); + annotation.onExit(PointerExitEvent.fromMouseEvent(latestEvent)); } } @@ -466,7 +465,7 @@ class MouseTracker extends ChangeNotifier { for (final MouseTrackerAnnotation annotation in enteringAnnotations) { assert(trackedAnnotations.contains(annotation)); if (annotation.onEnter != null) { - annotation.onEnter(PointerEnterEvent.fromMouseEvent(unhandledEvent)); + annotation.onEnter(PointerEnterEvent.fromMouseEvent(latestEvent)); } } @@ -476,6 +475,7 @@ class MouseTracker extends ChangeNotifier { if (unhandledEvent is PointerHoverEvent) { final Iterable hoveringAnnotations = nextAnnotations.toList().reversed; + final Offset lastHoverPosition = previousEvent is PointerHoverEvent ? previousEvent.position : null; for (final MouseTrackerAnnotation annotation in hoveringAnnotations) { // Deduplicate: Trigger hover if it's a newly hovered annotation // or the position has changed diff --git a/packages/flutter/test/widgets/listener_deprecated_test.dart b/packages/flutter/test/widgets/listener_deprecated_test.dart index b28e0bcbcbc..9a2444d3bf2 100644 --- a/packages/flutter/test/widgets/listener_deprecated_test.dart +++ b/packages/flutter/test/widgets/listener_deprecated_test.dart @@ -107,8 +107,8 @@ 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(); + await gesture.moveTo(const Offset(400.0, 300.0)); expect(move, isNotNull); expect(move.position, equals(const Offset(400.0, 300.0))); expect(enter, isNotNull); @@ -593,8 +593,8 @@ void main() { TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(location: Offset.zero); addTearDown(() => gesture?.removePointer()); - await gesture.moveTo(tester.getCenter(find.byType(Container))); await tester.pumpAndSettle(); + await gesture.moveTo(tester.getCenter(find.byType(Container))); expect(enter.length, 1); expect(enter.single.position, const Offset(400.0, 300.0)); diff --git a/packages/flutter/test/widgets/mouse_region_test.dart b/packages/flutter/test/widgets/mouse_region_test.dart index 6a99ee8214d..fae36a9e888 100644 --- a/packages/flutter/test/widgets/mouse_region_test.dart +++ b/packages/flutter/test/widgets/mouse_region_test.dart @@ -101,8 +101,11 @@ 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(); + move = null; + enter = null; + exit = null; + await gesture.moveTo(const Offset(400.0, 300.0)); expect(move, isNotNull); expect(move.position, equals(const Offset(400.0, 300.0))); expect(enter, isNotNull); @@ -132,15 +135,118 @@ void main() { await tester.pump(); move = null; enter = null; + exit = null; await gesture.moveTo(const Offset(1.0, 1.0)); - await tester.pump(); expect(move, isNull); expect(enter, isNull); expect(exit, isNotNull); expect(exit.position, equals(const Offset(1.0, 1.0))); }); - testWidgets('detects pointer exit when widget disappears', (WidgetTester tester) async { + testWidgets('triggers pointer enter when a mouse is connected', (WidgetTester tester) async { + PointerEnterEvent enter; + PointerHoverEvent move; + PointerExitEvent exit; + await tester.pumpWidget(Center( + child: MouseRegion( + child: Container( + width: 100.0, + height: 100.0, + ), + onEnter: (PointerEnterEvent details) => enter = details, + onHover: (PointerHoverEvent details) => move = details, + onExit: (PointerExitEvent details) => exit = details, + ), + )); + await tester.pump(); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: const Offset(400, 300)); + addTearDown(gesture.removePointer); + expect(move, isNull); + expect(enter, isNull); + expect(exit, isNull); + await tester.pump(); + expect(move, isNull); + expect(enter, isNotNull); + expect(enter.position, equals(const Offset(400.0, 300.0))); + expect(exit, isNull); + }); + + testWidgets('triggers pointer exit when a mouse is disconnected', (WidgetTester tester) async { + PointerEnterEvent enter; + PointerHoverEvent move; + PointerExitEvent exit; + await tester.pumpWidget(Center( + child: MouseRegion( + child: Container( + width: 100.0, + height: 100.0, + ), + onEnter: (PointerEnterEvent details) => enter = details, + onHover: (PointerHoverEvent details) => move = details, + onExit: (PointerExitEvent details) => exit = details, + ), + )); + await tester.pump(); + + TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: const Offset(400, 300)); + addTearDown(() => gesture?.removePointer); + await tester.pump(); + move = null; + enter = null; + exit = null; + await gesture.removePointer(); + gesture = null; + expect(move, isNull); + expect(enter, isNull); + expect(exit, isNotNull); + expect(exit.position, equals(const Offset(400.0, 300.0))); + exit = null; + await tester.pump(); + expect(move, isNull); + expect(enter, isNull); + expect(exit, isNull); + }); + + testWidgets('triggers pointer enter when widget appears', (WidgetTester tester) async { + PointerEnterEvent enter; + PointerHoverEvent move; + PointerExitEvent exit; + await tester.pumpWidget(Center( + child: Container( + width: 100.0, + height: 100.0, + ), + )); + 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(); + expect(enter, isNull); + expect(move, isNull); + expect(exit, isNull); + await tester.pumpWidget(Center( + child: MouseRegion( + child: Container( + width: 100.0, + height: 100.0, + ), + onEnter: (PointerEnterEvent details) => enter = details, + onHover: (PointerHoverEvent details) => move = details, + onExit: (PointerExitEvent details) => exit = details, + ), + )); + await tester.pump(); + expect(move, isNull); + expect(enter, isNotNull); + expect(enter.position, equals(const Offset(400.0, 300.0))); + expect(exit, isNull); + }); + + testWidgets("doesn't trigger pointer exit when widget disappears", (WidgetTester tester) async { PointerEnterEvent enter; PointerHoverEvent move; PointerExitEvent exit; @@ -161,21 +267,105 @@ void main() { 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(enter, isNotNull); - expect(enter.position, equals(const Offset(400.0, 300.0))); - expect(exit, isNull); + move = null; + enter = null; + exit = null; await tester.pumpWidget(Center( child: Container( width: 100.0, height: 100.0, ), )); + expect(enter, isNull); + expect(move, isNull); expect(exit, isNull); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener.hoverAnnotation), isFalse); }); + testWidgets('triggers pointer enter when widget moves in', (WidgetTester tester) async { + PointerEnterEvent enter; + PointerHoverEvent move; + PointerExitEvent exit; + await tester.pumpWidget(Container( + alignment: Alignment.center, + child: MouseRegion( + child: Container( + width: 100.0, + height: 100.0, + ), + onEnter: (PointerEnterEvent details) => enter = details, + onHover: (PointerHoverEvent details) => move = details, + onExit: (PointerExitEvent details) => exit = details, + ), + )); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: const Offset(1.0, 1.0)); + addTearDown(gesture.removePointer); + await tester.pump(); + expect(enter, isNull); + expect(move, isNull); + expect(exit, isNull); + await tester.pumpWidget(Container( + alignment: Alignment.topLeft, + child: MouseRegion( + child: Container( + width: 100.0, + height: 100.0, + ), + onEnter: (PointerEnterEvent details) => enter = details, + onHover: (PointerHoverEvent details) => move = details, + onExit: (PointerExitEvent details) => exit = details, + ), + )); + await tester.pump(); + expect(enter, isNotNull); + expect(enter.position, equals(const Offset(1.0, 1.0))); + expect(move, isNull); + expect(exit, isNull); + }); + + testWidgets('triggers pointer exit when widget moves out', (WidgetTester tester) async { + PointerEnterEvent enter; + PointerHoverEvent move; + PointerExitEvent exit; + await tester.pumpWidget(Container( + alignment: Alignment.center, + child: MouseRegion( + child: Container( + width: 100.0, + height: 100.0, + ), + onEnter: (PointerEnterEvent details) => enter = details, + onHover: (PointerHoverEvent details) => move = details, + onExit: (PointerExitEvent details) => exit = details, + ), + )); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: const Offset(400, 300)); + addTearDown(gesture.removePointer); + await tester.pump(); + enter = null; + move = null; + exit = null; + await tester.pumpWidget(Container( + alignment: Alignment.topLeft, + child: MouseRegion( + child: Container( + width: 100.0, + height: 100.0, + ), + onEnter: (PointerEnterEvent details) => enter = details, + onHover: (PointerHoverEvent details) => move = details, + onExit: (PointerExitEvent details) => exit = details, + ), + )); + await tester.pump(); + expect(enter, isNull); + expect(move, isNull); + expect(exit, isNotNull); + expect(exit.position, equals(const Offset(400, 300))); + }); + testWidgets('Hover works with nested listeners', (WidgetTester tester) async { final UniqueKey key1 = UniqueKey(); final UniqueKey key2 = UniqueKey(); @@ -680,8 +870,8 @@ void main() { TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(location: Offset.zero); addTearDown(() => gesture?.removePointer()); - await gesture.moveTo(tester.getCenter(find.byType(Container))); await tester.pumpAndSettle(); + await gesture.moveTo(tester.getCenter(find.byType(Container))); expect(enter.length, 1); expect(enter.single.position, const Offset(400.0, 300.0));