Treat hover events as normal pointer events, and bring them back to Listener (#63834)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<MouseTrackerAnnotation, Matrix4> 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<MouseTrackerAnnotation> hoveringAnnotations = pointerHasMoved ? nextAnnotations.keys.toList().reversed : enteringAnnotations;
|
||||
for (final MouseTrackerAnnotation annotation in hoveringAnnotations) {
|
||||
if (annotation.onHover != null) {
|
||||
annotation.onHover!(triggeringEvent.transformed(nextAnnotations[annotation]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -848,6 +848,10 @@ abstract class AndroidViewController extends PlatformViewController {
|
||||
/// for description of the parameters.
|
||||
@override
|
||||
Future<void> dispatchPointerEvent(PointerEvent event) async {
|
||||
if (event is PointerHoverEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event is PointerDownEvent) {
|
||||
_motionEventConverter.handlePointerDownEvent(event);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<PointerHoverEvent>());
|
||||
expect(events[1], isA<PointerHoverEvent>());
|
||||
expect(events[2], isA<PointerHoverEvent>());
|
||||
expect(pointerRouterEvents.length, 6,
|
||||
reason: 'pointerRouterEvents contains: $pointerRouterEvents');
|
||||
expect(pointerRouterEvents[0], isA<PointerAddedEvent>());
|
||||
|
||||
@@ -546,7 +546,7 @@ void main() {
|
||||
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
|
||||
_pointerData(PointerChange.hover, const Offset(0.0, 10.0)),
|
||||
]));
|
||||
expect(logs, <String>['enterA', 'enterB', 'hoverA', 'hoverB']);
|
||||
expect(logs, <String>['enterA', 'enterB', 'hoverB', 'hoverA']);
|
||||
logs.clear();
|
||||
|
||||
// Moves out of A within one frame.
|
||||
|
||||
@@ -77,7 +77,6 @@ class TestAnnotationTarget with Diagnosticable implements MouseTrackerAnnotation
|
||||
@override
|
||||
final PointerEnterEventListener? onEnter;
|
||||
|
||||
@override
|
||||
final PointerHoverEventListener? onHover;
|
||||
|
||||
@override
|
||||
|
||||
@@ -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: <ui.PointerData>[
|
||||
_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
|
||||
}
|
||||
|
||||
|
||||
@@ -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<bool> onHover;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
HoverClientState createState() => HoverClientState();
|
||||
}
|
||||
|
||||
class HoverClientState extends State<HoverClient> {
|
||||
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<HoverFeedback> {
|
||||
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<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);
|
||||
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: <Widget>[
|
||||
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<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);
|
||||
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: <Widget>[
|
||||
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<PointerEvent> events = <PointerEvent>[];
|
||||
|
||||
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<PointerEnterEvent>());
|
||||
expect(events.last, isA<PointerHoverEvent>());
|
||||
events.clear();
|
||||
|
||||
await gesture.moveTo(bottomLeft + const Offset(1, -1));
|
||||
await tester.pump();
|
||||
expect(events.single, isA<PointerHoverEvent>());
|
||||
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<PointerExitEvent>());
|
||||
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<TransformLayer>(), 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<TransformLayer>(), 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<PointerEnterEvent> enter = <PointerEnterEvent>[];
|
||||
final List<PointerHoverEvent> hover = <PointerHoverEvent>[];
|
||||
final List<PointerExitEvent> exit = <PointerExitEvent>[];
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -49,6 +49,34 @@ void main() {
|
||||
]));
|
||||
});
|
||||
|
||||
testWidgets('Detects hover events from touch devices', (WidgetTester tester) async {
|
||||
final List<String> log = <String>[];
|
||||
|
||||
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(<String>[
|
||||
'bottom',
|
||||
]));
|
||||
});
|
||||
|
||||
group('transformed events', () {
|
||||
testWidgets('simple offset for touch/signal', (WidgetTester tester) async {
|
||||
final List<PointerEvent> events = <PointerEvent>[];
|
||||
|
||||
@@ -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, <String>['enterA', 'enterB', 'enterC', 'hoverA', 'hoverB', 'hoverC']);
|
||||
expect(logs, <String>['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, <String>['exitC', 'hoverA', 'hoverB']);
|
||||
expect(logs, <String>['exitC', 'hoverB', 'hoverA']);
|
||||
logs.clear();
|
||||
|
||||
// Move back to the overlapping area.
|
||||
await gesture.moveTo(const Offset(75, 75));
|
||||
await tester.pumpAndSettle();
|
||||
expect(logs, <String>['enterC', 'hoverA', 'hoverB', 'hoverC']);
|
||||
expect(logs, <String>['enterC', 'hoverC', 'hoverB', 'hoverA']);
|
||||
logs.clear();
|
||||
|
||||
// Move to the C only area.
|
||||
await gesture.moveTo(const Offset(125, 75));
|
||||
await tester.pumpAndSettle();
|
||||
expect(logs, <String>['exitB', 'hoverA', 'hoverC']);
|
||||
expect(logs, <String>['exitB', 'hoverC', 'hoverA']);
|
||||
logs.clear();
|
||||
|
||||
// Move back to the overlapping area.
|
||||
await gesture.moveTo(const Offset(75, 75));
|
||||
await tester.pumpAndSettle();
|
||||
expect(logs, <String>['enterB', 'hoverA', 'hoverB', 'hoverC']);
|
||||
expect(logs, <String>['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, <String>['enterA', 'enterC', 'hoverA', 'hoverC']);
|
||||
expect(logs, <String>['enterA', 'enterC', 'hoverC', 'hoverA']);
|
||||
logs.clear();
|
||||
|
||||
// Move to the B only area.
|
||||
await gesture.moveTo(const Offset(25, 75));
|
||||
await tester.pumpAndSettle();
|
||||
expect(logs, <String>['exitC', 'enterB', 'hoverA', 'hoverB']);
|
||||
expect(logs, <String>['exitC', 'enterB', 'hoverB', 'hoverA']);
|
||||
logs.clear();
|
||||
|
||||
// Move back to the overlapping area.
|
||||
await gesture.moveTo(const Offset(75, 75));
|
||||
await tester.pumpAndSettle();
|
||||
expect(logs, <String>['exitB', 'enterC', 'hoverA', 'hoverC']);
|
||||
expect(logs, <String>['exitB', 'enterC', 'hoverC', 'hoverA']);
|
||||
logs.clear();
|
||||
|
||||
// Move to the C only area.
|
||||
await gesture.moveTo(const Offset(125, 75));
|
||||
await tester.pumpAndSettle();
|
||||
expect(logs, <String>['hoverA', 'hoverC']);
|
||||
expect(logs, <String>['hoverC', 'hoverA']);
|
||||
logs.clear();
|
||||
|
||||
// Move back to the overlapping area.
|
||||
await gesture.moveTo(const Offset(75, 75));
|
||||
await tester.pumpAndSettle();
|
||||
expect(logs, <String>['hoverA', 'hoverC']);
|
||||
expect(logs, <String>['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, <String>['enterA', 'enterC', 'hoverA', 'hoverC']);
|
||||
expect(logs, <String>['enterA', 'enterC', 'hoverC', 'hoverA']);
|
||||
logs.clear();
|
||||
|
||||
// Move out.
|
||||
|
||||
@@ -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<void> 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<void> moveTo(Offset location, { Duration timeStamp = Duration.zero }) {
|
||||
return TestAsyncUtils.guard<void>(() {
|
||||
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));
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user