Redo: Rewrite MouseTracker's tracking and notifying algorithm (#42486)

* Revert "Revert "Rewrite MouseTracker's tracking and notifying algorithm (#42031)" (#42478)"

This reverts commit eede792923ece2d76106c9e0742c45000be48834.

* Fix tests
diff --git a/packages/flutter/lib/src/foundation/diagnostics.dart b/packages/flutter/lib/src/foundation/diagnostics.dart
index b4c77ed..89c5769 100644
--- a/packages/flutter/lib/src/foundation/diagnostics.dart
+++ b/packages/flutter/lib/src/foundation/diagnostics.dart
@@ -2435,7 +2435,7 @@
 ///
 /// See also:
 ///
-///  * [ObjectFlagSummary], which provides similar functionality but accepts
+///  * [ObjectFlagProperty], which provides similar functionality but accepts
 ///    only one flag, and is preferred if there is only one entry.
 ///  * [IterableProperty], which provides similar functionality describing
 ///    the values a collection of objects.
diff --git a/packages/flutter/lib/src/gestures/mouse_tracking.dart b/packages/flutter/lib/src/gestures/mouse_tracking.dart
index 9c35961..2060cb3 100644
--- a/packages/flutter/lib/src/gestures/mouse_tracking.dart
+++ b/packages/flutter/lib/src/gestures/mouse_tracking.dart
@@ -2,9 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:collection' show LinkedHashSet;
 import 'dart:ui';
 
-import 'package:flutter/foundation.dart' show ChangeNotifier, visibleForTesting;
+import 'package:flutter/foundation.dart';
 import 'package:flutter/scheduler.dart';
 
 import 'events.dart';
@@ -48,49 +49,82 @@
 
   @override
   String toString() {
-    final String none = (onEnter == null && onExit == null && onHover == null) ? ' <none>' : '';
-    return '[$runtimeType${hashCode.toRadixString(16)}$none'
-        '${onEnter == null ? '' : ' onEnter'}'
-        '${onHover == null ? '' : ' onHover'}'
-        '${onExit == null ? '' : ' onExit'}]';
+    final List<String> callbacks = <String>[];
+    if (onEnter != null)
+      callbacks.add('enter');
+    if (onHover != null)
+      callbacks.add('hover');
+    if (onExit != null)
+      callbacks.add('exit');
+    final String describeCallbacks = callbacks.isEmpty
+      ? '<none>'
+      : callbacks.join(' ');
+    return '${describeIdentity(this)}(callbacks: $describeCallbacks)';
   }
 }
 
-// Used internally by the MouseTracker for accounting for which annotation is
-// active on which devices inside of the MouseTracker.
-class _TrackedAnnotation {
-  _TrackedAnnotation(this.annotation);
-
-  final MouseTrackerAnnotation annotation;
-
-  /// Tracks devices that are currently active for this annotation.
-  ///
-  /// If the mouse pointer corresponding to the integer device ID is
-  /// present in the Set, then it is currently inside of the annotated layer.
-  ///
-  /// This is used to detect layers that used to have the mouse pointer inside
-  /// them, but now no longer do (to facilitate exit notification).
-  Set<int> activeDevices = <int>{};
-}
-
-/// Describes a function that finds an annotation given an offset in logical
-/// coordinates.
+/// Signature for searching for [MouseTrackerAnnotation]s at the given offset.
 ///
 /// It is used by the [MouseTracker] to fetch annotations for the mouse
 /// position.
 typedef MouseDetectorAnnotationFinder = Iterable<MouseTrackerAnnotation> Function(Offset offset);
 
-/// Keeps state about which objects are interested in tracking mouse positions
-/// and notifies them when a mouse pointer enters, moves, or leaves an annotated
-/// region that they are interested in.
+// Various states of each connected mouse device.
+//
+// It is used by [MouseTracker] to compute which callbacks should be triggered
+// by each event.
+class _MouseState {
+  _MouseState({
+    @required PointerEvent mostRecentEvent,
+  }) : assert(mostRecentEvent != null),
+       _mostRecentEvent = mostRecentEvent;
+
+  // The list of annotations that contains this device during the last frame.
+  //
+  // It uses [LinkedHashSet] to keep the insertion order.
+  LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = LinkedHashSet<MouseTrackerAnnotation>();
+
+  // The most recent mouse event observed from this device.
+  //
+  // The [mostRecentEvent] is never null.
+  PointerEvent get mostRecentEvent => _mostRecentEvent;
+  PointerEvent _mostRecentEvent;
+  set mostRecentEvent(PointerEvent value) {
+    assert(value != null);
+    assert(value.device == _mostRecentEvent.device);
+    _mostRecentEvent = value;
+  }
+
+  int get device => _mostRecentEvent.device;
+
+  @override
+  String toString() {
+    final String describeEvent = '${_mostRecentEvent.runtimeType}(device: ${_mostRecentEvent.device})';
+    final String describeAnnotations = '[list of ${lastAnnotations.length}]';
+    return '${describeIdentity(this)}(event: $describeEvent, annotations: $describeAnnotations)';
+  }
+}
+
+/// Maintains the relationship between mouse devices and
+/// [MouseTrackerAnnotation]s, and notifies interested callbacks of the changes
+/// thereof.
 ///
 /// This class is a [ChangeNotifier] that notifies its listeners if the value of
 /// [mouseIsConnected] changes.
 ///
