MouseTracker no longer requires annotations attached (#48453)

This commit is contained in:
Tong Mu 2020-01-28 15:03:01 -08:00 committed by GitHub
parent dd98046fe5
commit da0a7d8b2e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 523 additions and 603 deletions

View file

@ -30,7 +30,7 @@ typedef PointerHoverEventListener = void Function(PointerHoverEvent event);
/// movements.
///
/// This is added to a layer and managed by the [MouseRegion] widget.
class MouseTrackerAnnotation {
class MouseTrackerAnnotation extends Diagnosticable {
/// Creates an annotation that can be used to find layers interested in mouse
/// movements.
const MouseTrackerAnnotation({this.onEnter, this.onHover, this.onExit});
@ -39,24 +39,13 @@ class MouseTrackerAnnotation {
/// entered the annotated region.
///
/// This callback is triggered when the pointer has started to be contained
/// by the annotationed region for any reason.
///
/// More specifically, the callback is triggered by the following cases:
///
/// * A new annotated region has appeared under a pointer.
/// * An existing annotated region has moved to under a pointer.
/// * A new pointer has been added to somewhere within an annotated region.
/// * An existing pointer has moved into an annotated region.
///
/// This callback is not always matched by an [onExit]. If the render object
/// that owns the annotation is disposed while being hovered by a pointer,
/// the [onExit] callback of that annotation will never called, despite
/// the earlier call of [onEnter]. For more details, see [onExit].
/// by the annotationed region for any reason, which means it always matches a
/// later [onExit].
///
/// See also:
///
/// * [MouseRegion.onEnter], which uses this callback.
/// * [onExit], which is triggered when a mouse pointer exits the region.
/// * [MouseRegion.onEnter], which uses this callback.
final PointerEnterEventListener onEnter;
/// Triggered when a pointer has moved within the annotated region without
@ -69,7 +58,7 @@ class MouseTrackerAnnotation {
/// * A pointer has moved onto, or moved within an annotation without buttons
/// pressed.
///
/// This callback is not triggered when
/// This callback is not triggered when:
///
/// * An annotation that is containing the pointer has moved, and still
/// contains the pointer.
@ -78,59 +67,30 @@ class MouseTrackerAnnotation {
/// Triggered when a mouse pointer, with or without buttons pressed, has
/// exited the annotated region when the annotated region still exists.
///
/// This callback is triggered when the pointer has stopped to be contained
/// by the region, except when it's caused by the removal of the render object
/// that owns the annotation. More specifically, the callback is triggered by
/// the following cases:
///
/// * An annotated region that used to contain a pointer has moved away.
/// * A pointer that used to be within an annotated region has been removed.
/// * A pointer that used to be within an annotated region has moved away.
///
/// And is __not__ triggered by the following case,
///
/// * An annotated region that used to contain a pointer has disappeared.
///
/// The last case is the only case when [onExit] does not match an earlier
/// This callback is triggered when the pointer has stopped being contained
/// by the region for any reason, which means it always matches an earlier
/// [onEnter].
/// {@template flutter.mouseTracker.onExit}
/// This design is because the last case is very likely to be
/// handled improperly and cause exceptions (such as calling `setState` of the
/// disposed widget). There are a few ways to mitigate this limit:
///
/// * If the state of hovering is contained within a widget that
/// unconditionally attaches the annotation (as long as a mouse is
/// connected), then this will not be a concern, since when the annotation
/// is disposed the state is no longer used.
/// * If you're accessible to the condition that controls whether the
/// annotation is attached, then you can call the callback when that
/// condition goes from true to false.
/// * In the cases where the solutions above won't work, you can always
/// override [State.dispose] or [RenderObject.detach].
/// {@endtemplate}
///
/// Technically, whether [onExit] will be called is controlled by
/// [MouseTracker.attachAnnotation] and [MouseTracker.detachAnnotation].
///
/// See also:
///
/// * [MouseRegion.onExit], which uses this callback.
/// * [onEnter], which is triggered when a mouse pointer enters the region.
/// * [RenderMouseRegion.onExit], which uses this callback.
/// * [MouseRegion.onExit], which uses this callback, but is not triggered in
/// certain cases and does not always match its earier [MouseRegion.onEnter].
final PointerExitEventListener onExit;
@override
String toString() {
final List<String> callbacks = <String>[];
if (onEnter != null)
callbacks.add('enter');
if (onHover != null)
callbacks.add('hover');
if (onExit != null)
callbacks.add('exit');
final String describeCallbacks = callbacks.isEmpty
? '<none>'
: callbacks.join(' ');
return '${describeIdentity(this)}(callbacks: $describeCallbacks)';
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagsSummary<Function>(
'callbacks',
<String, Function> {
'enter': onEnter,
'hover': onHover,
'exit': onExit,
},
ifEmpty: '<none>',
));
}
}
@ -194,11 +154,9 @@ class _MouseState {
///
/// ### Details
///
/// The state of [MouseTracker] consists of 3 parts:
/// The state of [MouseTracker] consists of two parts:
///
/// * The mouse devices that are connected.
/// * The annotations that are attached, i.e. whose owner render object is
/// painted on the screen.
/// * In which annotations each device is contained.
///
/// The states remain stable most of the time, and are only changed at the
@ -247,10 +205,6 @@ class MouseTracker extends ChangeNotifier {
// mouse events from.
final PointerRouter _router;
// The collection of annotations that are currently being tracked. It is
// operated on by [attachAnnotation] and [detachAnnotation].
final Set<MouseTrackerAnnotation> _trackedAnnotations = <MouseTrackerAnnotation>{};
// Tracks the state of connected mouse devices.
//
// It is the source of truth for the list of connected mouse devices.
@ -298,7 +252,6 @@ class MouseTracker extends ChangeNotifier {
nextAnnotations: mouseState.annotations,
previousEvent: previousEvent,
unhandledEvent: event,
trackedAnnotations: _trackedAnnotations,
);
},
);
@ -306,12 +259,12 @@ class MouseTracker extends ChangeNotifier {
// Find the annotations that is hovered by the device of the `state`.
//
// If the device is not connected or there are no annotations attached, empty
// is returned without calling `annotationFinder`.
// If the device is not connected, an empty set is returned without calling
// `annotationFinder`.
LinkedHashSet<MouseTrackerAnnotation> _findAnnotations(_MouseState state) {
final Offset globalPosition = state.latestEvent.position;
final int device = state.device;
return (_mouseStates.containsKey(device) && _trackedAnnotations.isNotEmpty)
return (_mouseStates.containsKey(device))
? LinkedHashSet<MouseTrackerAnnotation>.from(annotationFinder(globalPosition))
: <MouseTrackerAnnotation>{} as LinkedHashSet<MouseTrackerAnnotation>;
}
@ -332,7 +285,6 @@ class MouseTracker extends ChangeNotifier {
nextAnnotations: mouseState.annotations,
previousEvent: mouseState.latestEvent,
unhandledEvent: null,
trackedAnnotations: _trackedAnnotations,
);
}
);
@ -428,16 +380,15 @@ class MouseTracker extends ChangeNotifier {
// null, which means the update is triggered by a new event.
// The `unhandledEvent` can be null, which means the update is not triggered
// by an event.
// However, one of `previousEvent` or `unhandledEvent` must not be null.
static void _dispatchDeviceCallbacks({
@required LinkedHashSet<MouseTrackerAnnotation> lastAnnotations,
@required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations,
@required PointerEvent previousEvent,
@required PointerEvent unhandledEvent,
@required Set<MouseTrackerAnnotation> trackedAnnotations,
}) {
assert(lastAnnotations != null);
assert(nextAnnotations != null);
assert(trackedAnnotations != null);
final PointerEvent latestEvent = unhandledEvent ?? previousEvent;
assert(latestEvent != null);
// Order is important for mouse event callbacks. The `findAnnotations`
@ -446,45 +397,41 @@ class MouseTracker extends ChangeNotifier {
// The algorithm here is explained in
// https://github.com/flutter/flutter/issues/41420
// Send exit events in visual order.
final Iterable<MouseTrackerAnnotation> exitingAnnotations =
lastAnnotations.difference(nextAnnotations);
// Send exit events to annotations that are in last but not in next, in
// visual order.
final Iterable<MouseTrackerAnnotation> exitingAnnotations = lastAnnotations.where(
(MouseTrackerAnnotation value) => !nextAnnotations.contains(value),
);
for (final MouseTrackerAnnotation annotation in exitingAnnotations) {
final bool attached = trackedAnnotations.contains(annotation);
// Exit is not sent if annotation is no longer attached, because this
// trigger may cause exceptions and has safer alternatives. See
// [MouseRegion.onExit] for details.
if (annotation.onExit != null && attached) {
if (annotation.onExit != null) {
annotation.onExit(PointerExitEvent.fromMouseEvent(latestEvent));
}
}
// Send enter events in reverse visual order.
// Send enter events to annotations that are not in last but in next, in
// reverse visual order.
final Iterable<MouseTrackerAnnotation> enteringAnnotations =
nextAnnotations.difference(lastAnnotations).toList().reversed;
for (final MouseTrackerAnnotation annotation in enteringAnnotations) {
assert(trackedAnnotations.contains(annotation));
if (annotation.onEnter != null) {
annotation.onEnter(PointerEnterEvent.fromMouseEvent(latestEvent));
}
}
// Send hover events in reverse visual order.
// For now the order between the hover events is designed this way for no
// solid reasons but to keep it aligned with enter events for simplicity.
// 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 (unhandledEvent is PointerHoverEvent) {
final Iterable<MouseTrackerAnnotation> hoveringAnnotations =
nextAnnotations.toList().reversed;
final Offset lastHoverPosition = previousEvent is PointerHoverEvent ? previousEvent.position : null;
final bool pointerHasMoved = lastHoverPosition == null || lastHoverPosition != unhandledEvent.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.toList().reversed : enteringAnnotations;
for (final MouseTrackerAnnotation annotation in hoveringAnnotations) {
// Deduplicate: Trigger hover if it's a newly hovered annotation
// or the position has changed
assert(trackedAnnotations.contains(annotation));
if (!lastAnnotations.contains(annotation)
|| lastHoverPosition != unhandledEvent.position) {
if (annotation.onHover != null) {
annotation.onHover(unhandledEvent);
}
if (annotation.onHover != null) {
annotation.onHover(unhandledEvent);
}
}
}
@ -519,63 +466,4 @@ class MouseTracker extends ChangeNotifier {
/// Whether or not a mouse is connected and has produced events.
bool get mouseIsConnected => _mouseStates.isNotEmpty;
/// Checks if the given [MouseTrackerAnnotation] is attached to this
/// [MouseTracker].
///
/// This function is only public to allow for proper testing of the
/// MouseTracker. Do not call in other contexts.
@visibleForTesting
bool isAnnotationAttached(MouseTrackerAnnotation annotation) {
return _trackedAnnotations.contains(annotation);
}
/// Notify [MouseTracker] that a new [MouseTrackerAnnotation] has started to
/// take effect.
///
/// This method is typically called by the [RenderObject] that owns an
/// annotation, as soon as the render object is added to the render tree.
///
/// {@template flutter.mouseTracker.attachAnnotation}
/// Render objects that call this method might want to schedule a frame as
/// well, typically by calling [RenderObject.markNeedsPaint], because this
/// method does not cause any immediate effect, since the state it changes is
/// used during a post-frame callback or when handling certain pointer events.
///
/// ### About annotation attachment
///
/// It is the responsibility of the render object that owns the annotation to
/// maintain the attachment of the annotation. Whether an annotation is
/// attached should be kept in sync with whether its owner object is mounted,
/// which is used in the following ways:
///
/// * If a pointer enters an annotation, it is asserted that the annotation
/// is attached.
/// * If a pointer stops being contained by an annotation,
/// the exit event is triggered only if the annotation is still attached.
/// This is to prevent exceptions caused calling setState of a disposed
/// widget. See [MouseTrackerAnnotation.onExit] for more details.
/// * The [MouseTracker] also uses the attachment to track the number of
/// attached annotations, and will skip mouse position checks if there is no
/// annotations attached.
/// {@endtemplate}
/// * Attaching an annotation that has been attached will assert.
void attachAnnotation(MouseTrackerAnnotation annotation) {
assert(!_duringDeviceUpdate);
assert(!_trackedAnnotations.contains(annotation));
_trackedAnnotations.add(annotation);
}
/// Notify [MouseTracker] that a mouse tracker annotation that was previously
/// attached has stopped taking effect.
///
/// This method is typically called by the [RenderObject] that owns an
/// annotation, as soon as the render object is removed from the render tree.
/// {@macro flutter.mouseTracker.attachAnnotation}
/// * Detaching an annotation that has not been attached will assert.
void detachAnnotation(MouseTrackerAnnotation annotation) {
assert(!_duringDeviceUpdate);
assert(_trackedAnnotations.contains(annotation));
_trackedAnnotations.remove(annotation);
}
}

View file

@ -10,7 +10,6 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter/services.dart';
import 'binding.dart';
import 'box.dart';
import 'layer.dart';
import 'object.dart';
@ -838,13 +837,11 @@ mixin _PlatformViewGestureMixin on RenderBox {
if (_handlePointerEvent != null)
_handlePointerEvent(event);
});
RendererBinding.instance.mouseTracker.attachAnnotation(_hoverAnnotation);
}
@override
void detach() {
_gestureRecognizer.reset();
RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation);
_hoverAnnotation = null;
super.detach();
}

View file

@ -2659,6 +2659,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
/// [RenderMouseRegion].
class RenderMouseRegion extends RenderProxyBox {
/// Creates a render object that forwards pointer events to callbacks.
///
/// All parameters are optional. By default this method creates an opaque
/// mouse region with no callbacks.
RenderMouseRegion({
PointerEnterEventListener onEnter,
PointerHoverEventListener onHover,
@ -2698,17 +2701,23 @@ class RenderMouseRegion extends RenderProxyBox {
set opaque(bool value) {
if (_opaque != value) {
_opaque = value;
_updateAnnotations();
_markPropertyUpdated(mustRepaint: true);
}
}
/// Called when a mouse pointer enters the region (with or without buttons
/// pressed).
/// Called when a mouse pointer starts being contained by the region (with or
/// without buttons pressed) for any reason.
///
/// This callback is always matched by a later [onExit].
///
/// See also:
///
/// * [MouseRegion.onEnter], which uses this callback.
PointerEnterEventListener get onEnter => _onEnter;
set onEnter(PointerEnterEventListener value) {
if (_onEnter != value) {
_onEnter = value;
_updateAnnotations();
_markPropertyUpdated(mustRepaint: false);
}
}
PointerEnterEventListener _onEnter;
@ -2723,7 +2732,7 @@ class RenderMouseRegion extends RenderProxyBox {
set onHover(PointerHoverEventListener value) {
if (_onHover != value) {
_onHover = value;
_updateAnnotations();
_markPropertyUpdated(mustRepaint: false);
}
}
PointerHoverEventListener _onHover;
@ -2732,13 +2741,20 @@ class RenderMouseRegion extends RenderProxyBox {
_onHover(event);
}
/// Called when a pointer leaves the region (with or without buttons pressed)
/// and the annotation is still attached.
/// Called when a pointer is no longer contained by the region (with or
/// without buttons pressed) for any reason.
///
/// This callback is always matched by an earlier [onEnter].
///
/// See also:
///
/// * [MouseRegion.onExit], which uses this callback, but is not triggered in
/// certain cases and does not always match its earier [MouseRegion.onEnter].
PointerExitEventListener get onExit => _onExit;
set onExit(PointerExitEventListener value) {
if (_onExit != value) {
_onExit = value;
_updateAnnotations();
_markPropertyUpdated(mustRepaint: false);
}
}
PointerExitEventListener _onExit;
@ -2757,64 +2773,52 @@ class RenderMouseRegion extends RenderProxyBox {
@visibleForTesting
MouseTrackerAnnotation get hoverAnnotation => _hoverAnnotation;
void _updateAnnotations() {
final bool annotationWasActive = _annotationIsActive;
final bool annotationWillBeActive = (
// Call this method when a property has changed and might affect the
// `_annotationIsActive` bit.
//
// If `mustRepaint` is false, this method does NOT call `markNeedsPaint`
// unless the `_annotationIsActive` bit is changed. If there is a property
// that needs updating while `_annotationIsActive` stays true, make
// `mustRepaint` true.
//
// This method must not be called during `paint`.
void _markPropertyUpdated({@required bool mustRepaint}) {
assert(owner == null || !owner.debugDoingPaint);
final bool newAnnotationIsActive = (
_onEnter != null ||
_onHover != null ||
_onExit != null ||
opaque
) &&
RendererBinding.instance.mouseTracker.mouseIsConnected;
if (annotationWasActive != annotationWillBeActive) {
) && RendererBinding.instance.mouseTracker.mouseIsConnected;
_setAnnotationIsActive(newAnnotationIsActive);
if (mustRepaint)
markNeedsPaint();
}
void _setAnnotationIsActive(bool value) {
final bool annotationWasActive = _annotationIsActive;
_annotationIsActive = value;
if (annotationWasActive != value) {
markNeedsPaint();
markNeedsCompositingBitsUpdate();
if (annotationWillBeActive) {
RendererBinding.instance.mouseTracker.attachAnnotation(_hoverAnnotation);
} else {
RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation);
}
_annotationIsActive = annotationWillBeActive;
}
}
void _handleUpdatedMouseIsConnected() {
_markPropertyUpdated(mustRepaint: false);
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
// Add a listener to listen for changes in mouseIsConnected.
RendererBinding.instance.mouseTracker.addListener(_updateAnnotations);
_updateAnnotations();
}
/// Attaches the annotation for this render object, if any.
///
/// This is called by the [MouseRegion]'s [Element] to tell this
/// [RenderMouseRegion] that it has transitioned from "inactive"
/// state to "active". We call it here so that
/// [MouseTrackerAnnotation.onEnter] isn't called during the build step for
/// the widget that provided the callback, and [State.setState] can safely be
/// called within that callback.
void postActivate() {
if (_annotationIsActive)
RendererBinding.instance.mouseTracker.attachAnnotation(_hoverAnnotation);
}
/// Detaches the annotation for this render object, if any.
///
/// This is called by the [MouseRegion]'s [Element] to tell this
/// [RenderMouseRegion] that it will shortly be transitioned from "active"
/// state to "inactive". We call it here so that
/// [MouseTrackerAnnotation.onExit] isn't called during the build step for the
/// widget that provided the callback, and [State.setState] can safely be
/// called within that callback.
void preDeactivate() {
if (_annotationIsActive)
RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation);
RendererBinding.instance.mouseTracker.addListener(_handleUpdatedMouseIsConnected);
_markPropertyUpdated(mustRepaint: false);
}
@override
void detach() {
RendererBinding.instance.mouseTracker.removeListener(_updateAnnotations);
RendererBinding.instance.mouseTracker.removeListener(_handleUpdatedMouseIsConnected);
super.detach();
}

View file

@ -5854,7 +5854,7 @@ class _PointerListener extends SingleChildRenderObjectWidget {
///
/// * [Listener], a similar widget that tracks pointer events when the pointer
/// have buttons pressed.
class MouseRegion extends SingleChildRenderObjectWidget {
class MouseRegion extends StatefulWidget {
/// Creates a widget that forwards mouse events to callbacks.
const MouseRegion({
Key key,
@ -5862,16 +5862,15 @@ class MouseRegion extends SingleChildRenderObjectWidget {
this.onExit,
this.onHover,
this.opaque = true,
Widget child,
this.child,
}) : assert(opaque != null),
super(key: key, child: child);
super(key: key);
/// Called when a mouse pointer, with or without buttons pressed, has
/// entered this widget.
/// Called when a mouse pointer has entered this widget.
///
/// This callback is triggered when the pointer has started to be contained
/// by the region of this widget. More specifically, the callback is triggered
/// by the following cases:
/// This callback is triggered when the pointer, with or without buttons
/// pressed, has started to be contained by the region of this widget. More
/// specifically, the callback is triggered by the following cases:
///
/// * This widget has appeared under a pointer.
/// * This widget has moved to under a pointer.
@ -5880,8 +5879,13 @@ class MouseRegion extends SingleChildRenderObjectWidget {
///
/// This callback is not always matched by an [onExit]. If the [MouseRegion]
/// is unmounted while being hovered by a pointer, the [onExit] of the widget
/// callback will never called, despite the earlier call of [onEnter]. For
/// more details, see [onExit].
/// callback will never called. For more details, see [onExit].
///
/// {@template flutter.mouseRegion.triggerTime}
/// The time that this callback is triggered is always between frames: either
/// during the post-frame callbacks, or during the callback of a pointer
/// event.
/// {@endtemplate}
///
/// See also:
///
@ -5890,47 +5894,182 @@ class MouseRegion extends SingleChildRenderObjectWidget {
/// internally implemented.
final PointerEnterEventListener onEnter;
/// Called when a mouse pointer changes position without buttons pressed, and
/// the new position is within the region defined by this widget.
/// Called when a mouse pointer moves within this widget without buttons
/// pressed.
///
/// This callback is triggered when:
/// This callback is not triggered when the [MouseRegion] has moved
/// while being hovered by the mouse pointer.
///
/// * An annotation that did not contain the pointer has moved to under a
/// pointer that has no buttons pressed.
/// * A pointer has moved onto, or moved within an annotation without buttons
/// pressed.
///
/// This callback is not triggered when
///
/// * An annotation that is containing the pointer has moved, and still
/// contains the pointer.
/// {@macro flutter.mouseRegion.triggerTime}
final PointerHoverEventListener onHover;
/// Called when a mouse pointer, with or without buttons pressed, has exited
/// this widget when the widget is still mounted.
/// Called when a mouse pointer has exited this widget when the widget is
/// still mounted.
///
/// This callback is triggered when the pointer has stopped to be contained
/// by the region of this widget, except when it's caused by the removal of
/// this widget. More specifically, the callback is triggered by
/// the following cases:
/// This callback is triggered when the pointer, with or without buttons
/// pressed, has stopped being contained by the region of this widget, except
/// when the exit is caused by the disappearance of this widget. More
/// specifically, this callback is triggered by the following cases:
///
/// * This widget, which used to contain a pointer, has moved away.
/// * A pointer that used to be within this widget has been removed.
/// * A pointer that used to be within this widget has moved away.
/// * A pointer that is hovering this widget has moved away.
/// * A pointer that is hovering this widget has been removed.
/// * This widget, which is being hovered by a pointer, has moved away.
///
/// And is __not__ triggered by the following case,
/// And is __not__ triggered by the following case:
///
/// * This widget, which used to contain a pointer, has disappeared.
/// * This widget, which is being hovered by a pointer, has disappeared.
///
/// The last case is the only case when [onExit] does not match an earlier
/// [onEnter].
/// {@macro flutter.mouseTracker.onExit}
/// This means that a [MouseRegion.onExit] might not be matched by a
/// [MouseRegion.onEnter].
///
/// This restriction aims to prevent a common misuse: if [setState] is called
/// during [MouseRegion.onExit] without checking whether the widget is still
/// mounted, an exception will occur. This is because the callback is
/// triggered during the post-frame phase, at which point the widget has been
/// unmounted. Since [setState] is exclusive to widgets, the restriction is
/// specific to [MouseRegion], and does not apply to its lower-level
/// counterparts, [RenderMouseRegion] and [MouseTrackerAnnotation].
///
/// There are a few ways to mitigate this restriction:
///
/// * If the hover state is completely contained within a widget that
/// unconditionally creates this [MouseRegion], then this will not be a
/// concern, since after the [MouseRegion] is unmounted the state is no
/// longer used.
/// * Otherwise, the outer widget very likely has access to the variable that
/// controls whether this [MouseRegion] is present. If so, call [onExit] at
/// the event that turns the condition from true to false.
/// * In cases where the solutions above won't work, you can always
/// override [State.dispose] and call [onExit], or create your own widget
/// using [RenderMouseRegion].
///
/// {@tool sample --template=stateful_widget_scaffold_center}
/// The following example shows a blue rectangular that turns yellow when
/// hovered. Since the hover state is completely contained within a widget
/// that unconditionally creates the `MouseRegion`, you can ignore the
/// aforementioned restriction.
///
/// ```dart
/// bool hovered = false;
///
/// @override
/// Widget build(BuildContext context) {
/// return Container(
/// height: 100,
/// width: 100,
/// decoration: BoxDecoration(color: hovered ? Colors.yellow : Colors.blue),
/// child: MouseRegion(
/// onEnter: (_) {
/// setState(() { hovered = true; });
/// },
/// onExit: (_) {
/// setState(() { hovered = false; });
/// },
/// ),
/// );
/// }
/// ```
/// {@end-tool}
///
/// {@tool sample --template=stateful_widget_scaffold_center}
/// The following example shows a widget that hides its content one second
/// after behing hovered, and also exposes the enter and exit callbacks.
/// Because the widget conditionally creates the `MouseRegion`, and leaks the
/// hover state, it needs to take the restriction into consideration. In this
/// case, since it has access to the event that triggers the disappearance of
/// the `MouseRegion`, it simply trigger the exit callback during that event
/// as well.
///
/// ```dart preamble
/// // A region that hides its content one second after being hovered.
/// class MyTimedButton extends StatefulWidget {
/// MyTimedButton({ Key key, this.onEnterButton, this.onExitButton })
/// : super(key: key);
///
/// final VoidCallback onEnterButton;
/// final VoidCallback onExitButton;
///
/// @override
/// _MyTimedButton createState() => _MyTimedButton();
/// }
///
/// class _MyTimedButton extends State<MyTimedButton> {
/// bool regionIsHidden = false;
/// bool hovered = false;
///
/// void startCountdown() async {
/// await Future.delayed(const Duration(seconds: 1));
/// hideButton();
/// }
///
/// void hideButton() {
/// setState(() { regionIsHidden = true; });
/// // This statement is necessary.
/// if (hovered)
/// widget.onExitButton();
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return Container(
/// width: 100,
/// height: 100,
/// child: MouseRegion(
/// child: regionIsHidden ? null : MouseRegion(
/// onEnter: (_) {
/// widget.onEnterButton();
/// setState(() { hovered = true; });
/// startCountdown();
/// },
/// onExit: (_) {
/// setState(() { hovered = false; });
/// widget.onExitButton();
/// },
/// child: Container(color: Colors.red),
/// ),
/// ),
/// );
/// }
/// }
/// ```
///
/// ```dart
/// Key key = UniqueKey();
/// bool hovering = false;
///
/// @override
/// Widget build(BuildContext context) {
/// return Column(
/// children: <Widget>[
/// RaisedButton(
/// onPressed: () {
/// setState(() { key = UniqueKey(); });
/// },
/// child: Text('Refresh'),
/// ),
/// hovering ? Text('Hovering') : Text('Not hovering'),
/// MyTimedButton(
/// key: key,
/// onEnterButton: () {
/// setState(() { hovering = true; });
/// },
/// onExitButton: () {
/// setState(() { hovering = false; });
/// },
/// ),
/// ],
/// );
/// }
/// ```
/// {@end-tool}
///
/// {@macro flutter.mouseRegion.triggerTime}
///
/// See also:
///
/// * [onEnter], which is triggered when a mouse pointer enters the region.
/// * [MouseTrackerAnnotation.onExit], which is how this callback is
/// internally implemented.
/// * [RenderMouseRegion] and [MouseTrackerAnnotation.onExit], which are how
/// this callback is internally implemented, but without the restriction.
final PointerExitEventListener onExit;
/// Whether this widget should prevent other [MouseRegion]s visually behind it
@ -5949,27 +6088,13 @@ class MouseRegion extends SingleChildRenderObjectWidget {
/// This defaults to true.
final bool opaque;
@override
_MouseRegionElement createElement() => _MouseRegionElement(this);
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.child}
final Widget child;
@override
RenderMouseRegion createRenderObject(BuildContext context) {
return RenderMouseRegion(
onEnter: onEnter,
onHover: onHover,
onExit: onExit,
opaque: opaque,
);
}
@override
void updateRenderObject(BuildContext context, RenderMouseRegion renderObject) {
renderObject
..onEnter = onEnter
..onHover = onHover
..onExit = onExit
..opaque = opaque;
}
_MouseRegionState createState() => _MouseRegionState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@ -5986,21 +6111,46 @@ class MouseRegion extends SingleChildRenderObjectWidget {
}
}
class _MouseRegionElement extends SingleChildRenderObjectElement {
_MouseRegionElement(SingleChildRenderObjectWidget widget) : super(widget);
class _MouseRegionState extends State<MouseRegion> {
void handleExit(PointerExitEvent event) {
if (widget.onExit != null && mounted)
widget.onExit(event);
}
@override
void activate() {
super.activate();
final RenderMouseRegion renderMouseRegion = renderObject as RenderMouseRegion;
renderMouseRegion.postActivate();
PointerExitEventListener getHandleExit() {
return widget.onExit == null ? null : handleExit;
}
@override
void deactivate() {
final RenderMouseRegion renderMouseRegion = renderObject as RenderMouseRegion;
renderMouseRegion.preDeactivate();
super.deactivate();
Widget build(BuildContext context) {
return _RawMouseRegion(this);
}
}
class _RawMouseRegion extends SingleChildRenderObjectWidget {
_RawMouseRegion(this.owner) : super(child: owner.widget.child);
final _MouseRegionState owner;
@override
RenderMouseRegion createRenderObject(BuildContext context) {
final MouseRegion widget = owner.widget;
return RenderMouseRegion(
onEnter: widget.onEnter,
onHover: widget.onHover,
onExit: owner.getHandleExit(),
opaque: widget.opaque,
);
}
@override
void updateRenderObject(BuildContext context, RenderMouseRegion renderObject) {
final MouseRegion widget = owner.widget;
renderObject
..onEnter = widget.onEnter
..onHover = widget.onHover
..onExit = owner.getHandleExit()
..opaque = widget.opaque;
}
}

View file

@ -85,7 +85,6 @@ void main() {
yield annotation;
},
);
_mouseTracker.attachAnnotation(annotation);
return annotation;
}
@ -102,7 +101,7 @@ void main() {
);
expect(
annotation1.toString(),
equals('MouseTrackerAnnotation#${shortHash(annotation1)}(callbacks: enter hover exit)'),
equals('MouseTrackerAnnotation#${shortHash(annotation1)}(callbacks: [enter, hover, exit])'),
);
const MouseTrackerAnnotation annotation2 = MouseTrackerAnnotation();
@ -249,73 +248,6 @@ void main() {
events.clear();
});
test('should not flip out when attaching and detaching during callbacks', () {
// It is a common pattern that a callback that listens to the changes of
// [MouseTracker.mouseIsConnected] triggers annotation attaching and
// detaching. This test ensures that no exceptions are thrown for this
// pattern.
bool isInHitRegion = false;
final List<PointerEvent> events = <PointerEvent>[];
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
onEnter: (PointerEnterEvent event) => events.add(event),
onHover: (PointerHoverEvent event) => events.add(event),
onExit: (PointerExitEvent event) => events.add(event),
);
_setUpMouseAnnotationFinder((Offset position) sync* {
if (isInHitRegion) {
yield annotation;
}
});
void mockMarkNeedsPaint() {
_binding.scheduleMouseTrackerPostFrameCheck();
}
final VoidCallback firstListener = () {
if (!_mouseTracker.mouseIsConnected) {
_mouseTracker.detachAnnotation(annotation);
isInHitRegion = false;
} else {
_mouseTracker.attachAnnotation(annotation);
isInHitRegion = true;
}
mockMarkNeedsPaint();
};
_mouseTracker.addListener(firstListener);
// The pointer is added onto the annotation, triggering attaching callback.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(1.0, 0.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
_binding.flushPostFrameCallbacks(Duration.zero);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerEnterEvent(position: Offset(1.0, 0.0)),
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
events.clear();
// The pointer is removed while on the annotation, triggering dettaching callback.
_mouseTracker.removeListener(firstListener);
_mouseTracker.addListener(() {
if (!_mouseTracker.mouseIsConnected) {
_mouseTracker.detachAnnotation(annotation);
isInHitRegion = false;
}
});
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(1.0, 0.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerExitEvent(position: Offset(1.0, 0.0)),
]));
expect(_mouseTracker.mouseIsConnected, isFalse);
events.clear();
});
test('should not handle non-hover events', () {
final List<PointerEvent> events = <PointerEvent>[];
_setUpWithOneAnnotation(logEvents: events);
@ -346,7 +278,7 @@ void main() {
events.clear();
});
test('should correctly handle when the annotation is attached or detached on the pointer', () {
test('should correctly handle when the annotation appears or disappears on the pointer', () {
bool isInHitRegion;
final List<Object> events = <PointerEvent>[];
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
@ -371,13 +303,8 @@ void main() {
expect(_mouseTracker.mouseIsConnected, isTrue);
events.clear();
// Attaching an annotation should trigger Enter event.
// Adding an annotation should trigger Enter event.
isInHitRegion = true;
_mouseTracker.attachAnnotation(annotation);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
expect(_binding.postFrameCallbacks, hasLength(0));
_binding.scheduleMouseTrackerPostFrameCheck();
expect(_binding.postFrameCallbacks, hasLength(1));
@ -387,18 +314,14 @@ void main() {
]));
events.clear();
// Detaching an annotation should not trigger events.
// Removing an annotation should trigger events.
isInHitRegion = false;
_mouseTracker.detachAnnotation(annotation);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
expect(_binding.postFrameCallbacks, hasLength(0));
_binding.scheduleMouseTrackerPostFrameCheck();
expect(_binding.postFrameCallbacks, hasLength(1));
_binding.flushPostFrameCallbacks(Duration.zero);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
const PointerExitEvent(position: Offset(0.0, 100.0)),
]));
expect(_binding.postFrameCallbacks, hasLength(0));
});
@ -417,8 +340,6 @@ void main() {
}
});
// Start with an annotation attached.
_mouseTracker.attachAnnotation(annotation);
isInHitRegion = false;
// Connect a mouse.
@ -468,8 +389,6 @@ void main() {
}
});
// Start with an annotation attached.
_mouseTracker.attachAnnotation(annotation);
isInHitRegion = false;
// Connect a mouse in the region. Should trigger Enter.
@ -508,8 +427,6 @@ void main() {
}
});
// Start with annotation and mouse attached.
_mouseTracker.attachAnnotation(annotation);
isInHitRegion = false;
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(200.0, 100.0)),
@ -541,75 +458,17 @@ void main() {
]));
});
test('should correctly handle when annotation is attached or detached while not containing the pointer', () {
final List<PointerEvent> events = <PointerEvent>[];
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
onEnter: (PointerEnterEvent event) => events.add(event),
onHover: (PointerHoverEvent event) => events.add(event),
onExit: (PointerExitEvent event) => events.add(event),
);
_setUpMouseAnnotationFinder((Offset position) sync* {
// This annotation is never in the region.
});
// Connect a mouse when there is no annotation.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 100.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
expect(_mouseTracker.mouseIsConnected, isTrue);
events.clear();
// Attaching an annotation should not trigger events.
_mouseTracker.attachAnnotation(annotation);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
expect(_binding.postFrameCallbacks, hasLength(0));
_binding.scheduleMouseTrackerPostFrameCheck();
expect(_binding.postFrameCallbacks, hasLength(1));
_binding.flushPostFrameCallbacks(Duration.zero);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
events.clear();
// Detaching an annotation should not trigger events.
_mouseTracker.detachAnnotation(annotation);
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
expect(_binding.postFrameCallbacks, hasLength(0));
_binding.scheduleMouseTrackerPostFrameCheck();
expect(_binding.postFrameCallbacks, hasLength(1));
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.remove, const Offset(0.0, 100.0)),
]));
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
]));
});
test('should not schedule postframe callbacks when no mouse is connected', () {
const MouseTrackerAnnotation annotation = MouseTrackerAnnotation();
_setUpMouseAnnotationFinder((Offset position) sync* {
});
// This device only supports touching
// Connect a touch device, which should not be recognized by MouseTracker
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 100.0), kind: PointerDeviceKind.touch),
]));
expect(_mouseTracker.mouseIsConnected, isFalse);
// Attaching an annotation just in case
_mouseTracker.attachAnnotation(annotation);
expect(_binding.postFrameCallbacks, hasLength(0));
_binding.scheduleMouseTrackerPostFrameCheck();
expect(_binding.postFrameCallbacks, hasLength(0));
_mouseTracker.detachAnnotation(annotation);
});
test('should not flip out if not all mouse events are listened to', () {
@ -628,60 +487,15 @@ void main() {
yield annotation2;
});
final ui.PointerDataPacket packet = ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 101.0)),
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
]);
isInHitRegionOne = false;
isInHitRegionTwo = true;
_mouseTracker.attachAnnotation(annotation2);
ui.window.onPointerDataPacket(packet);
_mouseTracker.detachAnnotation(annotation2);
isInHitRegionTwo = false;
// Passes if no errors are thrown.
});
test('should not call annotationFinder when no annotations are attached', () {
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
onEnter: (PointerEnterEvent event) {},
);
int finderCalled = 0;
_setUpMouseAnnotationFinder((Offset position) sync* {
finderCalled++;
// This annotation is never in the region.
});
// When no annotations are attached, hovering should not call finder.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.add, const Offset(0.0, 101.0)),
_pointerData(PointerChange.hover, const Offset(1.0, 101.0)),
]));
expect(finderCalled, 0);
// Attaching should not call finder.
_mouseTracker.attachAnnotation(annotation);
_binding.flushPostFrameCallbacks(Duration.zero);
expect(finderCalled, 0);
// When annotations are attached, hovering should call finder.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(0.0, 201.0)),
]));
expect(finderCalled, 1);
finderCalled = 0;
// Detaching an annotation should not call finder.
_mouseTracker.detachAnnotation(annotation);
_binding.flushPostFrameCallbacks(Duration.zero);
expect(finderCalled, 0);
// When all annotations are detached, hovering should not call finder.
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
_pointerData(PointerChange.hover, const Offset(0.0, 201.0)),
]));
expect(finderCalled, 0);
// Passes if no errors are thrown.
});
test('should trigger callbacks between parents and children in correct order', () {
@ -713,8 +527,6 @@ void main() {
yield annotationA;
}
});
_mouseTracker.attachAnnotation(annotationA);
_mouseTracker.attachAnnotation(annotationB);
// Starts out of A.
isInB = false;
@ -768,8 +580,6 @@ void main() {
yield annotationB;
}
});
_mouseTracker.attachAnnotation(annotationA);
_mouseTracker.attachAnnotation(annotationB);
// Starts within A.
isInA = true;
@ -868,13 +678,13 @@ class _EventCriticalFieldsMatcher extends Matcher {
return mismatchDescription
.add('is ')
.addDescriptionOf(item.runtimeType)
.add(' and doesn\'t match ')
.add(" and doesn't match ")
.addDescriptionOf(_expected.runtimeType);
}
return mismatchDescription
.add('has ')
.addDescriptionOf(matchState['actual'])
.add(' at field `${matchState['field']}`, which doesn\'t match the expected ')
.add(" at field `${matchState['field']}`, which doesn't match the expected ")
.addDescriptionOf(matchState['expected']);
}
}
@ -940,7 +750,7 @@ class _EventListCriticalFieldsMatcher extends Matcher {
mismatchDescription
.add('has\n ')
.addDescriptionOf(matchState['actual'])
.add('\nat index ${matchState['index']}, which doesn\'t match\n ')
.add("\nat index ${matchState['index']}, which doesn't match\n ")
.addDescriptionOf(matchState['expected'])
.add('\nsince it ');
final Description subDescription = StringDescription();

View file

@ -467,6 +467,20 @@ void main() {
// transform -> clip
_testFittedBoxWithClipRectLayer();
});
test('RenderMouseRegion can change properties when detached', () {
renderer.initMouseTracker(MouseTracker(
renderer.pointerRouter,
(_) => <MouseTrackerAnnotation>[],
));
final RenderMouseRegion object = RenderMouseRegion();
object
..opaque = false
..onEnter = (_) {}
..onExit = (_) {}
..onHover = (_) {};
// Passes if no error is thrown
});
}
class _TestRectClipper extends CustomClipper<Rect> {

View file

@ -162,7 +162,6 @@ void main() {
),
),
);
final RenderMouseRegion renderListener = tester.renderObject(find.byType(MouseRegion));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(400.0, 300.0));
addTearDown(gesture.removePointer);
@ -178,7 +177,6 @@ void main() {
),
));
expect(exit, isNull);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener.hoverAnnotation), isFalse);
});
testWidgets('Hover works with nested listeners', (WidgetTester tester) async {
final UniqueKey key1 = UniqueKey();
@ -229,9 +227,6 @@ void main() {
],
),
);
final List<RenderObject> listeners = tester.renderObjectList(find.byType(MouseRegion)).toList();
final RenderMouseRegion renderListener1 = listeners[0] as RenderMouseRegion;
final RenderMouseRegion renderListener2 = listeners[1] as RenderMouseRegion;
Offset center = tester.getCenter(find.byKey(key2));
await gesture.moveTo(center);
await tester.pump();
@ -243,8 +238,6 @@ void main() {
expect(enter1, isNotEmpty);
expect(enter1.last.position, equals(center));
expect(exit1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists();
// Now make sure that exiting the child only triggers the child exit, not
@ -259,8 +252,6 @@ void main() {
expect(move1.last.position, equals(center));
expect(enter1, isEmpty);
expect(exit1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists();
});
testWidgets('Hover transfers between two listeners', (WidgetTester tester) async {
@ -314,9 +305,6 @@ void main() {
],
),
);
final List<RenderObject> listeners = tester.renderObjectList(find.byType(MouseRegion)).toList();
final RenderMouseRegion renderListener1 = listeners[0] as RenderMouseRegion;
final RenderMouseRegion renderListener2 = listeners[1] as RenderMouseRegion;
final Offset center1 = tester.getCenter(find.byKey(key1));
final Offset center2 = tester.getCenter(find.byKey(key2));
await gesture.moveTo(center1);
@ -329,8 +317,6 @@ void main() {
expect(move2, isEmpty);
expect(enter2, isEmpty);
expect(exit2, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists();
await gesture.moveTo(center2);
await tester.pump();
@ -343,8 +329,6 @@ void main() {
expect(enter2, isNotEmpty);
expect(enter2.last.position, equals(center2));
expect(exit2, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists();
await gesture.moveTo(const Offset(400.0, 450.0));
await tester.pump();
@ -355,8 +339,6 @@ void main() {
expect(enter2, isEmpty);
expect(exit2, isNotEmpty);
expect(exit2.last.position, equals(const Offset(400.0, 450.0)));
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists();
await tester.pumpWidget(Container());
expect(move1, isEmpty);
@ -365,8 +347,6 @@ void main() {
expect(move2, isEmpty);
expect(enter2, isEmpty);
expect(exit2, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isFalse);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isFalse);
});
testWidgets('needsCompositing set when parent class needsCompositing is set', (WidgetTester tester) async {

View file

@ -261,7 +261,6 @@ void main() {
onExit: (PointerExitEvent details) => exit = details,
),
));
final RenderMouseRegion renderListener = tester.renderObject(find.byType(MouseRegion));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: Offset.zero);
addTearDown(gesture.removePointer);
@ -279,7 +278,6 @@ void main() {
expect(enter, isNull);
expect(move, isNull);
expect(exit, isNull);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener.hoverAnnotation), isFalse);
});
testWidgets('triggers pointer enter when widget moves in', (WidgetTester tester) async {
@ -415,8 +413,6 @@ void main() {
],
),
);
final RenderMouseRegion renderListener1 = tester.renderObject(find.byKey(key1));
final RenderMouseRegion renderListener2 = tester.renderObject(find.byKey(key2));
Offset center = tester.getCenter(find.byKey(key2));
await gesture.moveTo(center);
await tester.pump();
@ -428,8 +424,6 @@ void main() {
expect(enter1, isNotEmpty);
expect(enter1.last.position, equals(center));
expect(exit1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists();
// Now make sure that exiting the child only triggers the child exit, not
@ -444,8 +438,6 @@ void main() {
expect(move1.last.position, equals(center));
expect(enter1, isEmpty);
expect(exit1, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists();
});
@ -500,8 +492,6 @@ void main() {
],
),
);
final RenderMouseRegion renderListener1 = tester.renderObject(find.byKey(key1));
final RenderMouseRegion renderListener2 = tester.renderObject(find.byKey(key2));
final Offset center1 = tester.getCenter(find.byKey(key1));
final Offset center2 = tester.getCenter(find.byKey(key2));
await gesture.moveTo(center1);
@ -514,8 +504,6 @@ void main() {
expect(move2, isEmpty);
expect(enter2, isEmpty);
expect(exit2, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists();
await gesture.moveTo(center2);
await tester.pump();
@ -528,8 +516,6 @@ void main() {
expect(enter2, isNotEmpty);
expect(enter2.last.position, equals(center2));
expect(exit2, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists();
await gesture.moveTo(const Offset(400.0, 450.0));
await tester.pump();
@ -540,8 +526,6 @@ void main() {
expect(enter2, isEmpty);
expect(exit2, isNotEmpty);
expect(exit2.last.position, equals(const Offset(400.0, 450.0)));
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
clearLists();
await tester.pumpWidget(Container());
expect(move1, isEmpty);
@ -550,8 +534,6 @@ void main() {
expect(move2, isEmpty);
expect(enter2, isEmpty);
expect(exit2, isEmpty);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isFalse);
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isFalse);
});
testWidgets('MouseRegion uses updated callbacks', (WidgetTester tester) async {
@ -713,8 +695,8 @@ void main() {
child: const MouseRegion(opaque: false),
),
);
final RenderMouseRegion listener = tester.renderObject(find.byType(MouseRegion));
expect(listener.needsCompositing, isFalse);
final RenderMouseRegion mouseRegion = tester.renderObject(find.byType(MouseRegion));
expect(mouseRegion.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
@ -731,7 +713,7 @@ void main() {
),
),
);
expect(listener.needsCompositing, isTrue);
expect(mouseRegion.needsCompositing, isTrue);
// Compositing is required, therefore a dedicated TransformLayer for
// `Transform.scale` is added.
expect(tester.layers.whereType<TransformLayer>(), hasLength(2));
@ -742,7 +724,7 @@ void main() {
child: const MouseRegion(opaque: false),
),
);
expect(listener.needsCompositing, isFalse);
expect(mouseRegion.needsCompositing, isFalse);
// TransformLayer for `Transform.scale` is removed again as transform is
// executed directly on the canvas.
expect(tester.layers.whereType<TransformLayer>(), hasLength(1));
@ -756,7 +738,7 @@ void main() {
),
),
);
expect(listener.needsCompositing, isTrue);
expect(mouseRegion.needsCompositing, isTrue);
// Compositing is required, therefore a dedicated TransformLayer for
// `Transform.scale` is added.
expect(tester.layers.whereType<TransformLayer>(), hasLength(2));
@ -767,20 +749,20 @@ void main() {
addTearDown(gesture.removePointer);
await gesture.addPointer(location: Offset.zero);
int numEntries = 0;
int numEntrances = 0;
int numExits = 0;
await tester.pumpWidget(
Center(
child: HoverFeedback(
onEnter: () => numEntries++,
onExit: () => numExits++,
onEnter: () { numEntrances += 1; },
onExit: () { numExits += 1; },
)),
);
await gesture.moveTo(tester.getCenter(find.byType(Text)));
await tester.pumpAndSettle();
expect(numEntries, equals(1));
expect(numEntrances, equals(1));
expect(numExits, equals(0));
expect(find.text('HOVERING'), findsOneWidget);
@ -788,18 +770,18 @@ void main() {
Container(),
);
await tester.pump();
expect(numEntries, equals(1));
expect(numEntrances, equals(1));
expect(numExits, equals(0));
await tester.pumpWidget(
Center(
child: HoverFeedback(
onEnter: () => numEntries++,
onExit: () => numExits++,
onEnter: () { numEntrances += 1; },
onExit: () { numExits += 1; },
)),
);
await tester.pump();
expect(numEntries, equals(2));
expect(numEntrances, equals(2));
expect(numExits, equals(0));
});
@ -809,41 +791,43 @@ void main() {
await gesture.addPointer();
addTearDown(gesture.removePointer);
int numEntries = 0;
int numEntrances = 0;
int numExits = 0;
await tester.pumpWidget(
Center(
child: HoverFeedback(
key: feedbackKey,
onEnter: () => numEntries++,
onExit: () => numExits++,
onEnter: () { numEntrances += 1; },
onExit: () { numExits += 1; },
)),
);
await gesture.moveTo(tester.getCenter(find.byType(Text)));
await tester.pumpAndSettle();
expect(numEntries, equals(1));
expect(numEntrances, equals(1));
expect(numExits, equals(0));
expect(find.text('HOVERING'), findsOneWidget);
await tester.pumpWidget(
Center(
child: Container(
child: HoverFeedback(
key: feedbackKey,
onEnter: () => numEntries++,
onExit: () => numExits++,
))),
child: Container(
child: HoverFeedback(
key: feedbackKey,
onEnter: () { numEntrances += 1; },
onExit: () { numExits += 1; },
),
),
),
);
await tester.pump();
expect(numEntries, equals(1));
expect(numEntrances, equals(1));
expect(numExits, equals(0));
await tester.pumpWidget(
Container(),
);
await tester.pump();
expect(numEntries, equals(1));
expect(numEntrances, equals(1));
expect(numExits, equals(0));
});
@ -920,8 +904,8 @@ void main() {
textDirection: TextDirection.ltr,
child: MouseRegion(
onEnter: (PointerEnterEvent e) {},
child: _PaintDelegateWidget(
onPaint: _VoidDelegate(() => paintCount++),
child: CustomPaint(
painter: _DelegatedPainter(onPaint: () { paintCount += 1; }),
child: const Text('123'),
),
),
@ -943,8 +927,8 @@ void main() {
textDirection: TextDirection.ltr,
child: MouseRegion(
onEnter: (PointerEnterEvent e) {},
child: _PaintDelegateWidget(
onPaint: _VoidDelegate(() => paintCount++),
child: CustomPaint(
painter: _DelegatedPainter(onPaint: () { paintCount += 1; }),
child: const Text('123'),
),
),
@ -1299,12 +1283,12 @@ void main() {
Align(
alignment: Alignment.topLeft,
child: MouseRegion(
onEnter: (_) { bottomRegionIsHovered = true; },
onHover: (_) { bottomRegionIsHovered = true; },
onExit: (_) { bottomRegionIsHovered = true; },
child: Container(
width: 10,
height: 10,
onEnter: (_) { bottomRegionIsHovered = true; },
onHover: (_) { bottomRegionIsHovered = true; },
onExit: (_) { bottomRegionIsHovered = true; },
child: Container(
width: 10,
height: 10,
),
),
),
@ -1325,7 +1309,118 @@ void main() {
expect(bottomRegionIsHovered, isFalse);
});
testWidgets('RenderMouseRegion\'s debugFillProperties when default', (WidgetTester tester) async {
testWidgets("Changing MouseRegion's callbacks is effective and doesn't repaint", (WidgetTester tester) async {
final List<String> logs = <String>[];
const Key key = ValueKey<int>(1);
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(20, 20));
addTearDown(gesture.removePointer);
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
onEnter: (_) { logs.add('enter1'); },
onHover: (_) { logs.add('hover1'); },
onExit: (_) { logs.add('exit1'); },
child: CustomPaint(
painter: _DelegatedPainter(onPaint: () { logs.add('paint'); }, key: key),
),
),
),
));
expect(logs, <String>['paint']);
logs.clear();
await gesture.moveTo(const Offset(5, 5));
expect(logs, <String>['enter1', 'hover1']);
logs.clear();
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
onEnter: (_) { logs.add('enter2'); },
onHover: (_) { logs.add('hover2'); },
onExit: (_) { logs.add('exit2'); },
child: CustomPaint(
painter: _DelegatedPainter(onPaint: () { logs.add('paint'); }, key: key),
),
),
),
));
expect(logs, isEmpty);
await gesture.moveTo(const Offset(6, 6));
expect(logs, <String>['hover2']);
logs.clear();
// Compare: It repaints if the MouseRegion is unactivated.
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
opaque: false,
child: CustomPaint(
painter: _DelegatedPainter(onPaint: () { logs.add('paint'); }, key: key),
),
),
),
));
expect(logs, <String>['paint']);
});
testWidgets('Changing MouseRegion.opaque is effective and repaints', (WidgetTester tester) async {
final List<String> logs = <String>[];
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer(location: const Offset(5, 5));
addTearDown(gesture.removePointer);
final PointerHoverEventListener onHover = (_) {};
final VoidCallback onPaintChild = () { logs.add('paint'); };
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
opaque: true,
// Dummy callback so that MouseRegion stays affective after opaque
// turns false.
onHover: onHover,
child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)),
),
),
background: MouseRegion(onEnter: (_) { logs.add('hover-enter'); })
));
expect(logs, <String>['paint']);
logs.clear();
expect(logs, isEmpty);
logs.clear();
await tester.pumpWidget(_Scaffold(
topLeft: Container(
height: 10,
width: 10,
child: MouseRegion(
opaque: false,
onHover: onHover,
child: CustomPaint(painter: _DelegatedPainter(onPaint: onPaintChild)),
),
),
background: MouseRegion(onEnter: (_) { logs.add('hover-enter'); })
));
expect(logs, <String>['paint', 'hover-enter']);
});
testWidgets("RenderMouseRegion's debugFillProperties when default", (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
RenderMouseRegion().debugFillProperties(builder);
@ -1339,7 +1434,7 @@ void main() {
]);
});
testWidgets('RenderMouseRegion\'s debugFillProperties when full', (WidgetTester tester) async {
testWidgets("RenderMouseRegion's debugFillProperties when full", (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
RenderMouseRegion(
onEnter: (PointerEnterEvent event) {},
@ -1377,64 +1472,46 @@ void main() {
await gesture.moveBy(const Offset(10.0, 10.0));
expect(tester.binding.hasScheduledFrame, isFalse);
});
}
testWidgets("MouseTracker's attachAnnotation doesn't schedule any frames", (WidgetTester tester) async {
// This test is here because MouseTracker can't use testWidgets.
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
onEnter: (PointerEnterEvent event) {},
onHover: (PointerHoverEvent event) {},
onExit: (PointerExitEvent event) {},
// Render widget `topLeft` at the top-left corner, stacking on top of the widget
// `background`.
class _Scaffold extends StatelessWidget {
const _Scaffold({this.topLeft, this.background});
final Widget topLeft;
final Widget background;
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Stack(
children: <Widget>[
if (background != null) background,
Align(
alignment: Alignment.topLeft,
child: topLeft,
),
],
),
);
RendererBinding.instance.mouseTracker.attachAnnotation(annotation);
expect(tester.binding.hasScheduledFrame, isFalse);
expect(RendererBinding.instance.mouseTracker.isAnnotationAttached(annotation), isTrue);
RendererBinding.instance.mouseTracker.detachAnnotation(annotation);
});
}
// This widget allows you to send a callback that is called during `onPaint`.
@immutable
class _PaintDelegateWidget extends SingleChildRenderObjectWidget {
const _PaintDelegateWidget({
Key key,
Widget child,
this.onPaint,
}) : super(key: key, child: child);
final _VoidDelegate onPaint;
@override
RenderObject createRenderObject(BuildContext context) {
return _PaintCallbackObject(onPaint: onPaint?.callback);
}
@override
void updateRenderObject(BuildContext context, _PaintCallbackObject renderObject) {
renderObject..onPaint = onPaint?.callback;
}
}
class _VoidDelegate {
_VoidDelegate(this.callback);
void Function() callback;
}
class _PaintCallbackObject extends RenderProxyBox {
_PaintCallbackObject({
RenderBox child,
this.onPaint,
}) : super(child);
void Function() onPaint;
class _DelegatedPainter extends CustomPainter {
_DelegatedPainter({this.key, this.onPaint});
final Key key;
final VoidCallback onPaint;
@override
void paint(PaintingContext context, Offset offset) {
if (onPaint != null) {
onPaint();
}
super.paint(context, offset);
void paint(Canvas canvas, Size size) {
onPaint();
}
@override
bool shouldRepaint(CustomPainter oldDelegate) =>
!(oldDelegate is _DelegatedPainter && key == oldDelegate.key);
}
class _HoverClientWithClosures extends StatefulWidget {