From 8bea3fb2ebadc3933b6b213483d2d4379ac53a5c Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Thu, 11 Apr 2019 13:11:22 -0700 Subject: [PATCH] Keep hover annotation layers in sync with the mouse detector. (#30829) Adds a paint after detaching/attaching hover annotations to keep the annotation layers in sync with the annotations attached to the mouse detector. Fixes #30744 --- .../lib/src/gestures/mouse_tracking.dart | 3 +- .../flutter/lib/src/rendering/proxy_box.dart | 7 +- .../flutter/test/widgets/listener_test.dart | 103 ++++++++++++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/gestures/mouse_tracking.dart b/packages/flutter/lib/src/gestures/mouse_tracking.dart index b4701467d9..3b5fa1d44f 100644 --- a/packages/flutter/lib/src/gestures/mouse_tracking.dart +++ b/packages/flutter/lib/src/gestures/mouse_tracking.dart @@ -120,7 +120,6 @@ class MouseTracker { /// [collectMousePositions] will assert the next time it is called. void detachAnnotation(MouseTrackerAnnotation annotation) { final _TrackedAnnotation trackedAnnotation = _findAnnotation(annotation); - assert(trackedAnnotation != null, "Tried to detach an annotation that wasn't attached: $annotation"); for (int deviceId in trackedAnnotation.activeDevices) { annotation.onExit(PointerExitEvent.fromMouseEvent(_lastMouseEvent[deviceId])); } @@ -178,7 +177,7 @@ class MouseTracker { /// MouseTracker. Do not call in other contexts. @visibleForTesting bool isAnnotationAttached(MouseTrackerAnnotation annotation) { - return _trackedAnnotations[annotation] != null; + return _trackedAnnotations.containsKey(annotation); } /// Tells interested objects that a mouse has entered, exited, or moved, given diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index f0489d77c7..2f0275b6bb 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2543,7 +2543,7 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { /// no longer directed towards this receiver. PointerCancelEventListener onPointerCancel; - /// Called when a pointer signal occures over this object. + /// Called when a pointer signal occurs over this object. PointerSignalEventListener onPointerSignal; // Object used for annotation of the layer used for hover hit detection. @@ -2557,6 +2557,8 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { MouseTrackerAnnotation get hoverAnnotation => _hoverAnnotation; void _updateAnnotations() { + assert(_onPointerEnter != _hoverAnnotation.onEnter || _onPointerHover != _hoverAnnotation.onHover || _onPointerExit != _hoverAnnotation.onExit, + "Shouldn't call _updateAnnotations if nothing has changed."); if (_hoverAnnotation != null && attached) { RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation); } @@ -2572,6 +2574,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { } else { _hoverAnnotation = null; } + // Needs to paint in any case, in order to insert/remove the annotation + // layer associated with the updated _hoverAnnotation. + markNeedsPaint(); } @override diff --git a/packages/flutter/test/widgets/listener_test.dart b/packages/flutter/test/widgets/listener_test.dart index 54f54e8a22..a4691e3084 100644 --- a/packages/flutter/test/widgets/listener_test.dart +++ b/packages/flutter/test/widgets/listener_test.dart @@ -129,5 +129,108 @@ void main() { expect(exit.position, equals(const Offset(400.0, 300.0))); expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener.hoverAnnotation), isFalse); }); + testWidgets('Hover transfers between two listeners', (WidgetTester tester) async { + final UniqueKey key1 = UniqueKey(); + final UniqueKey key2 = UniqueKey(); + final List enter1 = []; + final List move1 = []; + final List exit1 = []; + final List enter2 = []; + final List move2 = []; + final List exit2 = []; + void clearLists() { + enter1.clear(); + move1.clear(); + exit1.clear(); + enter2.clear(); + move2.clear(); + exit2.clear(); + } + + await tester.pumpWidget(Container()); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.moveTo(const Offset(400.0, 0.0)); + await tester.pump(); + await tester.pumpWidget( + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Listener( + key: key1, + child: Container( + width: 100.0, + height: 100.0, + ), + onPointerEnter: (PointerEnterEvent details) => enter1.add(details), + onPointerHover: (PointerHoverEvent details) => move1.add(details), + onPointerExit: (PointerExitEvent details) => exit1.add(details), + ), + Listener( + key: key2, + child: Container( + width: 100.0, + height: 100.0, + ), + onPointerEnter: (PointerEnterEvent details) => enter2.add(details), + onPointerHover: (PointerHoverEvent details) => move2.add(details), + onPointerExit: (PointerExitEvent details) => exit2.add(details), + ), + ], + ), + ); + final RenderPointerListener renderListener1 = tester.renderObject(find.byKey(key1)); + final RenderPointerListener renderListener2 = tester.renderObject(find.byKey(key2)); + final Offset center1 = tester.getCenter(find.byKey(key1)); + final Offset center2 = tester.getCenter(find.byKey(key2)); + await gesture.moveTo(center1); + await tester.pump(); + expect(move1, isNotEmpty); + expect(move1.last.position, equals(center1)); + expect(enter1, isNotEmpty); + expect(enter1.last.position, equals(center1)); + expect(exit1, isEmpty); + expect(move2, isEmpty); + expect(enter2, isEmpty); + expect(exit2, isEmpty); + expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue); + expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue); + clearLists(); + await gesture.moveTo(center2); + await tester.pump(); + expect(move1, isEmpty); + expect(enter1, isEmpty); + expect(exit1, isNotEmpty); + expect(exit1.last.position, equals(center2)); + expect(move2, isNotEmpty); + expect(move2.last.position, equals(center2)); + expect(enter2, isNotEmpty); + expect(enter2.last.position, equals(center2)); + expect(exit2, isEmpty); + expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue); + expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue); + clearLists(); + await gesture.moveTo(const Offset(400.0, 450.0)); + await tester.pump(); + expect(move1, isEmpty); + expect(enter1, isEmpty); + expect(exit1, isEmpty); + expect(move2, isEmpty); + expect(enter2, isEmpty); + expect(exit2, isNotEmpty); + expect(exit2.last.position, equals(const Offset(400.0, 450.0))); + expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue); + expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue); + clearLists(); + await tester.pumpWidget(Container()); + expect(move1, isEmpty); + expect(enter1, isEmpty); + expect(exit1, isEmpty); + expect(move2, isEmpty); + expect(enter2, isEmpty); + expect(exit2, isEmpty); + expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isFalse); + expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isFalse); + }); }); }