-/// Owned by the [RendererBinding] class.
+/// An instance of [MouseTracker] is owned by the global singleton of
+/// [RendererBinding].
 class MouseTracker extends ChangeNotifier {
   /// Creates a mouse tracker to keep track of mouse locations.
   ///
+  /// The first parameter is a [PointerRouter], which [MouseTracker] will
+  /// subscribe to and receive events from. Usually it is the global singleton
+  /// instance [GestureBinding.pointerRouter].
+  ///
+  /// The second parameter is a function with which the [MouseTracker] can
+  /// search for [MouseTrackerAnnotation]s at a given position.
+  /// Usually it is [Layer.findAll] of the root layer.
+  ///
   /// All of the parameters must not be null.
   MouseTracker(this._router, this.annotationFinder)
       : assert(_router != null),
@@ -104,60 +138,67 @@
     _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.
+  /// Find annotations at a given offset in global logical coordinate space
+  /// in visual order from front to back.
+  ///
+  /// [MouseTracker] uses this callback to know which annotations are affected
+  /// by each device.
+  ///
+  /// The annotations should be returned in visual order from front to
+  /// back, so that the callbacks are called in an correct order.
   final MouseDetectorAnnotationFinder annotationFinder;
 
-  // The collection of annotations that are currently being tracked. They may or
-  // may not be active, depending on the value of _TrackedAnnotation.active.
-  final Map<MouseTrackerAnnotation, _TrackedAnnotation> _trackedAnnotations = <MouseTrackerAnnotation, _TrackedAnnotation>{};
+  // The pointer router that the mouse tracker listens to, and receives new
+  // mouse events from.
+  final PointerRouter _router;
 
-  /// Track an annotation so that if the mouse enters it, we send it events.
-  ///
-  /// This is typically called when the [AnnotatedRegion] containing this
-  /// 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 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();
+  // Tracks the state of connected mouse devices.
+  //
+  // It is the source of truth for the list of connected mouse devices.
+  final Map<int, _MouseState> _mouseStates = <int, _MouseState>{};
+
+  // Returns the mouse state of a device. If it doesn't exist, create one using
+  // `mostRecentEvent`.
+  //
+  // The returned value is never null.
+  _MouseState _guaranteeMouseState(int device, PointerEvent mostRecentEvent) {
+    final _MouseState currentState = _mouseStates[device];
+    if (currentState == null) {
+      _addMouseDevice(device, mostRecentEvent);
+    }
+    final _MouseState result = currentState ?? _mouseStates[device];
+    assert(result != null);
+    return result;
+  }
+
+  // The collection of annotations that are currently being tracked.
+  // It is operated on by [attachAnnotation] and [detachAnnotation].
+  final Set<MouseTrackerAnnotation> _trackedAnnotations = <MouseTrackerAnnotation>{};
+  bool get _hasAttachedAnnotations => _trackedAnnotations.isNotEmpty;
+
+  void _addMouseDevice(int device, PointerEvent event) {
+    final bool wasConnected = mouseIsConnected;
+    assert(!_mouseStates.containsKey(device));
+    _mouseStates[device] = _MouseState(mostRecentEvent: event);
+    // Schedule a check to enter annotations that might contain this pointer.
+    _checkDeviceUpdates(device: device);
+    if (mouseIsConnected != wasConnected) {
+      notifyListeners();
     }
   }
 
-  /// Stops tracking an annotation, indicating that it has been removed from the
-  /// layer tree.
-  ///
-  /// An assertion error will be thrown if the associated layer is not removed
-  /// and receives another mouse hit.
-  void detachAnnotation(MouseTrackerAnnotation annotation) {
-    final _TrackedAnnotation trackedAnnotation = _findAnnotation(annotation);
-    for (int deviceId in trackedAnnotation.activeDevices) {
-      if (annotation.onExit != null) {
-        final PointerEvent event = _lastMouseEvent[deviceId] ?? _pendingRemovals[deviceId];
-        assert(event != null);
-        annotation.onExit(PointerExitEvent.fromMouseEvent(event));
-      }
-    }
-    _trackedAnnotations.remove(annotation);
-  }
-
-  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 && !_scheduledPostFramePositionCheck) {
-      _scheduledPostFramePositionCheck = true;
-      SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
-        _sendMouseNotifications(_lastMouseEvent.keys);
-        _scheduledPostFramePositionCheck = false;
-      });
+  void _removeMouseDevice(int device, PointerEvent event) {
+    final bool wasConnected = mouseIsConnected;
+    assert(_mouseStates.containsKey(device));
+    final _MouseState disconnectedMouseState = _mouseStates.remove(device);
+    disconnectedMouseState.mostRecentEvent = event;
+    // Schedule a check to exit annotations that used to contain this pointer.
+    _checkDeviceUpdates(
+      device: device,
+      disconnectedMouseState: disconnectedMouseState,
+    );
+    if (mouseIsConnected != wasConnected) {
+      notifyListeners();
     }
   }
 
@@ -166,40 +207,137 @@
     if (event.kind != PointerDeviceKind.mouse) {
       return;
     }
-    final int deviceId = event.device;
+    final int device = event.device;
     if (event is PointerAddedEvent) {
-      // 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.
-      _sendMouseNotifications(<int>{deviceId});
-    } else {
-      if (event is PointerMoveEvent || event is PointerHoverEvent || event is PointerDownEvent) {
-        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.
-          _sendMouseNotifications(<int>{deviceId});
-        }
+      _addMouseDevice(device, event);
+    } else if (event is PointerRemovedEvent) {
+      _removeMouseDevice(device, event);
+    } else if (event is PointerHoverEvent) {
+      final _MouseState mouseState = _guaranteeMouseState(device, event);
+      final PointerEvent previousEvent = mouseState.mostRecentEvent;
+      mouseState.mostRecentEvent = event;
+      if (previousEvent is PointerAddedEvent || previousEvent.position != event.position) {
+        // Only send notifications if we have our first event, or if the
+        // location of the mouse has changed
+        _checkDeviceUpdates(device: device);
       }
     }
   }
 
