Unverified Commit 2cdf2f00 authored by Tong Mu's avatar Tong Mu Committed by GitHub

Treat hover events as normal pointer events, and bring them back to Listener (#63834)

parent 2cdec258
......@@ -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
}
......
// 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));
}
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment