From 543d3cfa572327efc52011aaa8a1aed6f921f6ea Mon Sep 17 00:00:00 2001 From: xubaolin Date: Wed, 18 Nov 2020 00:03:02 +0800 Subject: [PATCH] Add a [valid] property of [MouseTrackerAnnotation] indicates the annotation states. (#69866) --- .../lib/src/rendering/mouse_tracking.dart | 18 +++++++--- .../lib/src/rendering/platform_view.dart | 3 ++ .../flutter/lib/src/rendering/proxy_box.dart | 22 +++++++++++++ .../rendering/mouse_tracking_test_utils.dart | 5 ++- .../test/widgets/mouse_region_test.dart | 33 +++++++++++++++++++ 5 files changed, 76 insertions(+), 5 deletions(-) diff --git a/packages/flutter/lib/src/rendering/mouse_tracking.dart b/packages/flutter/lib/src/rendering/mouse_tracking.dart index bdd90f8fbd6..1fb9055ae49 100644 --- a/packages/flutter/lib/src/rendering/mouse_tracking.dart +++ b/packages/flutter/lib/src/rendering/mouse_tracking.dart @@ -55,10 +55,11 @@ class MouseTrackerAnnotation with Diagnosticable { this.onEnter, this.onExit, this.cursor = MouseCursor.defer, + this.validForMouseTracker = true, }) : assert(cursor != null); /// Triggered when a mouse pointer, with or without buttons pressed, has - /// entered the region. + /// entered the region and [validForMouseTracker] is true. /// /// This callback is triggered when the pointer has started to be contained by /// the region, either due to a pointer event, or due to the movement or @@ -72,7 +73,7 @@ class MouseTrackerAnnotation with Diagnosticable { final PointerEnterEventListener? onEnter; /// Triggered when a mouse pointer, with or without buttons pressed, has - /// exited the region. + /// exited the region and [validForMouseTracker] is true. /// /// This callback is triggered when the pointer has stopped being contained /// by the region, either due to a pointer event, or due to the movement or @@ -100,6 +101,15 @@ class MouseTrackerAnnotation with Diagnosticable { /// * [MouseRegion.cursor], which provide values to this field. final MouseCursor cursor; + /// Whether this is included when [MouseTracker] collects the list of annotations. + /// + /// If [validForMouseTracker] is false, this object is excluded from the current annotation list + /// even if it's included in the hit test, affecting mouse-related behavior such as enter events, + /// exit events, and mouse cursors. The [validForMouseTracker] does not affect hit testing. + /// + /// The [validForMouseTracker] is true for [MouseTrackerAnnotation]s built by the constructor. + final bool validForMouseTracker; + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); @@ -487,7 +497,7 @@ mixin _MouseTrackerEventMixin on BaseMouseTracker { final PointerExitEvent baseExitEvent = PointerExitEvent.fromMouseEvent(latestEvent); lastAnnotations.forEach((MouseTrackerAnnotation annotation, Matrix4 transform) { if (!nextAnnotations.containsKey(annotation)) - if (annotation.onExit != null) + if (annotation.validForMouseTracker && annotation.onExit != null) annotation.onExit!(baseExitEvent.transformed(lastAnnotations[annotation])); }); @@ -498,7 +508,7 @@ mixin _MouseTrackerEventMixin on BaseMouseTracker { ).toList(); final PointerEnterEvent baseEnterEvent = PointerEnterEvent.fromMouseEvent(latestEvent); for (final MouseTrackerAnnotation annotation in enteringAnnotations.reversed) { - if (annotation.onEnter != null) + if (annotation.validForMouseTracker && annotation.onEnter != null) annotation.onEnter!(baseEnterEvent.transformed(nextAnnotations[annotation])); } } diff --git a/packages/flutter/lib/src/rendering/platform_view.dart b/packages/flutter/lib/src/rendering/platform_view.dart index 04786bf7fb2..8f01e33769e 100644 --- a/packages/flutter/lib/src/rendering/platform_view.dart +++ b/packages/flutter/lib/src/rendering/platform_view.dart @@ -732,6 +732,9 @@ mixin _PlatformViewGestureMixin on RenderBox implements MouseTrackerAnnotation { @override MouseCursor get cursor => MouseCursor.uncontrolled; + @override + bool get validForMouseTracker => true; + @override void handleEvent(PointerEvent event, HitTestEntry entry) { if (event is PointerDownEvent) { diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index c55ed9cd545..8b67632b94e 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2777,11 +2777,13 @@ class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation this.onHover, this.onExit, MouseCursor cursor = MouseCursor.defer, + bool validForMouseTracker = true, bool opaque = true, RenderBox? child, }) : assert(opaque != null), assert(cursor != null), _cursor = cursor, + _validForMouseTracker = validForMouseTracker, _opaque = opaque, super(child); @@ -2849,6 +2851,25 @@ class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation } } + @override + bool get validForMouseTracker => _validForMouseTracker; + bool _validForMouseTracker; + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + _validForMouseTracker = true; + } + + @override + void detach() { + // It's possible that the renderObject be detached during mouse events + // dispatching, set the [MouseTrackerAnnotation.validForMouseTracker] false to prevent + // the callbacks from being called. + _validForMouseTracker = false; + super.detach(); + } + @override void performResize() { size = constraints.biggest; @@ -2868,6 +2889,7 @@ class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation )); properties.add(DiagnosticsProperty('cursor', cursor, defaultValue: MouseCursor.defer)); properties.add(DiagnosticsProperty('opaque', opaque, defaultValue: true)); + properties.add(FlagProperty('validForMouseTracker', value: validForMouseTracker, defaultValue: true, ifFalse: 'invalid for MouseTracker')); } } diff --git a/packages/flutter/test/rendering/mouse_tracking_test_utils.dart b/packages/flutter/test/rendering/mouse_tracking_test_utils.dart index 21f294623f6..e5494c8dd9c 100644 --- a/packages/flutter/test/rendering/mouse_tracking_test_utils.dart +++ b/packages/flutter/test/rendering/mouse_tracking_test_utils.dart @@ -72,7 +72,7 @@ class TestMouseTrackerFlutterBinding extends BindingBase // An object that mocks the behavior of a render object with [MouseTrackerAnnotation]. class TestAnnotationTarget with Diagnosticable implements MouseTrackerAnnotation, HitTestTarget { - const TestAnnotationTarget({this.onEnter, this.onHover, this.onExit, this.cursor = MouseCursor.defer}); + const TestAnnotationTarget({this.onEnter, this.onHover, this.onExit, this.cursor = MouseCursor.defer, this.validForMouseTracker = true}); @override final PointerEnterEventListener? onEnter; @@ -85,6 +85,9 @@ class TestAnnotationTarget with Diagnosticable implements MouseTrackerAnnotation @override final MouseCursor cursor; + @override + final bool validForMouseTracker; + @override void handleEvent(PointerEvent event, HitTestEntry entry) { if (event is PointerHoverEvent) diff --git a/packages/flutter/test/widgets/mouse_region_test.dart b/packages/flutter/test/widgets/mouse_region_test.dart index c8e0f174a7f..02a379c66b3 100644 --- a/packages/flutter/test/widgets/mouse_region_test.dart +++ b/packages/flutter/test/widgets/mouse_region_test.dart @@ -1695,6 +1695,7 @@ void main() { onExit: (PointerExitEvent event) {}, onHover: (PointerHoverEvent event) {}, cursor: SystemMouseCursors.click, + validForMouseTracker: false, child: RenderErrorBox(), ).debugFillProperties(builder); @@ -1706,6 +1707,7 @@ void main() { 'size: MISSING', 'listeners: enter, hover, exit', 'cursor: SystemMouseCursor(click)', + 'invalid for MouseTracker', ]); }); @@ -1728,6 +1730,37 @@ void main() { await gesture.moveBy(const Offset(10.0, 10.0)); expect(tester.binding.hasScheduledFrame, isFalse); }); + + // Regression test for https://github.com/flutter/flutter/issues/67044 + testWidgets('Handle mouse events should ignore the detached MouseTrackerAnnotation', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: Center( + child: Draggable( + feedback: Container(width: 20, height: 20, color: Colors.blue), + childWhenDragging: Container(width: 20, height: 20, color: Colors.yellow), + child: RaisedButton(child: const Text('Drag me'), onPressed: (){}), + ), + ), + )); + + // Long press the button with mouse. + final Offset textFieldPos = tester.getCenter(find.byType(Text)); + final TestGesture gesture = await tester.startGesture( + textFieldPos, + kind: PointerDeviceKind.mouse, + ); + addTearDown(gesture.removePointer); + await tester.pump(const Duration(seconds: 2)); + await tester.pumpAndSettle(); + + // Drag the Draggable Widget will replace the child with [childWhenDragging]. + await gesture.moveBy(const Offset(10.0, 10.0)); + await tester.pump(); // Trigger detach the button. + + // Continue drag mouse should not trigger any assert. + await gesture.moveBy(const Offset(10.0, 10.0)); + expect(tester.takeException(), isNull); + }); } // Render widget `topLeft` at the top-left corner, stacking on top of the widget