-  _TrackedAnnotation _findAnnotation(MouseTrackerAnnotation annotation) {
-    final _TrackedAnnotation trackedAnnotation = _trackedAnnotations[annotation];
-    assert(
-        trackedAnnotation != null,
-        'Unable to find annotation $annotation in tracked annotations. '
-        'Check that attachAnnotation has been called for all annotated layers.');
-    return trackedAnnotation;
+  bool _scheduledPostFramePositionCheck = false;
+  // Schedules a position check at the end of this frame.
+  // It is only called during a frame during which annotations 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 (!_scheduledPostFramePositionCheck) {
+      _scheduledPostFramePositionCheck = true;
+      SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
+        _checkAllDevicesUpdates();
+        _scheduledPostFramePositionCheck = false;
+      });
+    }
+  }
+
+  // Collect the latest states of the given mouse device `device`, and call
+  // interested callbacks.
+  //
+  // The enter or exit events are called for annotations that the pointer
+  // enters or leaves, while hover events are always called for each
+  // annotations that the pointer stays in, even if the pointer has not moved
+  // since the last call. Therefore it's caller's responsibility to check if
+  // the pointer has moved.
+  //
+  // If `disconnectedMouseState` is provided, this state will be used instead,
+  // but this mouse will be hovering no annotations.
+  void _checkDeviceUpdates({
+    int device,
+    _MouseState disconnectedMouseState,
+  }) {
+    final _MouseState mouseState = disconnectedMouseState ?? _mouseStates[device];
+    final bool thisDeviceIsConnected = mouseState != disconnectedMouseState;
+    assert(mouseState != null);
+
+    final LinkedHashSet<MouseTrackerAnnotation> nextAnnotations =
+      (_hasAttachedAnnotations && thisDeviceIsConnected)
+        ? LinkedHashSet<MouseTrackerAnnotation>.from(
+            annotationFinder(mouseState.mostRecentEvent.position)
+          )
+        : <MouseTrackerAnnotation>{};
+
+    _dispatchDeviceCallbacks(
+      currentState: mouseState,
+      nextAnnotations: nextAnnotations,
+    );
+
+    mouseState.lastAnnotations = nextAnnotations;
+  }
+
+  // Collect the latest states of all mouse devices, and call interested
+  // callbacks.
+  //
+  // For detailed behaviors, see [_checkDeviceUpdates].
+  void _checkAllDevicesUpdates() {
+    for (final int device in _mouseStates.keys) {
+      _checkDeviceUpdates(device: device);
+    }
+  }
+
+  // Dispatch callbacks related to a device after all necessary information
+  // has been collected.
+  //
+  // This function should not change the provided states, and should not access
+  // information that is not provided in parameters (hence being static).
+  static void _dispatchDeviceCallbacks({
+    @required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations,
+    @required _MouseState currentState,
+  }) {
+    // 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".
+    // The algorithm here is explained in
+    // https://github.com/flutter/flutter/issues/41420
+
+    // The `nextAnnotations` is annotations that contains this device in the
+    // coming frame in visual order.
+    // Order is preserved with the help of [LinkedHashSet].
+
+    final PointerEvent mostRecentEvent = currentState.mostRecentEvent;
+    // The `lastAnnotations` is annotations that contains this device in the
+    // previous frame in visual order.
+    final LinkedHashSet<MouseTrackerAnnotation> lastAnnotations = currentState.lastAnnotations;
+
+    // Send exit events in visual order.
+    final Iterable<MouseTrackerAnnotation> exitingAnnotations =
+      lastAnnotations.difference(nextAnnotations);
+    for (final MouseTrackerAnnotation annotation in exitingAnnotations) {
+      if (annotation.onExit != null) {
+        annotation.onExit(PointerExitEvent.fromMouseEvent(mostRecentEvent));
+      }
+    }
+
+    // Send enter events in reverse visual order.
+    final Iterable<MouseTrackerAnnotation> enteringAnnotations =
+      nextAnnotations.difference(lastAnnotations).toList().reversed;
+    for (final MouseTrackerAnnotation annotation in enteringAnnotations) {
+      if (annotation.onEnter != null) {
+        annotation.onEnter(PointerEnterEvent.fromMouseEvent(mostRecentEvent));
+      }
+    }
+
+    // Send hover events in reverse visual order.
+    // For now the order between the hover events is designed this way for no
+    // solid reasons but to keep it aligned with enter events for simplicity.
+    if (mostRecentEvent is PointerHoverEvent) {
+      final Iterable<MouseTrackerAnnotation> hoveringAnnotations =
+        nextAnnotations.toList().reversed;
+      for (final MouseTrackerAnnotation annotation in hoveringAnnotations) {
+        if (annotation.onHover != null) {
+          annotation.onHover(mostRecentEvent);
+        }
+      }
+    }
   }
 
   /// Checks if the given [MouseTrackerAnnotation] is attached to this
@@ -209,127 +347,59 @@
   /// MouseTracker. Do not call in other contexts.
   @visibleForTesting
   bool isAnnotationAttached(MouseTrackerAnnotation annotation) {
-    return _trackedAnnotations.containsKey(annotation);
+    return _trackedAnnotations.contains(annotation);
   }
 
-  // Tells interested objects that a mouse has entered, exited, or moved, given
-  // a callback to fetch the [MouseTrackerAnnotation] associated with a global
-  // offset.
-  //
-  // This is called from a post-frame callback when the layer tree has been
-  // updated, right after rendering the frame.
-  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);
-    }
-
-    void exitAllDevices(_TrackedAnnotation trackedAnnotation) {
-      if (trackedAnnotation.activeDevices.isNotEmpty) {
-        final Set<int> deviceIds = trackedAnnotation.activeDevices.toSet();
-        for (int deviceId in deviceIds) {
-          exitAnnotation(trackedAnnotation, deviceId);
-        }
-      }
-    }
-
-    try {
-      // This indicates that all mouse pointers were removed, or none have been
-      // connected yet. If no mouse is connected, then we want to make sure that
-      // all active annotations are exited.
-      if (!mouseIsConnected) {
-        _trackedAnnotations.values.forEach(exitAllDevices);
-        return;
-      }
-
-      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
-        // exit to all active tracked annotations, since none of them were hit.
-        if (hits.isEmpty) {
-          // Send an exit to all tracked animations tracking this deviceId.
-          for (_TrackedAnnotation trackedAnnotation in _trackedAnnotations.values) {
-            exitAnnotation(trackedAnnotation, deviceId);
-          }
-          continue;
-        }
-
-        final Set<_TrackedAnnotation> hitAnnotations = hits.map<_TrackedAnnotation>((MouseTrackerAnnotation hit) => _findAnnotation(hit)).toSet();
-        for (_TrackedAnnotation hitAnnotation in hitAnnotations) {
-          if (!hitAnnotation.activeDevices.contains(deviceId)) {
-            // A tracked annotation that just became active and needs to have an enter
-            // event sent to it.
-            hitAnnotation.activeDevices.add(deviceId);
-            if (hitAnnotation.annotation?.onEnter != null) {
-              hitAnnotation.annotation.onEnter(PointerEnterEvent.fromMouseEvent(lastEvent));
-            }
-          }
-          if (hitAnnotation.annotation?.onHover != null && lastEvent is PointerHoverEvent) {
-            hitAnnotation.annotation.onHover(lastEvent);
-          }
-
-          // Tell any tracked annotations that weren't hit that they are no longer
-          // active.
-          for (_TrackedAnnotation trackedAnnotation in _trackedAnnotations.values) {
-            if (hitAnnotations.contains(trackedAnnotation)) {
-              continue;
-            }
-            if (trackedAnnotation.activeDevices.contains(deviceId)) {
-              if (trackedAnnotation.annotation?.onExit != null) {
-                trackedAnnotation.annotation.onExit(PointerExitEvent.fromMouseEvent(lastEvent));
-              }
-              trackedAnnotation.activeDevices.remove(deviceId);
-            }
-          }
-        }
-      }
-    } finally {
-      _pendingRemovals.clear();
-    }
-  }
-
-  void _addMouseEvent(int deviceId, PointerEvent event) {
-    final bool wasConnected = mouseIsConnected;
-    if (event is PointerAddedEvent) {
-      // If we are adding the device again, then we're not removing it anymore.
-      _pendingRemovals.remove(deviceId);
-    }
-    _lastMouseEvent[deviceId] = event;
-    if (mouseIsConnected != wasConnected) {
-      notifyListeners();
-    }
-  }
-
-  void _removeMouseEvent(int deviceId, PointerEvent event) {
-    final bool wasConnected = mouseIsConnected;
-    assert(event is PointerRemovedEvent);
-    _pendingRemovals[deviceId] = event;
-    _lastMouseEvent.remove(deviceId);
-    if (mouseIsConnected != wasConnected) {
-      notifyListeners();
-    }
-  }
-
-  // A list of device IDs that should be removed and notified when scheduling a
-  // mouse position check.
-  final Map<int, PointerRemovedEvent> _pendingRemovals = <int, PointerRemovedEvent>{};
-
-  /// 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.
-  final Map<int, PointerEvent> _lastMouseEvent = <int, PointerEvent>{};
-
   /// Whether or not a mouse is connected and has produced events.
-  bool get mouseIsConnected => _lastMouseEvent.isNotEmpty;
+  bool get mouseIsConnected => _mouseStates.isNotEmpty;
+
+  /// Notify [MouseTracker] that a new mouse tracker annotation has started to
+  /// take effect.
+  ///
+  /// This should be called as soon as the layer that owns this annotation is
+  /// added to the layer tree.
+  ///
+  /// This triggers [MouseTracker] to schedule a mouse position check during the
+  /// post frame to see if this new annotation might trigger enter events.
+  ///
+  /// The [MouseTracker] also uses this to track the number of attached
+  /// annotations, and will skip mouse position checks if there is no
+  /// annotations attached.
+  void attachAnnotation(MouseTrackerAnnotation annotation) {
+    // 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.
+    _trackedAnnotations.add(annotation);
+    if (mouseIsConnected) {
+      _scheduleMousePositionCheck();
+    }
+  }
+
+
+  /// Notify [MouseTracker] that a mouse tracker annotation that was previously
+  /// attached has stopped taking effect.
+  ///
+  /// This should be called as soon as the layer that owns this annotation is
+  /// removed from the layer tree. An assertion error will be thrown if the
+  /// associated layer is not removed and receives another mouse hit.
+  ///
+  /// This triggers [MouseTracker] to perform a mouse position check immediately
+  /// to see if this annotation removal triggers any exit events.
+  ///
+  /// The [MouseTracker] also uses this to track the number of attached
+  /// annotations, and will skip mouse position checks if there is no
+  /// annotations attached.
+  void detachAnnotation(MouseTrackerAnnotation annotation) {
+    _mouseStates.forEach((int device, _MouseState mouseState) {
+      if (mouseState.lastAnnotations.contains(annotation)) {
+        if (annotation.onExit != null) {
+          final PointerEvent event = mouseState.mostRecentEvent;
+          assert(event != null);
+          annotation.onExit(PointerExitEvent.fromMouseEvent(event));
+        }
+        mouseState.lastAnnotations.remove(annotation);
+      }
+    });
+    _trackedAnnotations.remove(annotation);
+  }
 }
diff --git a/packages/flutter/test/gestures/mouse_tracking_test.dart b/packages/flutter/test/gestures/mouse_tracking_test.dart
index 6987010..8cc3629 100644
--- a/packages/flutter/test/gestures/mouse_tracking_test.dart
+++ b/packages/flutter/test/gestures/mouse_tracking_test.dart
@@ -16,211 +16,579 @@
 
 typedef HandleEventCallback = void Function(PointerEvent event);
 
