diff --git a/packages/flutter/lib/src/gestures/binding.dart b/packages/flutter/lib/src/gestures/binding.dart index 6005fcb2b0..d9de195a3d 100644 --- a/packages/flutter/lib/src/gestures/binding.dart +++ b/packages/flutter/lib/src/gestures/binding.dart @@ -270,7 +270,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H void handlePointerEvent(PointerEvent event) { assert(!locked); HitTestResult? hitTestResult; - if (event is PointerDownEvent || event is PointerSignalEvent) { + if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) { assert(!_hitTests.containsKey(event.pointer)); hitTestResult = HitTestResult(); hitTest(hitTestResult, event.position); @@ -298,7 +298,6 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H return true; }()); if (hitTestResult != null || - event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent) { assert(event.position != null); @@ -318,8 +317,8 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H /// null, the event is also sent to every [HitTestTarget] in the entries of the /// given [HitTestResult]. Any exceptions from the handlers are caught. /// - /// The `hitTestResult` argument may only be null for [PointerHoverEvent]s, - /// [PointerAddedEvent]s, or [PointerRemovedEvent]s. + /// The `hitTestResult` argument may only be null for [PointerAddedEvent]s or + /// [PointerRemovedEvent]s. @override // from HitTestDispatcher void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { assert(!locked); @@ -327,7 +326,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H // [PointerAddedEvent], or [PointerRemovedEvent]. These events are specially // routed here; other events will be routed through the `handleEvent` below. if (hitTestResult == null) { - assert(event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent); + assert(event is PointerAddedEvent || event is PointerRemovedEvent); try { pointerRouter.route(event); } catch (exception, stack) { diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index d98f34efe0..8b1a6fe3eb 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -259,7 +259,6 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture @override // from GestureBinding void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { if (hitTestResult != null || - event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent) { assert(event.position != null); diff --git a/packages/flutter/lib/src/rendering/mouse_tracking.dart b/packages/flutter/lib/src/rendering/mouse_tracking.dart index 03ed7e571d..9de866e424 100644 --- a/packages/flutter/lib/src/rendering/mouse_tracking.dart +++ b/packages/flutter/lib/src/rendering/mouse_tracking.dart @@ -53,7 +53,6 @@ class MouseTrackerAnnotation with Diagnosticable { /// All arguments are optional. The [cursor] must not be null. const MouseTrackerAnnotation({ this.onEnter, - this.onHover, this.onExit, this.cursor = MouseCursor.defer, }) : assert(cursor != null); @@ -72,16 +71,6 @@ class MouseTrackerAnnotation with Diagnosticable { /// * [MouseRegion.onEnter], which uses this callback. final PointerEnterEventListener? onEnter; - /// Triggered when a mouse pointer has moved onto or within the region without - /// buttons pressed. - /// - /// This callback is not triggered by the movement of an annotation. - /// - /// See also: - /// - /// * [MouseRegion.onHover], which uses this callback. - final PointerHoverEventListener? onHover; - /// Triggered when a mouse pointer, with or without buttons pressed, has /// exited the region. /// @@ -482,8 +471,6 @@ abstract class BaseMouseTracker extends ChangeNotifier { mixin _MouseTrackerEventMixin on BaseMouseTracker { // Handles device update and dispatches mouse event callbacks. static void _handleDeviceUpdateMouseEvents(MouseTrackerUpdateDetails details) { - final PointerEvent? previousEvent = details.previousEvent; - final PointerEvent? triggeringEvent = details.triggeringEvent; final PointerEvent latestEvent = details.latestEvent; final LinkedHashMap lastAnnotations = details.lastAnnotations; @@ -514,24 +501,6 @@ mixin _MouseTrackerEventMixin on BaseMouseTracker { if (annotation.onEnter != null) annotation.onEnter!(baseEnterEvent.transformed(nextAnnotations[annotation])); } - - // Send hover events to annotations that are in next, in reverse visual - // order. The reverse visual order is chosen only because of the simplicity - // by keeping the hover events aligned with enter events. - if (triggeringEvent is PointerHoverEvent) { - final Offset? hoverPositionBeforeUpdate = previousEvent is PointerHoverEvent ? previousEvent.position : null; - final bool pointerHasMoved = hoverPositionBeforeUpdate == null || hoverPositionBeforeUpdate != triggeringEvent.position; - // If the hover event follows a non-hover event, or has moved since the - // last hover, then trigger the hover callback on all annotations. - // Otherwise, trigger the hover callback only on annotations that it - // newly enters. - final Iterable hoveringAnnotations = pointerHasMoved ? nextAnnotations.keys.toList().reversed : enteringAnnotations; - for (final MouseTrackerAnnotation annotation in hoveringAnnotations) { - if (annotation.onHover != null) { - annotation.onHover!(triggeringEvent.transformed(nextAnnotations[annotation])); - } - } - } } @protected diff --git a/packages/flutter/lib/src/rendering/platform_view.dart b/packages/flutter/lib/src/rendering/platform_view.dart index 667df604b1..0b69147679 100644 --- a/packages/flutter/lib/src/rendering/platform_view.dart +++ b/packages/flutter/lib/src/rendering/platform_view.dart @@ -723,13 +723,6 @@ mixin _PlatformViewGestureMixin on RenderBox implements MouseTrackerAnnotation { @override PointerEnterEventListener? get onEnter => null; - @override - PointerHoverEventListener get onHover => _handleHover; - void _handleHover(PointerHoverEvent event) { - if (_handlePointerEvent != null) - _handlePointerEvent!(event); - } - @override PointerExitEventListener? get onExit => null; @@ -741,6 +734,9 @@ mixin _PlatformViewGestureMixin on RenderBox implements MouseTrackerAnnotation { if (event is PointerDownEvent) { _gestureRecognizer!.addPointer(event); } + if (event is PointerHoverEvent) { + _handlePointerEvent?.call(event); + } } @override diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index 2d86e0b3b1..ddc2fe7d5a 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -2662,10 +2662,10 @@ typedef PointerSignalEventListener = void Function(PointerSignalEvent event); /// Calls callbacks in response to common pointer events. /// /// It responds to events that can construct gestures, such as when the -/// pointer is pressed, moved, then released or canceled. +/// pointer is pointer is pressed and moved, and then released or canceled. /// /// It does not respond to events that are exclusive to mouse, such as when the -/// mouse enters, exits or hovers a region without pressing any buttons. For +/// mouse enters and exits a region without pressing any buttons. For /// these events, use [RenderMouseRegion]. /// /// If it has a child, defers to the child for sizing behavior. @@ -2679,6 +2679,7 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { this.onPointerDown, this.onPointerMove, this.onPointerUp, + this.onPointerHover, this.onPointerCancel, this.onPointerSignal, HitTestBehavior behavior = HitTestBehavior.deferToChild, @@ -2697,6 +2698,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { /// contact with the screen. PointerUpEventListener? onPointerUp; + /// Called when a pointer that has not an [onPointerDown] changes position. + PointerHoverEventListener? onPointerHover; + /// Called when the input from a pointer that triggered an [onPointerDown] is /// no longer directed towards this receiver. PointerCancelEventListener? onPointerCancel; @@ -2712,16 +2716,18 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { @override void handleEvent(PointerEvent event, HitTestEntry entry) { assert(debugHandleEvent(event, entry)); - if (onPointerDown != null && event is PointerDownEvent) - return onPointerDown!(event); - if (onPointerMove != null && event is PointerMoveEvent) - return onPointerMove!(event); - if (onPointerUp != null && event is PointerUpEvent) - return onPointerUp!(event); - if (onPointerCancel != null && event is PointerCancelEvent) - return onPointerCancel!(event); - if (onPointerSignal != null && event is PointerSignalEvent) - return onPointerSignal!(event); + if (event is PointerDownEvent) + return onPointerDown?.call(event); + if (event is PointerMoveEvent) + return onPointerMove?.call(event); + if (event is PointerUpEvent) + return onPointerUp?.call(event); + if (event is PointerHoverEvent) + return onPointerHover?.call(event); + if (event is PointerCancelEvent) + return onPointerCancel?.call(event); + if (event is PointerSignalEvent) + return onPointerSignal?.call(event); } @override @@ -2733,6 +2739,7 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior { 'down': onPointerDown, 'move': onPointerMove, 'up': onPointerUp, + 'hover': onPointerHover, 'cancel': onPointerCancel, 'signal': onPointerSignal, }, @@ -2821,7 +2828,10 @@ class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation @override PointerEnterEventListener? onEnter; - @override + /// Triggered when a pointer has moved onto or within the region without + /// buttons pressed. + /// + /// This callback is not triggered by the movement of the object. PointerHoverEventListener? onHover; @override diff --git a/packages/flutter/lib/src/services/platform_views.dart b/packages/flutter/lib/src/services/platform_views.dart index 9238ca3650..2019cedf26 100644 --- a/packages/flutter/lib/src/services/platform_views.dart +++ b/packages/flutter/lib/src/services/platform_views.dart @@ -848,6 +848,10 @@ abstract class AndroidViewController extends PlatformViewController { /// for description of the parameters. @override Future dispatchPointerEvent(PointerEvent event) async { + if (event is PointerHoverEvent) { + return; + } + if (event is PointerDownEvent) { _motionEventConverter.handlePointerDownEvent(event); } diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 2958280acb..029c8c1ae8 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -5812,27 +5812,8 @@ class Listener extends StatelessWidget { Key? key, this.onPointerDown, this.onPointerMove, - // We have to ignore the lint rule here in order to use deprecated - // parameters and keep backward compatibility. - // TODO(tongmu): After it goes stable, remove these 3 parameters from Listener - // and Listener should no longer need an intermediate class _PointerListener. - // https://github.com/flutter/flutter/issues/36085 - @Deprecated( - 'Use MouseRegion.onEnter instead. See MouseRegion.opaque for behavioral difference. ' - 'This feature was deprecated after v1.10.14.' - ) - this.onPointerEnter, - @Deprecated( - 'Use MouseRegion.onExit instead. See MouseRegion.opaque for behavioral difference. ' - 'This feature was deprecated after v1.10.14.' - ) - this.onPointerExit, - @Deprecated( - 'Use MouseRegion.onHover instead. See MouseRegion.opaque for behavioral difference. ' - 'This feature was deprecated after v1.10.14.' - ) - this.onPointerHover, this.onPointerUp, + this.onPointerHover, this.onPointerCancel, this.onPointerSignal, this.behavior = HitTestBehavior.deferToChild, @@ -5849,15 +5830,9 @@ class Listener extends StatelessWidget { /// Called when a pointer that triggered an [onPointerDown] changes position. final PointerMoveEventListener? onPointerMove; - /// Called when a pointer enters the region for this widget. - /// - /// This is only fired for pointers which report their location when not down - /// (e.g. mouse pointers, but not most touch pointers). - /// - /// If this is a mouse pointer, this will fire when the mouse pointer enters - /// the region defined by this widget, or when the widget appears under the - /// pointer. - final PointerEnterEventListener? onPointerEnter; + /// Called when a pointer that triggered an [onPointerDown] is no longer in + /// contact with the screen. + final PointerUpEventListener? onPointerUp; /// Called when a pointer that has not triggered an [onPointerDown] changes /// position. @@ -5866,20 +5841,6 @@ class Listener extends StatelessWidget { /// (e.g. mouse pointers, but not most touch pointers). final PointerHoverEventListener? onPointerHover; - /// Called when a pointer leaves the region for this widget. - /// - /// This is only fired for pointers which report their location when not down - /// (e.g. mouse pointers, but not most touch pointers). - /// - /// If this is a mouse pointer, this will fire when the mouse pointer leaves - /// the region defined by this widget, or when the widget disappears from - /// under the pointer. - final PointerExitEventListener? onPointerExit; - - /// Called when a pointer that triggered an [onPointerDown] is no longer in - /// contact with the screen. - final PointerUpEventListener? onPointerUp; - /// Called when the input from a pointer that triggered an [onPointerDown] is /// no longer directed towards this receiver. final PointerCancelEventListener? onPointerCancel; @@ -5904,28 +5865,18 @@ class Listener extends StatelessWidget { @override Widget build(BuildContext context) { - Widget? result = _child; - if (onPointerEnter != null || - onPointerExit != null || - onPointerHover != null) { - result = MouseRegion( - onEnter: onPointerEnter, - onExit: onPointerExit, - onHover: onPointerHover, - opaque: false, - child: result, - ); - } - result = _PointerListener( + // TODO(dkwingsmt): Remove the extra wrapper, and make `Listener` a + // StatelessWidget. https://github.com/flutter/flutter/issues/65586 + return _PointerListener( onPointerDown: onPointerDown, onPointerUp: onPointerUp, onPointerMove: onPointerMove, + onPointerHover: onPointerHover, onPointerCancel: onPointerCancel, onPointerSignal: onPointerSignal, behavior: behavior, - child: result, + child: _child, ); - return result; } } @@ -5935,6 +5886,7 @@ class _PointerListener extends SingleChildRenderObjectWidget { this.onPointerDown, this.onPointerMove, this.onPointerUp, + this.onPointerHover, this.onPointerCancel, this.onPointerSignal, this.behavior = HitTestBehavior.deferToChild, @@ -5945,6 +5897,7 @@ class _PointerListener extends SingleChildRenderObjectWidget { final PointerDownEventListener? onPointerDown; final PointerMoveEventListener? onPointerMove; final PointerUpEventListener? onPointerUp; + final PointerHoverEventListener? onPointerHover; final PointerCancelEventListener? onPointerCancel; final PointerSignalEventListener? onPointerSignal; final HitTestBehavior behavior; @@ -5955,6 +5908,7 @@ class _PointerListener extends SingleChildRenderObjectWidget { onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, + onPointerHover: onPointerHover, onPointerCancel: onPointerCancel, onPointerSignal: onPointerSignal, behavior: behavior, @@ -5967,6 +5921,7 @@ class _PointerListener extends SingleChildRenderObjectWidget { ..onPointerDown = onPointerDown ..onPointerMove = onPointerMove ..onPointerUp = onPointerUp + ..onPointerHover = onPointerHover ..onPointerCancel = onPointerCancel ..onPointerSignal = onPointerSignal ..behavior = behavior; @@ -5987,11 +5942,15 @@ class _PointerListener extends SingleChildRenderObjectWidget { } } -/// A widget that tracks the movement of mice, even when no button is pressed. +/// A widget that tracks the movement of mice. /// -/// It does not listen to events that can construct gestures, such as when the -/// pointer is pressed, moved, then released or canceled. For these events, -/// use [Listener], or more preferably, [GestureDetector]. +/// [MouseRegion] is used +/// when it is needed to compare the list of objects that a mouse pointer is +/// hovering over betweeen this frame and the last frame. This means entering +/// events, exiting events, and mouse cursors. +/// +/// To listen to general pointer events, use [Listener], or more preferably, +/// [GestureDetector]. /// /// ## Layout behavior /// @@ -6110,13 +6069,23 @@ class MouseRegion extends StatefulWidget { /// internally implemented. final PointerEnterEventListener? onEnter; - /// Triggered when a mouse pointer has moved onto or within the widget without + /// Triggered when a pointer moves into a position within this widget without /// buttons pressed. /// - /// This callback is not triggered by the movement of an annotation. + /// Usually this is only fired for pointers which report their location when + /// not down (e.g. mouse pointers). Certain devices also fire this event on + /// single taps in accessibility mode. + /// + /// This callback is not triggered by the movement of the widget. /// /// The time that this callback is triggered is during the callback of a /// pointer event, which is always between frames. + /// + /// See also: + /// + /// * [Listener.onPointerHover], which does the same job. Prefer using + /// [Listener.onPointerHover], since hover events are similar to other regular + /// events. final PointerHoverEventListener? onHover; /// Triggered when a mouse pointer has exited this widget when the widget is diff --git a/packages/flutter/test/gestures/gesture_binding_test.dart b/packages/flutter/test/gestures/gesture_binding_test.dart index 0faa04e17a..816ae076ec 100644 --- a/packages/flutter/test/gestures/gesture_binding_test.dart +++ b/packages/flutter/test/gestures/gesture_binding_test.dart @@ -105,7 +105,10 @@ void main() { _binding.callback = events.add; ui.window.onPointerDataPacket(packet); - expect(events.length, 0); + expect(events.length, 3); + expect(events[0], isA()); + expect(events[1], isA()); + expect(events[2], isA()); expect(pointerRouterEvents.length, 6, reason: 'pointerRouterEvents contains: $pointerRouterEvents'); expect(pointerRouterEvents[0], isA()); diff --git a/packages/flutter/test/rendering/mouse_tracking_test.dart b/packages/flutter/test/rendering/mouse_tracking_test.dart index e80b1521c6..2a597b40fb 100644 --- a/packages/flutter/test/rendering/mouse_tracking_test.dart +++ b/packages/flutter/test/rendering/mouse_tracking_test.dart @@ -546,7 +546,7 @@ void main() { ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ _pointerData(PointerChange.hover, const Offset(0.0, 10.0)), ])); - expect(logs, ['enterA', 'enterB', 'hoverA', 'hoverB']); + expect(logs, ['enterA', 'enterB', 'hoverB', 'hoverA']); logs.clear(); // Moves out of A within one frame. diff --git a/packages/flutter/test/rendering/mouse_tracking_test_utils.dart b/packages/flutter/test/rendering/mouse_tracking_test_utils.dart index c122120926..21f294623f 100644 --- a/packages/flutter/test/rendering/mouse_tracking_test_utils.dart +++ b/packages/flutter/test/rendering/mouse_tracking_test_utils.dart @@ -77,7 +77,6 @@ class TestAnnotationTarget with Diagnosticable implements MouseTrackerAnnotation @override final PointerEnterEventListener? onEnter; - @override final PointerHoverEventListener? onHover; @override diff --git a/packages/flutter/test/rendering/platform_view_test.dart b/packages/flutter/test/rendering/platform_view_test.dart index 4f6ce2b658..4469531443 100644 --- a/packages/flutter/test/rendering/platform_view_test.dart +++ b/packages/flutter/test/rendering/platform_view_test.dart @@ -87,6 +87,19 @@ void main() { expect(fakePlatformViewController.dispatchedPointerEvents, isNotEmpty); }); + test('touch hover events are dispatched via PlatformViewController.dispatchPointerEvent', () { + layout(platformViewRenderBox); + pumpFrame(phase: EnginePhase.flushSemantics); + + ui.window.onPointerDataPacket(ui.PointerDataPacket(data: [ + _pointerData(ui.PointerChange.add, const Offset(0, 0)), + _pointerData(ui.PointerChange.hover, const Offset(10, 10)), + _pointerData(ui.PointerChange.remove, const Offset(10, 10)), + ])); + + expect(fakePlatformViewController.dispatchedPointerEvents, isNotEmpty); + }); + }, skip: isBrowser); // TODO(yjbanov): fails on Web with obscured stack trace: https://github.com/flutter/flutter/issues/42770 } diff --git a/packages/flutter/test/widgets/listener_deprecated_test.dart b/packages/flutter/test/widgets/listener_deprecated_test.dart deleted file mode 100644 index c53ed2a582..0000000000 --- a/packages/flutter/test/widgets/listener_deprecated_test.dart +++ /dev/null @@ -1,586 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// @dart = 2.8 - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter/gestures.dart'; - -// The tests in this file are moved from listener_test.dart, which tests several -// deprecated APIs. The file should be removed once these parameters are. - -class HoverClient extends StatefulWidget { - const HoverClient({Key key, this.onHover, this.child}) : super(key: key); - - final ValueChanged onHover; - final Widget child; - - @override - HoverClientState createState() => HoverClientState(); -} - -class HoverClientState extends State { - static int numEntries = 0; - static int numExits = 0; - - void _onExit(PointerExitEvent details) { - numExits++; - if (widget.onHover != null) { - widget.onHover(false); - } - } - - void _onEnter(PointerEnterEvent details) { - numEntries++; - if (widget.onHover != null) { - widget.onHover(true); - } - } - - @override - Widget build(BuildContext context) { - return Listener( - onPointerEnter: _onEnter, - onPointerExit: _onExit, - child: widget.child, - ); - } -} - -class HoverFeedback extends StatefulWidget { - const HoverFeedback({Key key}) : super(key: key); - - @override - _HoverFeedbackState createState() => _HoverFeedbackState(); -} - -class _HoverFeedbackState extends State { - bool _hovering = false; - - @override - Widget build(BuildContext context) { - return Directionality( - textDirection: TextDirection.ltr, - child: HoverClient( - onHover: (bool hovering) => setState(() => _hovering = hovering), - child: Text(_hovering ? 'HOVERING' : 'not hovering'), - ), - ); - } -} - -void main() { - group('Listener hover detection', () { - // TODO(tongmu): Remover this group of test after the deprecated callbacks - // onPointer{Enter,Hover,Exit} are removed. They were kept for compatibility, - // and the tests have been copied to mouse_region_test. - // https://github.com/flutter/flutter/issues/36085 - setUp(() { - HoverClientState.numExits = 0; - HoverClientState.numEntries = 0; - }); - - testWidgets('detects pointer enter', (WidgetTester tester) async { - PointerEnterEvent enter; - PointerHoverEvent move; - PointerExitEvent exit; - await tester.pumpWidget( - Center( - child: Listener( - child: Container( - color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), - width: 100.0, - height: 100.0, - ), - onPointerEnter: (PointerEnterEvent details) => enter = details, - onPointerHover: (PointerHoverEvent details) => move = details, - onPointerExit: (PointerExitEvent details) => exit = details, - ), - ), - ); - final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: Offset.zero); - addTearDown(gesture.removePointer); - await tester.pump(); - await gesture.moveTo(const Offset(400.0, 300.0)); - expect(move, isNotNull); - expect(move.position, equals(const Offset(400.0, 300.0))); - expect(enter, isNotNull); - expect(enter.position, equals(const Offset(400.0, 300.0))); - expect(exit, isNull); - }); - testWidgets('detects pointer exiting', (WidgetTester tester) async { - PointerEnterEvent enter; - PointerHoverEvent move; - PointerExitEvent exit; - await tester.pumpWidget( - Center( - child: Listener( - child: const SizedBox( - width: 100.0, - height: 100.0, - ), - onPointerEnter: (PointerEnterEvent details) => enter = details, - onPointerHover: (PointerHoverEvent details) => move = details, - onPointerExit: (PointerExitEvent details) => exit = details, - ), - ), - ); - final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: const Offset(400.0, 300.0)); - addTearDown(gesture.removePointer); - await tester.pump(); - move = null; - enter = null; - await gesture.moveTo(const Offset(1.0, 1.0)); - await tester.pump(); - expect(move, isNull); - expect(enter, isNull); - expect(exit, isNotNull); - expect(exit.position, equals(const Offset(1.0, 1.0))); - }); - testWidgets('does not detect pointer exit when widget disappears', (WidgetTester tester) async { - PointerEnterEvent enter; - PointerHoverEvent move; - PointerExitEvent exit; - await tester.pumpWidget( - Center( - child: Listener( - child: const SizedBox( - width: 100.0, - height: 100.0, - ), - onPointerEnter: (PointerEnterEvent details) => enter = details, - onPointerHover: (PointerHoverEvent details) => move = details, - onPointerExit: (PointerExitEvent details) => exit = details, - ), - ), - ); - final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: const Offset(400.0, 300.0)); - addTearDown(gesture.removePointer); - await tester.pump(); - expect(move, isNull); - expect(enter, isNotNull); - expect(enter.position, equals(const Offset(400.0, 300.0))); - expect(exit, isNull); - await tester.pumpWidget(const Center( - child: SizedBox( - width: 100.0, - height: 100.0, - ), - )); - expect(exit, isNull); - }); - testWidgets('Hover works with nested listeners', (WidgetTester tester) async { - final UniqueKey key1 = UniqueKey(); - final UniqueKey key2 = UniqueKey(); - final List enter1 = []; - final List move1 = []; - final List exit1 = []; - final List enter2 = []; - final List move2 = []; - final List exit2 = []; - 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); - addTearDown(gesture.removePointer); - await gesture.addPointer(location: const Offset(400.0, 0.0)); - await tester.pump(); - await tester.pumpWidget( - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Listener( - onPointerEnter: (PointerEnterEvent details) => enter1.add(details), - onPointerHover: (PointerHoverEvent details) => move1.add(details), - onPointerExit: (PointerExitEvent details) => exit1.add(details), - key: key1, - child: Container( - width: 200, - height: 200, - padding: const EdgeInsets.all(50.0), - child: Listener( - key: key2, - onPointerEnter: (PointerEnterEvent details) => enter2.add(details), - onPointerHover: (PointerHoverEvent details) => move2.add(details), - onPointerExit: (PointerExitEvent details) => exit2.add(details), - child: Container(), - ), - ), - ), - ], - ), - ); - Offset center = tester.getCenter(find.byKey(key2)); - await gesture.moveTo(center); - await tester.pump(); - expect(move2, isNotEmpty); - expect(enter2, isNotEmpty); - expect(exit2, isEmpty); - expect(move1, isNotEmpty); - expect(move1.last.position, equals(center)); - expect(enter1, isNotEmpty); - expect(enter1.last.position, equals(center)); - expect(exit1, isEmpty); - clearLists(); - - // Now make sure that exiting the child only triggers the child exit, not - // the parent too. - center = center - const Offset(75.0, 0.0); - await gesture.moveTo(center); - await tester.pumpAndSettle(); - expect(move2, isEmpty); - expect(enter2, isEmpty); - expect(exit2, isNotEmpty); - expect(move1, isNotEmpty); - expect(move1.last.position, equals(center)); - expect(enter1, isEmpty); - expect(exit1, isEmpty); - clearLists(); - }); - testWidgets('Hover transfers between two listeners', (WidgetTester tester) async { - final UniqueKey key1 = UniqueKey(); - final UniqueKey key2 = UniqueKey(); - final List enter1 = []; - final List move1 = []; - final List exit1 = []; - final List enter2 = []; - final List move2 = []; - final List exit2 = []; - 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); - addTearDown(gesture.removePointer); - await gesture.moveTo(const Offset(400.0, 0.0)); - await tester.pump(); - await tester.pumpWidget( - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Listener( - key: key1, - child: const SizedBox( - 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: const SizedBox( - 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 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); - 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); - 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))); - clearLists(); - await tester.pumpWidget(Container()); - expect(move1, isEmpty); - expect(enter1, isEmpty); - expect(exit1, isEmpty); - expect(move2, isEmpty); - expect(enter2, isEmpty); - expect(exit2, isEmpty); - }); - - testWidgets('needsCompositing set when parent class needsCompositing is set', (WidgetTester tester) async { - await tester.pumpWidget( - Listener( - onPointerEnter: (PointerEnterEvent _) {}, - child: const Opacity(opacity: 0.5, child: Placeholder()), - ), - ); - - RenderPointerListener listener = tester.renderObject(find.byType(Listener).first); - expect(listener.needsCompositing, isTrue); - - await tester.pumpWidget( - Listener( - onPointerEnter: (PointerEnterEvent _) {}, - child: const Placeholder(), - ), - ); - - listener = tester.renderObject(find.byType(Listener).first); - expect(listener.needsCompositing, isFalse); - }); - - testWidgets('works with transform', (WidgetTester tester) async { - // Regression test for https://github.com/flutter/flutter/issues/31986. - final Key key = UniqueKey(); - const double scaleFactor = 2.0; - const double localWidth = 150.0; - const double localHeight = 100.0; - final List events = []; - - await tester.pumpWidget( - MaterialApp( - home: Center( - child: Transform.scale( - scale: scaleFactor, - child: Listener( - onPointerEnter: (PointerEnterEvent event) { - events.add(event); - }, - onPointerHover: (PointerHoverEvent event) { - events.add(event); - }, - onPointerExit: (PointerExitEvent event) { - events.add(event); - }, - child: Container( - key: key, - color: Colors.blue, - height: localHeight, - width: localWidth, - child: const Text('Hi'), - ), - ), - ), - ), - ), - ); - - final Offset topLeft = tester.getTopLeft(find.byKey(key)); - final Offset topRight = tester.getTopRight(find.byKey(key)); - final Offset bottomLeft = tester.getBottomLeft(find.byKey(key)); - expect(topRight.dx - topLeft.dx, scaleFactor * localWidth); - expect(bottomLeft.dy - topLeft.dy, scaleFactor * localHeight); - - final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: topLeft - const Offset(1, 1)); - addTearDown(gesture.removePointer); - await tester.pump(); - expect(events, isEmpty); - - await gesture.moveTo(topLeft + const Offset(1, 1)); - await tester.pump(); - expect(events, hasLength(2)); - expect(events.first, isA()); - expect(events.last, isA()); - events.clear(); - - await gesture.moveTo(bottomLeft + const Offset(1, -1)); - await tester.pump(); - expect(events.single, isA()); - expect(events.single.delta, const Offset(0.0, scaleFactor * localHeight - 2)); - events.clear(); - - await gesture.moveTo(bottomLeft + const Offset(1, 1)); - await tester.pump(); - expect(events.single, isA()); - events.clear(); - }); - - testWidgets('needsCompositing is always false', (WidgetTester tester) async { - // Pretend that we have a mouse connected. - final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: Offset.zero); - addTearDown(gesture.removePointer); - - await tester.pumpWidget( - Transform.scale( - scale: 2.0, - child: Listener( - onPointerDown: (PointerDownEvent _) { }, - ), - ), - ); - final RenderPointerListener listener = tester.renderObject(find.byType(Listener)); - expect(listener.needsCompositing, isFalse); - // No TransformLayer for `Transform.scale` is added because composting is - // not required and therefore the transform is executed on the canvas - // directly. (One TransformLayer is always present for the root - // transform.) - expect(tester.layers.whereType(), hasLength(1)); - - await tester.pumpWidget( - Transform.scale( - scale: 2.0, - child: Listener( - onPointerDown: (PointerDownEvent _) { }, - onPointerHover: (PointerHoverEvent _) { }, - ), - ), - ); - expect(listener.needsCompositing, isFalse); - // If compositing was required, a dedicated TransformLayer for - // `Transform.scale` would be added. - expect(tester.layers.whereType(), hasLength(1)); - }); - - testWidgets("Callbacks aren't called during build", (WidgetTester tester) async { - final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: Offset.zero); - addTearDown(gesture.removePointer); - - await tester.pumpWidget( - const Center(child: HoverFeedback()), - ); - - await gesture.moveTo(tester.getCenter(find.byType(Text))); - await tester.pumpAndSettle(); - expect(HoverClientState.numEntries, equals(1)); - expect(HoverClientState.numExits, equals(0)); - expect(find.text('HOVERING'), findsOneWidget); - - await tester.pumpWidget( - Container(), - ); - await tester.pump(); - expect(HoverClientState.numEntries, equals(1)); - // Unmounting a MouseRegion doesn't trigger onExit - expect(HoverClientState.numExits, equals(0)); - - await tester.pumpWidget( - const Center(child: HoverFeedback()), - ); - await tester.pump(); - expect(HoverClientState.numEntries, equals(2)); - expect(HoverClientState.numExits, equals(0)); - }); - - testWidgets("Listener activate/deactivate don't duplicate annotations", (WidgetTester tester) async { - final GlobalKey feedbackKey = GlobalKey(); - final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: Offset.zero); - addTearDown(gesture.removePointer); - - await tester.pumpWidget( - Center(child: HoverFeedback(key: feedbackKey)), - ); - - await gesture.moveTo(tester.getCenter(find.byType(Text))); - await tester.pumpAndSettle(); - expect(HoverClientState.numEntries, equals(1)); - expect(HoverClientState.numExits, equals(0)); - expect(find.text('HOVERING'), findsOneWidget); - - await tester.pumpWidget( - Center(child: Container(child: HoverFeedback(key: feedbackKey))), - ); - await tester.pump(); - expect(HoverClientState.numEntries, equals(1)); - expect(HoverClientState.numExits, equals(0)); - await tester.pumpWidget( - Container(), - ); - await tester.pump(); - expect(HoverClientState.numEntries, equals(1)); - // Unmounting a MouseRegion doesn't trigger onExit - expect(HoverClientState.numExits, equals(0)); - }); - - testWidgets('Exit event when unplugging mouse should have a position', (WidgetTester tester) async { - final List enter = []; - final List hover = []; - final List exit = []; - - await tester.pumpWidget( - Center( - child: Listener( - onPointerEnter: (PointerEnterEvent e) => enter.add(e), - onPointerHover: (PointerHoverEvent e) => hover.add(e), - onPointerExit: (PointerExitEvent e) => exit.add(e), - child: const SizedBox( - height: 100.0, - width: 100.0, - ), - ), - ), - ); - - // Plug-in a mouse and move it to the center of the container. - TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); - await gesture.addPointer(location: Offset.zero); - addTearDown(() => gesture?.removePointer()); - await tester.pumpAndSettle(); - await gesture.moveTo(tester.getCenter(find.byType(SizedBox))); - - expect(enter.length, 1); - expect(enter.single.position, const Offset(400.0, 300.0)); - expect(hover.length, 1); - expect(hover.single.position, const Offset(400.0, 300.0)); - expect(exit.length, 0); - - enter.clear(); - hover.clear(); - exit.clear(); - - // Unplug the mouse. - await gesture.removePointer(); - gesture = null; - await tester.pumpAndSettle(); - - expect(enter.length, 0); - expect(hover.length, 0); - expect(exit.length, 1); - expect(exit.single.position, const Offset(400.0, 300.0)); - expect(exit.single.delta, Offset.zero); - }); - }); -} diff --git a/packages/flutter/test/widgets/listener_test.dart b/packages/flutter/test/widgets/listener_test.dart index a54db86bd6..62bd742c24 100644 --- a/packages/flutter/test/widgets/listener_test.dart +++ b/packages/flutter/test/widgets/listener_test.dart @@ -49,6 +49,34 @@ void main() { ])); }); + testWidgets('Detects hover events from touch devices', (WidgetTester tester) async { + final List log = []; + + await tester.pumpWidget( + Center( + child: SizedBox( + width: 300, + height: 300, + child: Listener( + onPointerHover: (_) { + log.add('bottom'); + }, + child: const Text('X', textDirection: TextDirection.ltr), + ), + ), + ), + ); + + final TestGesture gesture = await tester.createGesture(); + await gesture.addPointer(); + addTearDown(gesture.removePointer); + await gesture.moveTo(tester.getCenter(find.byType(Listener))); + + expect(log, equals([ + 'bottom', + ])); + }); + group('transformed events', () { testWidgets('simple offset for touch/signal', (WidgetTester tester) async { final List events = []; diff --git a/packages/flutter/test/widgets/mouse_region_test.dart b/packages/flutter/test/widgets/mouse_region_test.dart index 783d926bc7..e8ea0cc459 100644 --- a/packages/flutter/test/widgets/mouse_region_test.dart +++ b/packages/flutter/test/widgets/mouse_region_test.dart @@ -125,7 +125,8 @@ void main() { PointerExitEvent exit; await tester.pumpWidget(Center( child: MouseRegion( - child: const SizedBox( + child: Container( + color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), width: 100.0, height: 100.0, ), @@ -372,6 +373,37 @@ void main() { expect(exit.localPosition, equals(const Offset(50, 50))); }); + testWidgets('detects hover from touch devices', (WidgetTester tester) async { + PointerEnterEvent enter; + PointerHoverEvent move; + PointerExitEvent exit; + await tester.pumpWidget(Center( + child: MouseRegion( + child: Container( + color: const Color.fromARGB(0xff, 0xff, 0x00, 0x00), + width: 100.0, + height: 100.0, + ), + onEnter: (PointerEnterEvent details) => enter = details, + onHover: (PointerHoverEvent details) => move = details, + onExit: (PointerExitEvent details) => exit = details, + ), + )); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.touch); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + await tester.pump(); + move = null; + enter = null; + exit = null; + await gesture.moveTo(const Offset(400.0, 300.0)); + expect(move, isNotNull); + expect(move.position, equals(const Offset(400.0, 300.0))); + expect(move.localPosition, equals(const Offset(50.0, 50.0))); + expect(enter, isNull); + expect(exit, isNull); + }); + testWidgets('Hover works with nested listeners', (WidgetTester tester) async { final UniqueKey key1 = UniqueKey(); final UniqueKey key2 = UniqueKey(); @@ -589,8 +621,12 @@ void main() { } await tester.pumpWidget(hoverableContainer( - onEnter: (PointerEnterEvent details) { logs.add('enter1'); }, - onHover: (PointerHoverEvent details) { logs.add('hover1'); }, + onEnter: (PointerEnterEvent details) { + logs.add('enter1'); + }, + onHover: (PointerHoverEvent details) { + logs.add('hover1'); + }, onExit: (PointerExitEvent details) { logs.add('exit1'); }, )); @@ -1178,31 +1214,31 @@ void main() { // Move to the overlapping area. await gesture.moveTo(const Offset(75, 75)); await tester.pumpAndSettle(); - expect(logs, ['enterA', 'enterB', 'enterC', 'hoverA', 'hoverB', 'hoverC']); + expect(logs, ['enterA', 'enterB', 'enterC', 'hoverC', 'hoverB', 'hoverA']); logs.clear(); // Move to the B only area. await gesture.moveTo(const Offset(25, 75)); await tester.pumpAndSettle(); - expect(logs, ['exitC', 'hoverA', 'hoverB']); + expect(logs, ['exitC', 'hoverB', 'hoverA']); logs.clear(); // Move back to the overlapping area. await gesture.moveTo(const Offset(75, 75)); await tester.pumpAndSettle(); - expect(logs, ['enterC', 'hoverA', 'hoverB', 'hoverC']); + expect(logs, ['enterC', 'hoverC', 'hoverB', 'hoverA']); logs.clear(); // Move to the C only area. await gesture.moveTo(const Offset(125, 75)); await tester.pumpAndSettle(); - expect(logs, ['exitB', 'hoverA', 'hoverC']); + expect(logs, ['exitB', 'hoverC', 'hoverA']); logs.clear(); // Move back to the overlapping area. await gesture.moveTo(const Offset(75, 75)); await tester.pumpAndSettle(); - expect(logs, ['enterB', 'hoverA', 'hoverB', 'hoverC']); + expect(logs, ['enterB', 'hoverC', 'hoverB', 'hoverA']); logs.clear(); // Move out. @@ -1226,31 +1262,31 @@ void main() { // Move to the overlapping area. await gesture.moveTo(const Offset(75, 75)); await tester.pumpAndSettle(); - expect(logs, ['enterA', 'enterC', 'hoverA', 'hoverC']); + expect(logs, ['enterA', 'enterC', 'hoverC', 'hoverA']); logs.clear(); // Move to the B only area. await gesture.moveTo(const Offset(25, 75)); await tester.pumpAndSettle(); - expect(logs, ['exitC', 'enterB', 'hoverA', 'hoverB']); + expect(logs, ['exitC', 'enterB', 'hoverB', 'hoverA']); logs.clear(); // Move back to the overlapping area. await gesture.moveTo(const Offset(75, 75)); await tester.pumpAndSettle(); - expect(logs, ['exitB', 'enterC', 'hoverA', 'hoverC']); + expect(logs, ['exitB', 'enterC', 'hoverC', 'hoverA']); logs.clear(); // Move to the C only area. await gesture.moveTo(const Offset(125, 75)); await tester.pumpAndSettle(); - expect(logs, ['hoverA', 'hoverC']); + expect(logs, ['hoverC', 'hoverA']); logs.clear(); // Move back to the overlapping area. await gesture.moveTo(const Offset(75, 75)); await tester.pumpAndSettle(); - expect(logs, ['hoverA', 'hoverC']); + expect(logs, ['hoverC', 'hoverA']); logs.clear(); // Move out. @@ -1274,7 +1310,7 @@ void main() { // Move to the overlapping area. await gesture.moveTo(const Offset(75, 75)); await tester.pumpAndSettle(); - expect(logs, ['enterA', 'enterC', 'hoverA', 'hoverC']); + expect(logs, ['enterA', 'enterC', 'hoverC', 'hoverA']); logs.clear(); // Move out. diff --git a/packages/flutter_test/lib/src/test_pointer.dart b/packages/flutter_test/lib/src/test_pointer.dart index 3c236def90..77bd8bdd17 100644 --- a/packages/flutter_test/lib/src/test_pointer.dart +++ b/packages/flutter_test/lib/src/test_pointer.dart @@ -25,11 +25,7 @@ class TestPointer { this.kind = PointerDeviceKind.touch, int? device, int buttons = kPrimaryButton, - ]) - : assert(kind != null), - assert(pointer != null), - assert(buttons != null), - _buttons = buttons { + ]) : _buttons = buttons { switch (kind) { case PointerDeviceKind.mouse: _device = device ?? 1; @@ -211,7 +207,6 @@ class TestPointer { Duration timeStamp = Duration.zero, Offset? location, }) { - assert(timeStamp != null); _location = location ?? _location; return PointerAddedEvent( timeStamp: timeStamp, @@ -230,7 +225,6 @@ class TestPointer { Duration timeStamp = Duration.zero, Offset? location, }) { - assert(timeStamp != null); _location = location ?? _location; return PointerRemovedEvent( timeStamp: timeStamp, @@ -251,13 +245,10 @@ class TestPointer { Offset newLocation, { Duration timeStamp = Duration.zero, }) { - assert(newLocation != null); - assert(timeStamp != null); assert( !isDown, 'Hover events can only be generated when the pointer is up. To ' 'simulate movement when the pointer is down, use move() instead.'); - assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate hover events"); final Offset delta = location != null ? newLocation - location! : Offset.zero; _location = newLocation; return PointerHoverEvent( @@ -278,8 +269,6 @@ class TestPointer { Offset scrollDelta, { Duration timeStamp = Duration.zero, }) { - assert(scrollDelta != null); - assert(timeStamp != null); assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate pointer signal events"); assert(location != null); return PointerScrollEvent( @@ -327,11 +316,7 @@ class TestGesture { PointerDeviceKind kind = PointerDeviceKind.touch, int? device, int buttons = kPrimaryButton, - }) : assert(dispatcher != null), - assert(pointer != null), - assert(kind != null), - assert(buttons != null), - _dispatcher = dispatcher, + }) : _dispatcher = dispatcher, _pointer = TestPointer(pointer, kind, device, buttons); /// Dispatch a pointer down event at the given `downLocation`, caching the @@ -380,8 +365,7 @@ class TestGesture { /// Send a move event moving the pointer by the given offset. /// /// If the pointer is down, then a move event is dispatched. If the pointer is - /// up, then a hover event is dispatched. Touch devices are not able to send - /// hover events. + /// up, then a hover event is dispatched. Future moveBy(Offset offset, { Duration timeStamp = Duration.zero }) { assert(_pointer.location != null); return moveTo(_pointer.location! + offset, timeStamp: timeStamp); @@ -390,15 +374,12 @@ class TestGesture { /// Send a move event moving the pointer to the given location. /// /// If the pointer is down, then a move event is dispatched. If the pointer is - /// up, then a hover event is dispatched. Touch devices are not able to send - /// hover events. + /// up, then a hover event is dispatched. Future moveTo(Offset location, { Duration timeStamp = Duration.zero }) { return TestAsyncUtils.guard(() { if (_pointer._isDown) { return _dispatcher(_pointer.move(location, timeStamp: timeStamp)); } else { - assert(_pointer.kind != PointerDeviceKind.touch, - 'Touch device move events can only be sent if the pointer is down.'); return _dispatcher(_pointer.hover(location, timeStamp: timeStamp)); } });