diff --git a/packages/flutter/lib/src/gestures/binding.dart b/packages/flutter/lib/src/gestures/binding.dart index efe8c33a1b..ccb6be31be 100644 --- a/packages/flutter/lib/src/gestures/binding.dart +++ b/packages/flutter/lib/src/gestures/binding.dart @@ -337,8 +337,18 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H /// State for all pointers which are currently down. /// - /// The state of hovering pointers is not tracked because that would require - /// hit-testing on every frame. + /// This map caches the hit test result done when the pointer goes down + /// ([PointerDownEvent] and [PointerPanZoomStartEvent]). This hit test result + /// will be used throughout the entire pointer interaction; that is, the + /// pointer is seen as pointing to the same place even if it has moved away + /// until pointer goes up ([PointerUpEvent] and [PointerPanZoomEndEvent]). + /// This matches the expected gesture interaction with a button, and allows + /// devices that don't support hovering to perform as few hit tests as + /// possible. + /// + /// On the other hand, hovering requires hit testing on almost every frame. + /// This is handled in [RendererBinding] and [MouseTracker], and will ignore + /// the results cached here. final Map _hitTests = {}; /// Dispatch an event to the targets found by a hit test on its position. diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 5a1f8969b0..dec407bd5f 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -315,18 +315,21 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture @visibleForTesting void initMouseTracker([MouseTracker? tracker]) { _mouseTracker?.dispose(); - _mouseTracker = tracker ?? MouseTracker(); + _mouseTracker = tracker ?? MouseTracker((Offset position, int viewId) { + final HitTestResult result = HitTestResult(); + hitTestInView(result, position, viewId); + return result; + }); } @override // from GestureBinding void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { _mouseTracker!.updateWithEvent( event, - // Enter and exit events should be triggered with or without buttons - // pressed. When the button is pressed, normal hit test uses a cached + // When the button is pressed, normal hit test uses a cached // result, but MouseTracker requires that the hit test is re-executed to // update the hovering events. - () => (hitTestResult == null || event is PointerMoveEvent) ? renderView.hitTestMouseTrackers(event.position) : hitTestResult, + event is PointerMoveEvent ? null : hitTestResult, ); super.dispatchEvent(event, hitTestResult); } @@ -372,7 +375,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture _debugMouseTrackerUpdateScheduled = false; return true; }()); - _mouseTracker!.updateAllDevices(renderView.hitTestMouseTrackers); + _mouseTracker!.updateAllDevices(); }); } @@ -518,8 +521,16 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture await endOfFrame; } + late final int _implicitViewId = platformDispatcher.implicitView!.viewId; + @override void hitTestInView(HitTestResult result, Offset position, int viewId) { + // Currently Flutter only supports one view, the implicit view `renderView`. + // TODO(dkwingsmt): After Flutter supports multi-view, look up the correct + // render view for the ID. + // https://github.com/flutter/flutter/issues/121573 + assert(viewId == _implicitViewId, + 'Unexpected view ID $viewId (expecting implicit view ID $_implicitViewId)'); assert(viewId == renderView.flutterView.viewId); renderView.hitTest(result, position: position); super.hitTestInView(result, position, viewId); diff --git a/packages/flutter/lib/src/rendering/mouse_tracker.dart b/packages/flutter/lib/src/rendering/mouse_tracker.dart index 84eda80669..aa2efc7c6c 100644 --- a/packages/flutter/lib/src/rendering/mouse_tracker.dart +++ b/packages/flutter/lib/src/rendering/mouse_tracker.dart @@ -20,11 +20,11 @@ export 'package:flutter/services.dart' show MouseCursor, SystemMouseCursors; -/// Signature for searching for [MouseTrackerAnnotation]s at the given offset. +/// Signature for hit testing at the given offset for the specified view. /// /// It is used by the [MouseTracker] to fetch annotations for the mouse /// position. -typedef MouseDetectorAnnotationFinder = HitTestResult Function(Offset offset); +typedef MouseTrackerHitTest = HitTestResult Function(Offset offset, int viewId); // Various states of a connected mouse device used by [MouseTracker]. class _MouseState { @@ -161,6 +161,16 @@ class _MouseTrackerUpdateDetails with Diagnosticable { /// An instance of [MouseTracker] is owned by the global singleton /// [RendererBinding]. class MouseTracker extends ChangeNotifier { + /// Create a mouse tracker. + /// + /// The `hitTestInView` is used to find the render objects on a given + /// position in the specific view. It is typically provided by the + /// [RendererBinding]. + MouseTracker(MouseTrackerHitTest hitTestInView) + : _hitTestInView = hitTestInView; + + final MouseTrackerHitTest _hitTestInView; + final MouseCursorManager _mouseCursorMixin = MouseCursorManager( SystemMouseCursors.basic, ); @@ -224,7 +234,7 @@ class MouseTracker extends ChangeNotifier { || lastEvent.position != event.position; } - LinkedHashMap _hitTestResultToAnnotations(HitTestResult result) { + LinkedHashMap _hitTestInViewResultToAnnotations(HitTestResult result) { final LinkedHashMap annotations = LinkedHashMap(); for (final HitTestEntry entry in result.path) { final Object target = entry.target; @@ -240,14 +250,15 @@ class MouseTracker extends ChangeNotifier { // // If the device is not connected or not a mouse, an empty map is returned // without calling `hitTest`. - LinkedHashMap _findAnnotations(_MouseState state, MouseDetectorAnnotationFinder hitTest) { + LinkedHashMap _findAnnotations(_MouseState state) { final Offset globalPosition = state.latestEvent.position; final int device = state.device; + final int viewId = state.latestEvent.viewId; if (!_mouseStates.containsKey(device)) { return LinkedHashMap(); } - return _hitTestResultToAnnotations(hitTest(globalPosition)); + return _hitTestInViewResultToAnnotations(_hitTestInView(globalPosition, viewId)); } // A callback that is called on the update of a device. @@ -279,25 +290,34 @@ class MouseTracker extends ChangeNotifier { /// Whether or not at least one mouse is connected and has produced events. bool get mouseIsConnected => _mouseStates.isNotEmpty; - /// Trigger a device update with a new event and its corresponding hit test - /// result. + /// Perform a device update for one device according to the given new event. /// - /// The [updateWithEvent] indicates that an event has been observed, and is - /// called during the handler of the event. It is typically called by - /// [RendererBinding], and should be called with all events received, and let - /// [MouseTracker] filter which to react to. + /// The [updateWithEvent] is typically called by [RendererBinding] during the + /// handler of a pointer event. All pointer events should call this method, + /// and let [MouseTracker] filter which to react to. /// - /// The `getResult` is a function to return the hit test result at the - /// position of the event. It should not return a cached hit test - /// result, because the cache would not change during a tap sequence. - void updateWithEvent(PointerEvent event, ValueGetter getResult) { + /// The `hitTestResult` serves as an optional optimization, and is the hit + /// test result already performed by [RendererBinding] for other gestures. It + /// can be null, but when it's not null, it should be identical to the result + /// from directly calling `hitTestInView` given in the constructor (which + /// means that it should not use the cached result for [PointerMoveEvent]). + /// + /// The [updateWithEvent] is one of the two ways of updating mouse + /// states, the other one being [updateAllDevices]. + void updateWithEvent(PointerEvent event, HitTestResult? hitTestResult) { if (event.kind != PointerDeviceKind.mouse) { return; } if (event is PointerSignalEvent) { return; } - final HitTestResult result = event is PointerRemovedEvent ? HitTestResult() : getResult(); + final HitTestResult result; + if (event is PointerRemovedEvent) { + result = HitTestResult(); + } else { + final int viewId = event.viewId; + result = hitTestResult ?? _hitTestInView(event.position, viewId); + } final int device = event.device; final _MouseState? existingState = _mouseStates[device]; if (!_shouldMarkStateDirty(existingState, event)) { @@ -325,7 +345,7 @@ class MouseTracker extends ChangeNotifier { final PointerEvent lastEvent = targetState.replaceLatestEvent(event); final LinkedHashMap nextAnnotations = event is PointerRemovedEvent ? LinkedHashMap() : - _hitTestResultToAnnotations(result); + _hitTestInViewResultToAnnotations(result); final LinkedHashMap lastAnnotations = targetState.replaceAnnotations(nextAnnotations); _handleDeviceUpdate(_MouseTrackerUpdateDetails.byPointerEvent( @@ -338,21 +358,21 @@ class MouseTracker extends ChangeNotifier { }); } - /// Trigger a device update for all detected devices. + /// Perform a device update for all detected devices. /// /// The [updateAllDevices] is typically called during the post frame phase, - /// indicating a frame has passed and all objects have potentially moved. The - /// `hitTest` is a function that acquires the hit test result at a given - /// position, and must not be empty. - /// - /// For each connected device, the [updateAllDevices] will make a hit test on - /// the device's last seen position, and check if necessary changes need to be + /// indicating a frame has passed and all objects have potentially moved. For + /// each connected device, the [updateAllDevices] will make a hit test on the + /// device's last seen position, and check if necessary changes need to be /// made. - void updateAllDevices(MouseDetectorAnnotationFinder hitTest) { + /// + /// The [updateAllDevices] is one of the two ways of updating mouse + /// states, the other one being [updateWithEvent]. + void updateAllDevices() { _deviceUpdatePhase(() { for (final _MouseState dirtyState in _mouseStates.values) { final PointerEvent lastEvent = dirtyState.latestEvent; - final LinkedHashMap nextAnnotations = _findAnnotations(dirtyState, hitTest); + final LinkedHashMap nextAnnotations = _findAnnotations(dirtyState); final LinkedHashMap lastAnnotations = dirtyState.replaceAnnotations(nextAnnotations); _handleDeviceUpdate(_MouseTrackerUpdateDetails.byNewFrame( @@ -384,7 +404,7 @@ class MouseTracker extends ChangeNotifier { final LinkedHashMap nextAnnotations = details.nextAnnotations; // Order is important for mouse event callbacks. The - // `_hitTestResultToAnnotations` returns annotations in the visual order + // `_hitTestInViewResultToAnnotations` returns annotations in the visual order // from front to back, called the "hit-test order". The algorithm here is // explained in https://github.com/flutter/flutter/issues/41420 diff --git a/packages/flutter/lib/src/rendering/view.dart b/packages/flutter/lib/src/rendering/view.dart index a8cb7a2d3c..ac397ad76b 100644 --- a/packages/flutter/lib/src/rendering/view.dart +++ b/packages/flutter/lib/src/rendering/view.dart @@ -198,18 +198,6 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin return true; } - /// Determines the set of mouse tracker annotations at the given position. - /// - /// See also: - /// - /// * [Layer.findAllAnnotations], which is used by this method to find all - /// [AnnotatedRegionLayer]s annotated for mouse tracking. - HitTestResult hitTestMouseTrackers(Offset position) { - final BoxHitTestResult result = BoxHitTestResult(); - hitTest(result, position: position); - return result; - } - @override bool get isRepaintBoundary => true; diff --git a/packages/flutter/test/rendering/mouse_tracker_test_utils.dart b/packages/flutter/test/rendering/mouse_tracker_test_utils.dart index de07b3e2b2..1f9c5bfe05 100644 --- a/packages/flutter/test/rendering/mouse_tracker_test_utils.dart +++ b/packages/flutter/test/rendering/mouse_tracker_test_utils.dart @@ -48,7 +48,7 @@ class TestMouseTrackerFlutterBinding extends BindingBase final SchedulerPhase? lastPhase = _overridePhase; _overridePhase = SchedulerPhase.persistentCallbacks; addPostFrameCallback((_) { - mouseTracker.updateAllDevices(renderView.hitTestMouseTrackers); + mouseTracker.updateAllDevices(); }); _overridePhase = lastPhase; }