-class TestGestureFlutterBinding extends BindingBase with ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, RendererBinding {
-  HandleEventCallback callback;
-
+class _TestGestureFlutterBinding extends BindingBase
+    with ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, RendererBinding {
   @override
-  void handleEvent(PointerEvent event, HitTestEntry entry) {
-    super.handleEvent(event, entry);
-    if (callback != null) {
-      callback(event);
+  void initInstances() {
+    super.initInstances();
+    postFrameCallbacks = <void Function(Duration)>[];
+  }
+
+  List<void Function(Duration)> postFrameCallbacks;
+
+  // Proxy post-frame callbacks
+  @override
+  void addPostFrameCallback(void Function(Duration) callback) {
+    postFrameCallbacks.add(callback);
+  }
+
+  void flushPostFrameCallbacks(Duration duration) {
+    for (final void Function(Duration) callback in postFrameCallbacks) {
+      callback(duration);
     }
+    postFrameCallbacks.clear();
   }
 }
 
-TestGestureFlutterBinding _binding = TestGestureFlutterBinding();
+_TestGestureFlutterBinding _binding = _TestGestureFlutterBinding();
+MouseTracker get _mouseTracker => RendererBinding.instance.mouseTracker;
 
-void ensureTestGestureBinding() {
-  _binding ??= TestGestureFlutterBinding();
+void _ensureTestGestureBinding() {
+  _binding ??= _TestGestureFlutterBinding();
   assert(GestureBinding.instance != null);
 }
 
 void main() {
-  setUp(ensureTestGestureBinding);
+  void _setUpMouseAnnotationFinder(MouseDetectorAnnotationFinder annotationFinder) {
+    final MouseTracker mouseTracker = MouseTracker(
+      GestureBinding.instance.pointerRouter,
+      annotationFinder,
+    );
+    RendererBinding.instance.initMouseTracker(mouseTracker);
+  }
 
-  final List<PointerEvent> events = <PointerEvent>[];
-  final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
-    onEnter: (PointerEnterEvent event) => events.add(event),
-    onHover: (PointerHoverEvent event) => events.add(event),
-    onExit: (PointerExitEvent event) => events.add(event),
-  );
-  // Only respond to some mouse events.
-  final MouseTrackerAnnotation partialAnnotation = MouseTrackerAnnotation(
-    onEnter: (PointerEnterEvent event) => events.add(event),
-    onHover: (PointerHoverEvent event) => events.add(event),
-  );
-  bool isInHitRegionOne;
-  bool isInHitRegionTwo;
-
-  void clear() {
-    events.clear();
+  // Set up a trivial test environment that includes one annotation, which adds
+  // the enter, hover, and exit events it received to [logEvents].
+  MouseTrackerAnnotation _setUpWithOneAnnotation({List<PointerEvent> logEvents}) {
+    final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
+      onEnter: (PointerEnterEvent event) => logEvents.add(event),
+      onHover: (PointerHoverEvent event) => logEvents.add(event),
+      onExit: (PointerExitEvent event) => logEvents.add(event),
+    );
+    _setUpMouseAnnotationFinder(
+      (Offset position) sync* {
+        yield annotation;
+      },
+    );
+    _mouseTracker.attachAnnotation(annotation);
+    return annotation;
   }
 
   setUp(() {
-    clear();
-    isInHitRegionOne = true;
-    isInHitRegionTwo = false;
-    RendererBinding.instance.initMouseTracker(
-      MouseTracker(
-        GestureBinding.instance.pointerRouter,
-        (Offset position) sync* {
-          if (isInHitRegionOne)
-            yield annotation;
-          else if (isInHitRegionTwo) {
-            yield partialAnnotation;
-          }
-        },
-      ),
-    );
+    _ensureTestGestureBinding();
+    _binding.postFrameCallbacks.clear();
     PointerEventConverter.clearPointers();
   });
 
-  test('receives and processes mouse hover events', () {
-    final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
-      // Will implicitly also add a PointerAdded event.
-      _pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
-    ]);
-    final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
-      _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
-    ]);
-    final ui.PointerDataPacket packet3 = ui.PointerDataPacket(data: <ui.PointerData>[
-      _pointerData(PointerChange.remove, const Offset(1.0, 201.0)),
-    ]);
-    final ui.PointerDataPacket packet4 = ui.PointerDataPacket(data: <ui.PointerData>[
-      _pointerData(PointerChange.hover, const Offset(1.0, 301.0)),
-    ]);
-    final ui.PointerDataPacket packet5 = ui.PointerDataPacket(data: <ui.PointerData>[
-      _pointerData(PointerChange.hover, const Offset(1.0, 401.0), device: 1),
-    ]);
-    RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
-    isInHitRegionOne = true;
-    ui.window.onPointerDataPacket(packet1);
-    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
-      const PointerEnterEvent(position: Offset(0.0, 0.0)),
-      const PointerHoverEvent(position: Offset(0.0, 0.0)),
-    ]));
-    clear();
+  test('MouseTrackerAnnotation has correct toString', () {
+    final MouseTrackerAnnotation annotation1 = MouseTrackerAnnotation(
+      onEnter: (_) {},
+      onExit: (_) {},
+      onHover: (_) {},
+    );
+    expect(
+      annotation1.toString(),
+      equals('MouseTrackerAnnotation#${shortHash(annotation1)}(callbacks: enter hover exit)'),
+    );
 
-    ui.window.onPointerDataPacket(packet2);
+    const MouseTrackerAnnotation annotation2 = MouseTrackerAnnotation();
+    expect(
+      annotation2.toString(),
+      equals('MouseTrackerAnnotation#${shortHash(annotation2)}(callbacks: <none>)'),
+    );
+  });
+
+  test('should detect enter, hover, and exit from Added, Hover, and Removed events', () {
+    final List<PointerEvent> events = <PointerEvent>[];
+    _setUpWithOneAnnotation(logEvents: events);
+
+    final List<bool> listenerLogs = <bool>[];
+    _mouseTracker.addListener(() {
+      listenerLogs.add(_mouseTracker.mouseIsConnected);
+    });
+
+    expect(_mouseTracker.mouseIsConnected, isFalse);
+
+    // Enter
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(1.0, 0.0)),
+    ]));
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+      const PointerEnterEvent(position: Offset(1.0, 0.0)),
+      const PointerHoverEvent(position: Offset(1.0, 0.0)),
+    ]));
+    expect(listenerLogs, <bool>[true]);
+    events.clear();
+    listenerLogs.clear();
+
+    // Hover
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
+    ]));
     expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
       const PointerHoverEvent(position: Offset(1.0, 101.0)),
     ]));
-    clear();
+    expect(_mouseTracker.mouseIsConnected, isTrue);
+    expect(listenerLogs, <bool>[]);
+    events.clear();
 
-    ui.window.onPointerDataPacket(packet3);
+    // Remove
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.remove, const Offset(1.0, 201.0)),
+    ]));
     expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
       const PointerHoverEvent(position: Offset(1.0, 201.0)),
       const PointerExitEvent(position: Offset(1.0, 201.0)),
     ]));
+    expect(listenerLogs, <bool>[false]);
+    events.clear();
+    listenerLogs.clear();
 
-    clear();
-    ui.window.onPointerDataPacket(packet4);
+    // Add again
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(1.0, 301.0)),
+    ]));
     expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
       const PointerEnterEvent(position: Offset(1.0, 301.0)),
       const PointerHoverEvent(position: Offset(1.0, 301.0)),
     ]));
+    expect(listenerLogs, <bool>[true]);
+    events.clear();
+    listenerLogs.clear();
+  });
 
-    // add in a second mouse simultaneously.
-    clear();
-    ui.window.onPointerDataPacket(packet5);
+  test('should correctly handle multiple devices', () {
+    final List<PointerEvent> events = <PointerEvent>[];
+    _setUpWithOneAnnotation(logEvents: events);
+
+    expect(_mouseTracker.mouseIsConnected, isFalse);
+
+    // First mouse
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(0.0, 1.0)),
+    ]));
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+      const PointerEnterEvent(position: Offset(0.0, 1.0)),
+      const PointerHoverEvent(position: Offset(0.0, 1.0)),
+    ]));
+    expect(_mouseTracker.mouseIsConnected, isTrue);
+    events.clear();
+
+    // Second mouse
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(1.0, 401.0), device: 1),
+    ]));
     expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
       const PointerEnterEvent(position: Offset(1.0, 401.0), device: 1),
       const PointerHoverEvent(position: Offset(1.0, 401.0), device: 1),
     ]));
+    expect(_mouseTracker.mouseIsConnected, isTrue);
+    events.clear();
+
+    // First mouse hover
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(0.0, 101.0)),
+    ]));
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+      const PointerHoverEvent(position: Offset(0.0, 101.0)),
+    ]));
+    expect(_mouseTracker.mouseIsConnected, isTrue);
+    events.clear();
+
+    // Second mouse hover
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(1.0, 501.0), device: 1),
+    ]));
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+      const PointerHoverEvent(position: Offset(1.0, 501.0), device: 1),
+    ]));
+    expect(_mouseTracker.mouseIsConnected, isTrue);
+    events.clear();
+
+    // First mouse remove
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.remove, const Offset(0.0, 101.0)),
+    ]));
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+      const PointerExitEvent(position: Offset(0.0, 101.0)),
+    ]));
+    expect(_mouseTracker.mouseIsConnected, isTrue);
+    events.clear();
+
+    // Second mouse hover
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(1.0, 601.0), device: 1),
+    ]));
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+      const PointerHoverEvent(position: Offset(1.0, 601.0), device: 1),
+    ]));
+    expect(_mouseTracker.mouseIsConnected, isTrue);
+    events.clear();
+
+    // Second mouse remove
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.remove, const Offset(1.0, 601.0), device: 1),
+    ]));
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+      const PointerExitEvent(position: Offset(1.0, 601.0), device: 1),
+    ]));
+    expect(_mouseTracker.mouseIsConnected, isFalse);
+    events.clear();
   });
 
-  test('detects exit when annotated layer no longer hit', () {
-    final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
-      _pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
-      _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
-    ]);
-    final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
-      _pointerData(PointerChange.hover, const Offset(1.0, 201.0)),
-    ]);
-    isInHitRegionOne = true;
-    RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
+  test('should handle detaching during the callback of exiting', () {
+    bool isInHitRegion;
+    final List<PointerEvent> events = <PointerEvent>[];
+    final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
+      onEnter: (PointerEnterEvent event) => events.add(event),
+      onHover: (PointerHoverEvent event) => events.add(event),
+      onExit: (PointerExitEvent event) => events.add(event),
+    );
+    _setUpMouseAnnotationFinder((Offset position) sync* {
+      if (isInHitRegion) {
+        yield annotation;
+      }
+    });
 
-    ui.window.onPointerDataPacket(packet1);
+    isInHitRegion = true;
+    _mouseTracker.attachAnnotation(annotation);
 
-    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
-      const PointerEnterEvent(position: Offset(0.0, 0.0)),
-      const PointerHoverEvent(position: Offset(0.0, 0.0)),
-      const PointerHoverEvent(position: Offset(1.0, 101.0)),
+    // Enter
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(1.0, 0.0)),
     ]));
-    // Simulate layer going away by detaching it.
-    clear();
-    isInHitRegionOne = false;
-
-    ui.window.onPointerDataPacket(packet2);
     expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
-      const PointerExitEvent(position: Offset(1.0, 201.0)),
+      const PointerEnterEvent(position: Offset(1.0, 0.0)),
+      const PointerHoverEvent(position: Offset(1.0, 0.0)),
     ]));
+    expect(_mouseTracker.mouseIsConnected, isTrue);
+    events.clear();
 
-    // Actually detach annotation. Shouldn't receive hit.
-    RendererBinding.instance.mouseTracker.detachAnnotation(annotation);
-    clear();
-    isInHitRegionOne = false;
+    // Remove
+    _mouseTracker.addListener(() {
+      if (!_mouseTracker.mouseIsConnected) {
+        _mouseTracker.detachAnnotation(annotation);
+        isInHitRegion = false;
+      }
+    });
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.remove, const Offset(1.0, 0.0)),
+    ]));
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+      const PointerExitEvent(position: Offset(1.0, 0.0)),
+    ]));
+    expect(_mouseTracker.mouseIsConnected, isFalse);
+    events.clear();
+  });
 
