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);
+ });
});
}