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
diff --git a/packages/flutter/lib/src/gestures/mouse_tracking.dart b/packages/flutter/lib/src/gestures/mouse_tracking.dart
index b470146..3b5fa1d 100644
--- a/packages/flutter/lib/src/gestures/mouse_tracking.dart
+++ b/packages/flutter/lib/src/gestures/mouse_tracking.dart
@@ -120,7 +120,6 @@
   /// [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 @@
   /// 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 f0489d7..2f0275b 100644
--- a/packages/flutter/lib/src/rendering/proxy_box.dart
+++ b/packages/flutter/lib/src/rendering/proxy_box.dart
@@ -2543,7 +2543,7 @@
   /// 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 @@
   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 @@
     } 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 54f54e8..a4691e3 100644
--- a/packages/flutter/test/widgets/listener_test.dart
+++ b/packages/flutter/test/widgets/listener_test.dart
@@ -129,5 +129,108 @@
       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<PointerEnterEvent> enter1 = <PointerEnterEvent>[];
+      final List<PointerHoverEvent> move1 = <PointerHoverEvent>[];
+      final List<PointerExitEvent> exit1 = <PointerExitEvent>[];
+      final List<PointerEnterEvent> enter2 = <PointerEnterEvent>[];
+      final List<PointerHoverEvent> move2 = <PointerHoverEvent>[];
+      final List<PointerExitEvent> exit2 = <PointerExitEvent>[];
+      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: <Widget>[
+            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);
+    });
   });
 }