-    ui.window.onPointerDataPacket(packet2);
+  test('should not handle non-hover events', () {
+    final List<PointerEvent> events = <PointerEvent>[];
+    _setUpWithOneAnnotation(logEvents: events);
+
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.down, const Offset(0.0, 101.0)),
+    ]));
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+      // This Enter event is triggered by the [PointerAddedEvent] that was
+      // synthesized during the event normalization of pointer event converter.
+      // The [PointerDownEvent] is ignored by [MouseTracker].
+      const PointerEnterEvent(position: Offset(0.0, 101.0)),
+    ]));
+    events.clear();
+
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.move, const Offset(0.0, 201.0)),
+    ]));
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+    ]));
+    events.clear();
+
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.up, const Offset(0.0, 301.0)),
+    ]));
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+    ]));
+    events.clear();
+  });
+
+  test('should detect enter or exit when annotations are attached or detached on the pointer', () {
+    bool isInHitRegion;
+    final List<PointerEvent> events = <PointerEvent>[];
+    final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
+      onEnter: (PointerEnterEvent event) => events.add(event),
+      onHover: (PointerHoverEvent event) => events.add(event),
+      onExit: (PointerExitEvent event) => events.add(event),
+    );
+    _setUpMouseAnnotationFinder((Offset position) sync* {
+      if (isInHitRegion) {
+        yield annotation;
+      }
+    });
+
+    isInHitRegion = false;
+
+    // Connect a mouse when there is no annotation
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.add, const Offset(0.0, 100.0)),
+    ]));
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+    ]));
+    expect(_mouseTracker.mouseIsConnected, isTrue);
+    events.clear();
+
+    // Attach an annotation
+    isInHitRegion = true;
+    _mouseTracker.attachAnnotation(annotation);
+    // No callbacks are triggered immediately
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+    ]));
+    expect(_binding.postFrameCallbacks, hasLength(1));
+
+    _binding.flushPostFrameCallbacks(Duration.zero);
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+      const PointerEnterEvent(position: Offset(0.0, 100.0)),
+    ]));
+    events.clear();
+
+    // Detach the annotation
+    isInHitRegion = false;
+    _mouseTracker.detachAnnotation(annotation);
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+      const PointerExitEvent(position: Offset(0.0, 100.0)),
+    ]));
+    expect(_binding.postFrameCallbacks, hasLength(0));
+  });
+
+  test('should correctly stay quiet when annotations are attached or detached not on the pointer', () {
+    final List<PointerEvent> events = <PointerEvent>[];
+    final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
+      onEnter: (PointerEnterEvent event) => events.add(event),
+      onHover: (PointerHoverEvent event) => events.add(event),
+      onExit: (PointerExitEvent event) => events.add(event),
+    );
+    _setUpMouseAnnotationFinder((Offset position) sync* {
+      // This annotation is never in the region
+    });
+
+    // Connect a mouse when there is no annotation
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.add, const Offset(0.0, 100.0)),
+    ]));
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+    ]));
+    expect(_mouseTracker.mouseIsConnected, isTrue);
+    events.clear();
+
+    // Attach an annotation out of region
+    _mouseTracker.attachAnnotation(annotation);
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+    ]));
+    expect(_binding.postFrameCallbacks, hasLength(1));
+
+    _binding.flushPostFrameCallbacks(Duration.zero);
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+    ]));
+    events.clear();
+
+    // Detach the annotation
+    _mouseTracker.detachAnnotation(annotation);
+    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
+    ]));
+    expect(_binding.postFrameCallbacks, hasLength(0));
+
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.remove, const Offset(0.0, 100.0)),
+    ]));
     expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
     ]));
   });
 
-  test("don't flip out if not all mouse events are listened to", () {
+  test('should not flip out if not all mouse events are listened to', () {
+    bool isInHitRegionOne = true;
+    bool isInHitRegionTwo = false;
+    final MouseTrackerAnnotation annotation1 = MouseTrackerAnnotation(
+      onEnter: (PointerEnterEvent event) {}
+    );
+    final MouseTrackerAnnotation annotation2 = MouseTrackerAnnotation(
+      onExit: (PointerExitEvent event) {}
+    );
+    _setUpMouseAnnotationFinder((Offset position) sync* {
+      if (isInHitRegionOne)
+        yield annotation1;
+      else if (isInHitRegionTwo)
+        yield annotation2;
+    });
+
     final ui.PointerDataPacket packet = ui.PointerDataPacket(data: <ui.PointerData>[
       _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
     ]);
 
     isInHitRegionOne = false;
     isInHitRegionTwo = true;
-    RendererBinding.instance.mouseTracker.attachAnnotation(partialAnnotation);
+    _mouseTracker.attachAnnotation(annotation2);
 
     ui.window.onPointerDataPacket(packet);
-    RendererBinding.instance.mouseTracker.detachAnnotation(partialAnnotation);
+    _mouseTracker.detachAnnotation(annotation2);
     isInHitRegionTwo = false;
 
     // Passes if no errors are thrown
   });
 
-  test('detects exit when mouse goes away', () {
-    final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
-      _pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
-      _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
-    ]);
-    final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
-      _pointerData(PointerChange.remove, const Offset(1.0, 201.0)),
-    ]);
-    isInHitRegionOne = true;
-    RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
-    ui.window.onPointerDataPacket(packet1);
-    ui.window.onPointerDataPacket(packet2);
-    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
-      const PointerEnterEvent(position: Offset(0.0, 0.0)),
-      const PointerHoverEvent(position: Offset(0.0, 0.0)),
-      const PointerHoverEvent(position: Offset(1.0, 101.0)),
-      const PointerHoverEvent(position: Offset(1.0, 201.0)),
-      const PointerExitEvent(position: Offset(1.0, 201.0)),
+  test('should not call annotationFinder when no annotations are attached', () {
+    final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
+      onEnter: (PointerEnterEvent event) {},
+    );
+    int finderCalled = 0;
+    _setUpMouseAnnotationFinder((Offset position) sync* {
+      finderCalled++;
+      // This annotation is never in the region
+    });
+
+    // When no annotations are attached, hovering should not call finder.
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(0.0, 101.0)),
     ]));
+    expect(finderCalled, 0);
+
+    // Attaching should call finder during the post frame.
+    _mouseTracker.attachAnnotation(annotation);
+    expect(finderCalled, 0);
+
+    _binding.flushPostFrameCallbacks(Duration.zero);
+    expect(finderCalled, 1);
+    finderCalled = 0;
+
+    // When annotations are attached, hovering should call finder.
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(0.0, 201.0)),
+    ]));
+    expect(finderCalled, 1);
+    finderCalled = 0;
+
+    // Detaching an annotation should not call finder (because only history
+    // records are needed).
+    _mouseTracker.detachAnnotation(annotation);
+    expect(finderCalled, 0);
+
+    // When all annotations are detached, hovering should not call finder.
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(0.0, 201.0)),
+    ]));
+    expect(finderCalled, 0);
   });
 
-  test('handles mouse down and move', () {
-    final ui.PointerDataPacket packet1 = ui.PointerDataPacket(data: <ui.PointerData>[
-      _pointerData(PointerChange.hover, const Offset(0.0, 0.0)),
-      _pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
-    ]);
-    final ui.PointerDataPacket packet2 = ui.PointerDataPacket(data: <ui.PointerData>[
-      _pointerData(PointerChange.down, const Offset(1.0, 101.0)),
-      _pointerData(PointerChange.move, const Offset(1.0, 201.0)),
-    ]);
-    isInHitRegionOne = true;
-    RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
-    ui.window.onPointerDataPacket(packet1);
-    ui.window.onPointerDataPacket(packet2);
-    expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
-      const PointerEnterEvent(position: Offset(0.0, 0.0), delta: Offset(0.0, 0.0)),
-      const PointerHoverEvent(position: Offset(0.0, 0.0), delta: Offset(0.0, 0.0)),
-      const PointerHoverEvent(position: Offset(1.0, 101.0), delta: Offset(1.0, 101.0)),
+  test('should trigger callbacks between parents and children in correct order', () {
+    // This test simulates the scenario of a layer being the child of another.
+    //
+    //   ———————————
+    //   |A        |
+    //   |  —————— |
+    //   |  |B   | |
+    //   |  —————— |
+    //   ———————————
+
+    bool isInB;
+    final List<String> logs = <String>[];
+    final MouseTrackerAnnotation annotationA = MouseTrackerAnnotation(
+      onEnter: (PointerEnterEvent event) => logs.add('enterA'),
+      onExit: (PointerExitEvent event) => logs.add('exitA'),
+      onHover: (PointerHoverEvent event) => logs.add('hoverA'),
+    );
+    final MouseTrackerAnnotation annotationB = MouseTrackerAnnotation(
+      onEnter: (PointerEnterEvent event) => logs.add('enterB'),
+      onExit: (PointerExitEvent event) => logs.add('exitB'),
+      onHover: (PointerHoverEvent event) => logs.add('hoverB'),
+    );
+    _setUpMouseAnnotationFinder((Offset position) sync* {
+      // Children's annotations come before parents'
+      if (isInB) {
+        yield annotationB;
+        yield annotationA;
+      }
+    });
+    _mouseTracker.attachAnnotation(annotationA);
+    _mouseTracker.attachAnnotation(annotationB);
+
+    // Starts out of A
+    isInB = false;
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(0.0, 1.0)),
     ]));
+    expect(logs, <String>[]);
+
+    // Moves into B within one frame
+    isInB = true;
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(0.0, 10.0)),
+    ]));
+    expect(logs, <String>['enterA', 'enterB', 'hoverA', 'hoverB']);
+    logs.clear();
+
+    // Moves out of A within one frame
+    isInB = false;
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(0.0, 20.0)),
+    ]));
+    expect(logs, <String>['exitB', 'exitA']);
+  });
+
+  test('should trigger callbacks between disjoint siblings in correctly order', () {
+    // This test simulates the scenario of 2 sibling layers that do not overlap
+    // with each other.
+    //
+    //   ————————  ————————
+    //   |A     |  |B     |
+    //   |      |  |      |
+    //   ————————  ————————
+
+    bool isInA;
+    bool isInB;
+    final List<String> logs = <String>[];
+    final MouseTrackerAnnotation annotationA = MouseTrackerAnnotation(
+      onEnter: (PointerEnterEvent event) => logs.add('enterA'),
+      onExit: (PointerExitEvent event) => logs.add('exitA'),
+      onHover: (PointerHoverEvent event) => logs.add('hoverA'),
+    );
+    final MouseTrackerAnnotation annotationB = MouseTrackerAnnotation(
+      onEnter: (PointerEnterEvent event) => logs.add('enterB'),
+      onExit: (PointerExitEvent event) => logs.add('exitB'),
+      onHover: (PointerHoverEvent event) => logs.add('hoverB'),
+    );
+    _setUpMouseAnnotationFinder((Offset position) sync* {
+      if (isInA) {
+        yield annotationA;
+      } else if (isInB) {
+        yield annotationB;
+      }
+    });
+    _mouseTracker.attachAnnotation(annotationA);
+    _mouseTracker.attachAnnotation(annotationB);
+
+    // Starts within A
+    isInA = true;
+    isInB = false;
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(0.0, 1.0)),
+    ]));
+    expect(logs, <String>['enterA', 'hoverA']);
+    logs.clear();
+
+    // Moves into B within one frame
+    isInA = false;
+    isInB = true;
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(0.0, 10.0)),
+    ]));
+    expect(logs, <String>['exitA', 'enterB', 'hoverB']);
+    logs.clear();
+
+    // Moves into A within one frame
+    isInA = true;
+    isInB = false;
+    ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
+      _pointerData(PointerChange.hover, const Offset(0.0, 1.0)),
+    ]));
+    expect(logs, <String>['exitB', 'enterA', 'hoverA']);
   });
 }
 
diff --git a/packages/flutter/test/widgets/mouse_region_test.dart b/packages/flutter/test/widgets/mouse_region_test.dart
index c5f6683..d959175 100644
--- a/packages/flutter/test/widgets/mouse_region_test.dart
+++ b/packages/flutter/test/widgets/mouse_region_test.dart
@@ -836,7 +836,7 @@
       // Move to the overlapping area
       await gesture.moveTo(const Offset(75, 75));
       await tester.pumpAndSettle();
-      expect(logs, <String>['enterA', 'enterC', 'enterB']);
+      expect(logs, <String>['enterA', 'enterB', 'enterC']);
       logs.clear();
 
       // Move to the B only area
@@ -866,7 +866,7 @@
       // Move out
       await gesture.moveTo(const Offset(160, 160));
       await tester.pumpAndSettle();
-      expect(logs, <String>['exitA', 'exitB', 'exitC']);
+      expect(logs, <String>['exitC', 'exitB', 'exitA']);
     });
 
     testWidgets('an opaque one should prevent MouseRegions behind it receiving pointers', (WidgetTester tester) async {
@@ -890,13 +890,13 @@
       // Move to the B only area
       await gesture.moveTo(const Offset(25, 75));
       await tester.pumpAndSettle();
-      expect(logs, <String>['enterB', 'exitC']);
+      expect(logs, <String>['exitC', 'enterB']);
       logs.clear();
 
       // Move back to the overlapping area
       await gesture.moveTo(const Offset(75, 75));
       await tester.pumpAndSettle();
-      expect(logs, <String>['enterC', 'exitB']);
+      expect(logs, <String>['exitB', 'enterC']);
       logs.clear();
 
       // Move to the C only area
@@ -914,7 +914,7 @@
       // Move out
       await gesture.moveTo(const Offset(160, 160));
       await tester.pumpAndSettle();
-      expect(logs, <String>['exitA', 'exitC']);
+      expect(logs, <String>['exitC', 'exitA']);
     });
 
     testWidgets('opaque should default to true', (WidgetTester tester) async {
@@ -938,7 +938,7 @@
       // Move out
       await gesture.moveTo(const Offset(160, 160));
       await tester.pumpAndSettle();
-      expect(logs, <String>['exitA', 'exitC']);
+      expect(logs, <String>['exitC', 'exitA']);
     });
   });