MouseTracker no longer requires annotations attached (#48453)
This commit is contained in:
parent
dd98046fe5
commit
da0a7d8b2e
@ -30,7 +30,7 @@ typedef PointerHoverEventListener = void Function(PointerHoverEvent event);
|
|||||||
/// movements.
|
/// movements.
|
||||||
///
|
///
|
||||||
/// This is added to a layer and managed by the [MouseRegion] widget.
|
/// 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
|
/// Creates an annotation that can be used to find layers interested in mouse
|
||||||
/// movements.
|
/// movements.
|
||||||
const MouseTrackerAnnotation({this.onEnter, this.onHover, this.onExit});
|
const MouseTrackerAnnotation({this.onEnter, this.onHover, this.onExit});
|
||||||
@ -39,24 +39,13 @@ class MouseTrackerAnnotation {
|
|||||||
/// entered the annotated region.
|
/// entered the annotated region.
|
||||||
///
|
///
|
||||||
/// This callback is triggered when the pointer has started to be contained
|
/// This callback is triggered when the pointer has started to be contained
|
||||||
/// by the annotationed region for any reason.
|
/// by the annotationed region for any reason, which means it always matches a
|
||||||
///
|
/// later [onExit].
|
||||||
/// 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].
|
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [MouseRegion.onEnter], which uses this callback.
|
|
||||||
/// * [onExit], which is triggered when a mouse pointer exits the region.
|
/// * [onExit], which is triggered when a mouse pointer exits the region.
|
||||||
|
/// * [MouseRegion.onEnter], which uses this callback.
|
||||||
final PointerEnterEventListener onEnter;
|
final PointerEnterEventListener onEnter;
|
||||||
|
|
||||||
/// Triggered when a pointer has moved within the annotated region without
|
/// 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
|
/// * A pointer has moved onto, or moved within an annotation without buttons
|
||||||
/// pressed.
|
/// pressed.
|
||||||
///
|
///
|
||||||
/// This callback is not triggered when
|
/// This callback is not triggered when:
|
||||||
///
|
///
|
||||||
/// * An annotation that is containing the pointer has moved, and still
|
/// * An annotation that is containing the pointer has moved, and still
|
||||||
/// contains the pointer.
|
/// contains the pointer.
|
||||||
@ -78,59 +67,30 @@ class MouseTrackerAnnotation {
|
|||||||
/// Triggered when a mouse pointer, with or without buttons pressed, has
|
/// Triggered when a mouse pointer, with or without buttons pressed, has
|
||||||
/// exited the annotated region when the annotated region still exists.
|
/// exited the annotated region when the annotated region still exists.
|
||||||
///
|
///
|
||||||
/// This callback is triggered when the pointer has stopped to be contained
|
/// This callback is triggered when the pointer has stopped being contained
|
||||||
/// by the region, except when it's caused by the removal of the render object
|
/// by the region for any reason, which means it always matches an earlier
|
||||||
/// 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
|
|
||||||
/// [onEnter].
|
/// [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:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [MouseRegion.onExit], which uses this callback.
|
|
||||||
/// * [onEnter], which is triggered when a mouse pointer enters the region.
|
/// * [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;
|
final PointerExitEventListener onExit;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
final List<String> callbacks = <String>[];
|
super.debugFillProperties(properties);
|
||||||
if (onEnter != null)
|
properties.add(FlagsSummary<Function>(
|
||||||
callbacks.add('enter');
|
'callbacks',
|
||||||
if (onHover != null)
|
<String, Function> {
|
||||||
callbacks.add('hover');
|
'enter': onEnter,
|
||||||
if (onExit != null)
|
'hover': onHover,
|
||||||
callbacks.add('exit');
|
'exit': onExit,
|
||||||
final String describeCallbacks = callbacks.isEmpty
|
},
|
||||||
? '<none>'
|
ifEmpty: '<none>',
|
||||||
: callbacks.join(' ');
|
));
|
||||||
return '${describeIdentity(this)}(callbacks: $describeCallbacks)';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,11 +154,9 @@ class _MouseState {
|
|||||||
///
|
///
|
||||||
/// ### Details
|
/// ### Details
|
||||||
///
|
///
|
||||||
/// The state of [MouseTracker] consists of 3 parts:
|
/// The state of [MouseTracker] consists of two parts:
|
||||||
///
|
///
|
||||||
/// * The mouse devices that are connected.
|
/// * 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.
|
/// * In which annotations each device is contained.
|
||||||
///
|
///
|
||||||
/// The states remain stable most of the time, and are only changed at the
|
/// 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.
|
// mouse events from.
|
||||||
final PointerRouter _router;
|
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.
|
// Tracks the state of connected mouse devices.
|
||||||
//
|
//
|
||||||
// It is the source of truth for the list 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,
|
nextAnnotations: mouseState.annotations,
|
||||||
previousEvent: previousEvent,
|
previousEvent: previousEvent,
|
||||||
unhandledEvent: event,
|
unhandledEvent: event,
|
||||||
trackedAnnotations: _trackedAnnotations,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -306,12 +259,12 @@ class MouseTracker extends ChangeNotifier {
|
|||||||
|
|
||||||
// Find the annotations that is hovered by the device of the `state`.
|
// 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
|
// If the device is not connected, an empty set is returned without calling
|
||||||
// is returned without calling `annotationFinder`.
|
// `annotationFinder`.
|
||||||
LinkedHashSet<MouseTrackerAnnotation> _findAnnotations(_MouseState state) {
|
LinkedHashSet<MouseTrackerAnnotation> _findAnnotations(_MouseState state) {
|
||||||
final Offset globalPosition = state.latestEvent.position;
|
final Offset globalPosition = state.latestEvent.position;
|
||||||
final int device = state.device;
|
final int device = state.device;
|
||||||
return (_mouseStates.containsKey(device) && _trackedAnnotations.isNotEmpty)
|
return (_mouseStates.containsKey(device))
|
||||||
? LinkedHashSet<MouseTrackerAnnotation>.from(annotationFinder(globalPosition))
|
? LinkedHashSet<MouseTrackerAnnotation>.from(annotationFinder(globalPosition))
|
||||||
: <MouseTrackerAnnotation>{} as LinkedHashSet<MouseTrackerAnnotation>;
|
: <MouseTrackerAnnotation>{} as LinkedHashSet<MouseTrackerAnnotation>;
|
||||||
}
|
}
|
||||||
@ -332,7 +285,6 @@ class MouseTracker extends ChangeNotifier {
|
|||||||
nextAnnotations: mouseState.annotations,
|
nextAnnotations: mouseState.annotations,
|
||||||
previousEvent: mouseState.latestEvent,
|
previousEvent: mouseState.latestEvent,
|
||||||
unhandledEvent: null,
|
unhandledEvent: null,
|
||||||
trackedAnnotations: _trackedAnnotations,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -428,16 +380,15 @@ class MouseTracker extends ChangeNotifier {
|
|||||||
// null, which means the update is triggered by a new event.
|
// null, which means the update is triggered by a new event.
|
||||||
// The `unhandledEvent` can be null, which means the update is not triggered
|
// The `unhandledEvent` can be null, which means the update is not triggered
|
||||||
// by an event.
|
// by an event.
|
||||||
|
// However, one of `previousEvent` or `unhandledEvent` must not be null.
|
||||||
static void _dispatchDeviceCallbacks({
|
static void _dispatchDeviceCallbacks({
|
||||||
@required LinkedHashSet<MouseTrackerAnnotation> lastAnnotations,
|
@required LinkedHashSet<MouseTrackerAnnotation> lastAnnotations,
|
||||||
@required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations,
|
@required LinkedHashSet<MouseTrackerAnnotation> nextAnnotations,
|
||||||
@required PointerEvent previousEvent,
|
@required PointerEvent previousEvent,
|
||||||
@required PointerEvent unhandledEvent,
|
@required PointerEvent unhandledEvent,
|
||||||
@required Set<MouseTrackerAnnotation> trackedAnnotations,
|
|
||||||
}) {
|
}) {
|
||||||
assert(lastAnnotations != null);
|
assert(lastAnnotations != null);
|
||||||
assert(nextAnnotations != null);
|
assert(nextAnnotations != null);
|
||||||
assert(trackedAnnotations != null);
|
|
||||||
final PointerEvent latestEvent = unhandledEvent ?? previousEvent;
|
final PointerEvent latestEvent = unhandledEvent ?? previousEvent;
|
||||||
assert(latestEvent != null);
|
assert(latestEvent != null);
|
||||||
// Order is important for mouse event callbacks. The `findAnnotations`
|
// Order is important for mouse event callbacks. The `findAnnotations`
|
||||||
@ -446,49 +397,45 @@ class MouseTracker extends ChangeNotifier {
|
|||||||
// The algorithm here is explained in
|
// The algorithm here is explained in
|
||||||
// https://github.com/flutter/flutter/issues/41420
|
// https://github.com/flutter/flutter/issues/41420
|
||||||
|
|
||||||
// Send exit events in visual order.
|
// Send exit events to annotations that are in last but not in next, in
|
||||||
final Iterable<MouseTrackerAnnotation> exitingAnnotations =
|
// visual order.
|
||||||
lastAnnotations.difference(nextAnnotations);
|
final Iterable<MouseTrackerAnnotation> exitingAnnotations = lastAnnotations.where(
|
||||||
|
(MouseTrackerAnnotation value) => !nextAnnotations.contains(value),
|
||||||
|
);
|
||||||
for (final MouseTrackerAnnotation annotation in exitingAnnotations) {
|
for (final MouseTrackerAnnotation annotation in exitingAnnotations) {
|
||||||
final bool attached = trackedAnnotations.contains(annotation);
|
if (annotation.onExit != null) {
|
||||||
// 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) {
|
|
||||||
annotation.onExit(PointerExitEvent.fromMouseEvent(latestEvent));
|
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 =
|
final Iterable<MouseTrackerAnnotation> enteringAnnotations =
|
||||||
nextAnnotations.difference(lastAnnotations).toList().reversed;
|
nextAnnotations.difference(lastAnnotations).toList().reversed;
|
||||||
for (final MouseTrackerAnnotation annotation in enteringAnnotations) {
|
for (final MouseTrackerAnnotation annotation in enteringAnnotations) {
|
||||||
assert(trackedAnnotations.contains(annotation));
|
|
||||||
if (annotation.onEnter != null) {
|
if (annotation.onEnter != null) {
|
||||||
annotation.onEnter(PointerEnterEvent.fromMouseEvent(latestEvent));
|
annotation.onEnter(PointerEnterEvent.fromMouseEvent(latestEvent));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send hover events in reverse visual order.
|
// Send hover events to annotations that are in next, in reverse visual
|
||||||
// For now the order between the hover events is designed this way for no
|
// order. The reverse visual order is chosen only because of the simplicity
|
||||||
// solid reasons but to keep it aligned with enter events for simplicity.
|
// by keeping the hover events aligned with enter events.
|
||||||
if (unhandledEvent is PointerHoverEvent) {
|
if (unhandledEvent is PointerHoverEvent) {
|
||||||
final Iterable<MouseTrackerAnnotation> hoveringAnnotations =
|
|
||||||
nextAnnotations.toList().reversed;
|
|
||||||
final Offset lastHoverPosition = previousEvent is PointerHoverEvent ? previousEvent.position : null;
|
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) {
|
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) {
|
if (annotation.onHover != null) {
|
||||||
annotation.onHover(unhandledEvent);
|
annotation.onHover(unhandledEvent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
bool _hasScheduledPostFrameCheck = false;
|
bool _hasScheduledPostFrameCheck = false;
|
||||||
/// Mark all devices as dirty, and schedule a callback that is executed in the
|
/// Mark all devices as dirty, and schedule a callback that is executed in the
|
||||||
@ -519,63 +466,4 @@ class MouseTracker extends ChangeNotifier {
|
|||||||
|
|
||||||
/// Whether or not a mouse is connected and has produced events.
|
/// Whether or not a mouse is connected and has produced events.
|
||||||
bool get mouseIsConnected => _mouseStates.isNotEmpty;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:flutter/semantics.dart';
|
import 'package:flutter/semantics.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'binding.dart';
|
|
||||||
import 'box.dart';
|
import 'box.dart';
|
||||||
import 'layer.dart';
|
import 'layer.dart';
|
||||||
import 'object.dart';
|
import 'object.dart';
|
||||||
@ -838,13 +837,11 @@ mixin _PlatformViewGestureMixin on RenderBox {
|
|||||||
if (_handlePointerEvent != null)
|
if (_handlePointerEvent != null)
|
||||||
_handlePointerEvent(event);
|
_handlePointerEvent(event);
|
||||||
});
|
});
|
||||||
RendererBinding.instance.mouseTracker.attachAnnotation(_hoverAnnotation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void detach() {
|
void detach() {
|
||||||
_gestureRecognizer.reset();
|
_gestureRecognizer.reset();
|
||||||
RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation);
|
|
||||||
_hoverAnnotation = null;
|
_hoverAnnotation = null;
|
||||||
super.detach();
|
super.detach();
|
||||||
}
|
}
|
||||||
|
@ -2659,6 +2659,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
|
|||||||
/// [RenderMouseRegion].
|
/// [RenderMouseRegion].
|
||||||
class RenderMouseRegion extends RenderProxyBox {
|
class RenderMouseRegion extends RenderProxyBox {
|
||||||
/// Creates a render object that forwards pointer events to callbacks.
|
/// 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({
|
RenderMouseRegion({
|
||||||
PointerEnterEventListener onEnter,
|
PointerEnterEventListener onEnter,
|
||||||
PointerHoverEventListener onHover,
|
PointerHoverEventListener onHover,
|
||||||
@ -2698,17 +2701,23 @@ class RenderMouseRegion extends RenderProxyBox {
|
|||||||
set opaque(bool value) {
|
set opaque(bool value) {
|
||||||
if (_opaque != value) {
|
if (_opaque != value) {
|
||||||
_opaque = value;
|
_opaque = value;
|
||||||
_updateAnnotations();
|
_markPropertyUpdated(mustRepaint: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called when a mouse pointer enters the region (with or without buttons
|
/// Called when a mouse pointer starts being contained by the region (with or
|
||||||
/// pressed).
|
/// 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;
|
PointerEnterEventListener get onEnter => _onEnter;
|
||||||
set onEnter(PointerEnterEventListener value) {
|
set onEnter(PointerEnterEventListener value) {
|
||||||
if (_onEnter != value) {
|
if (_onEnter != value) {
|
||||||
_onEnter = value;
|
_onEnter = value;
|
||||||
_updateAnnotations();
|
_markPropertyUpdated(mustRepaint: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PointerEnterEventListener _onEnter;
|
PointerEnterEventListener _onEnter;
|
||||||
@ -2723,7 +2732,7 @@ class RenderMouseRegion extends RenderProxyBox {
|
|||||||
set onHover(PointerHoverEventListener value) {
|
set onHover(PointerHoverEventListener value) {
|
||||||
if (_onHover != value) {
|
if (_onHover != value) {
|
||||||
_onHover = value;
|
_onHover = value;
|
||||||
_updateAnnotations();
|
_markPropertyUpdated(mustRepaint: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PointerHoverEventListener _onHover;
|
PointerHoverEventListener _onHover;
|
||||||
@ -2732,13 +2741,20 @@ class RenderMouseRegion extends RenderProxyBox {
|
|||||||
_onHover(event);
|
_onHover(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called when a pointer leaves the region (with or without buttons pressed)
|
/// Called when a pointer is no longer contained by the region (with or
|
||||||
/// and the annotation is still attached.
|
/// 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;
|
PointerExitEventListener get onExit => _onExit;
|
||||||
set onExit(PointerExitEventListener value) {
|
set onExit(PointerExitEventListener value) {
|
||||||
if (_onExit != value) {
|
if (_onExit != value) {
|
||||||
_onExit = value;
|
_onExit = value;
|
||||||
_updateAnnotations();
|
_markPropertyUpdated(mustRepaint: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PointerExitEventListener _onExit;
|
PointerExitEventListener _onExit;
|
||||||
@ -2757,64 +2773,52 @@ class RenderMouseRegion extends RenderProxyBox {
|
|||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
MouseTrackerAnnotation get hoverAnnotation => _hoverAnnotation;
|
MouseTrackerAnnotation get hoverAnnotation => _hoverAnnotation;
|
||||||
|
|
||||||
void _updateAnnotations() {
|
// Call this method when a property has changed and might affect the
|
||||||
final bool annotationWasActive = _annotationIsActive;
|
// `_annotationIsActive` bit.
|
||||||
final bool annotationWillBeActive = (
|
//
|
||||||
|
// 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 ||
|
_onEnter != null ||
|
||||||
_onHover != null ||
|
_onHover != null ||
|
||||||
_onExit != null ||
|
_onExit != null ||
|
||||||
opaque
|
opaque
|
||||||
) &&
|
) && RendererBinding.instance.mouseTracker.mouseIsConnected;
|
||||||
RendererBinding.instance.mouseTracker.mouseIsConnected;
|
_setAnnotationIsActive(newAnnotationIsActive);
|
||||||
if (annotationWasActive != annotationWillBeActive) {
|
if (mustRepaint)
|
||||||
|
markNeedsPaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setAnnotationIsActive(bool value) {
|
||||||
|
final bool annotationWasActive = _annotationIsActive;
|
||||||
|
_annotationIsActive = value;
|
||||||
|
if (annotationWasActive != value) {
|
||||||
markNeedsPaint();
|
markNeedsPaint();
|
||||||
markNeedsCompositingBitsUpdate();
|
markNeedsCompositingBitsUpdate();
|
||||||
if (annotationWillBeActive) {
|
|
||||||
RendererBinding.instance.mouseTracker.attachAnnotation(_hoverAnnotation);
|
|
||||||
} else {
|
|
||||||
RendererBinding.instance.mouseTracker.detachAnnotation(_hoverAnnotation);
|
|
||||||
}
|
}
|
||||||
_annotationIsActive = annotationWillBeActive;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleUpdatedMouseIsConnected() {
|
||||||
|
_markPropertyUpdated(mustRepaint: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void attach(PipelineOwner owner) {
|
void attach(PipelineOwner owner) {
|
||||||
super.attach(owner);
|
super.attach(owner);
|
||||||
// Add a listener to listen for changes in mouseIsConnected.
|
// Add a listener to listen for changes in mouseIsConnected.
|
||||||
RendererBinding.instance.mouseTracker.addListener(_updateAnnotations);
|
RendererBinding.instance.mouseTracker.addListener(_handleUpdatedMouseIsConnected);
|
||||||
_updateAnnotations();
|
_markPropertyUpdated(mustRepaint: false);
|
||||||
}
|
|
||||||
|
|
||||||
/// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void detach() {
|
void detach() {
|
||||||
RendererBinding.instance.mouseTracker.removeListener(_updateAnnotations);
|
RendererBinding.instance.mouseTracker.removeListener(_handleUpdatedMouseIsConnected);
|
||||||
super.detach();
|
super.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5854,7 +5854,7 @@ class _PointerListener extends SingleChildRenderObjectWidget {
|
|||||||
///
|
///
|
||||||
/// * [Listener], a similar widget that tracks pointer events when the pointer
|
/// * [Listener], a similar widget that tracks pointer events when the pointer
|
||||||
/// have buttons pressed.
|
/// have buttons pressed.
|
||||||
class MouseRegion extends SingleChildRenderObjectWidget {
|
class MouseRegion extends StatefulWidget {
|
||||||
/// Creates a widget that forwards mouse events to callbacks.
|
/// Creates a widget that forwards mouse events to callbacks.
|
||||||
const MouseRegion({
|
const MouseRegion({
|
||||||
Key key,
|
Key key,
|
||||||
@ -5862,16 +5862,15 @@ class MouseRegion extends SingleChildRenderObjectWidget {
|
|||||||
this.onExit,
|
this.onExit,
|
||||||
this.onHover,
|
this.onHover,
|
||||||
this.opaque = true,
|
this.opaque = true,
|
||||||
Widget child,
|
this.child,
|
||||||
}) : assert(opaque != null),
|
}) : assert(opaque != null),
|
||||||
super(key: key, child: child);
|
super(key: key);
|
||||||
|
|
||||||
/// Called when a mouse pointer, with or without buttons pressed, has
|
/// Called when a mouse pointer has entered this widget.
|
||||||
/// entered this widget.
|
|
||||||
///
|
///
|
||||||
/// This callback is triggered when the pointer has started to be contained
|
/// This callback is triggered when the pointer, with or without buttons
|
||||||
/// by the region of this widget. More specifically, the callback is triggered
|
/// pressed, has started to be contained by the region of this widget. More
|
||||||
/// by the following cases:
|
/// specifically, the callback is triggered by the following cases:
|
||||||
///
|
///
|
||||||
/// * This widget has appeared under a pointer.
|
/// * This widget has appeared under a pointer.
|
||||||
/// * This widget has moved to 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]
|
/// 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
|
/// is unmounted while being hovered by a pointer, the [onExit] of the widget
|
||||||
/// callback will never called, despite the earlier call of [onEnter]. For
|
/// callback will never called. For more details, see [onExit].
|
||||||
/// 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:
|
/// See also:
|
||||||
///
|
///
|
||||||
@ -5890,47 +5894,182 @@ class MouseRegion extends SingleChildRenderObjectWidget {
|
|||||||
/// internally implemented.
|
/// internally implemented.
|
||||||
final PointerEnterEventListener onEnter;
|
final PointerEnterEventListener onEnter;
|
||||||
|
|
||||||
/// Called when a mouse pointer changes position without buttons pressed, and
|
/// Called when a mouse pointer moves within this widget without buttons
|
||||||
/// the new position is within the region defined by this widget.
|
|
||||||
///
|
|
||||||
/// This callback is triggered when:
|
|
||||||
///
|
|
||||||
/// * 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.
|
/// pressed.
|
||||||
///
|
///
|
||||||
/// This callback is not triggered when
|
/// This callback is not triggered when the [MouseRegion] has moved
|
||||||
|
/// while being hovered by the mouse pointer.
|
||||||
///
|
///
|
||||||
/// * An annotation that is containing the pointer has moved, and still
|
/// {@macro flutter.mouseRegion.triggerTime}
|
||||||
/// contains the pointer.
|
|
||||||
final PointerHoverEventListener onHover;
|
final PointerHoverEventListener onHover;
|
||||||
|
|
||||||
/// Called when a mouse pointer, with or without buttons pressed, has exited
|
/// Called when a mouse pointer has exited this widget when the widget is
|
||||||
/// this widget when the widget is still mounted.
|
/// still mounted.
|
||||||
///
|
///
|
||||||
/// This callback is triggered when the pointer has stopped to be contained
|
/// This callback is triggered when the pointer, with or without buttons
|
||||||
/// by the region of this widget, except when it's caused by the removal of
|
/// pressed, has stopped being contained by the region of this widget, except
|
||||||
/// this widget. More specifically, the callback is triggered by
|
/// when the exit is caused by the disappearance of this widget. More
|
||||||
/// the following cases:
|
/// specifically, this callback is triggered by the following cases:
|
||||||
///
|
///
|
||||||
/// * This widget, which used to contain a pointer, has moved away.
|
/// * A pointer that is hovering this widget has moved away.
|
||||||
/// * A pointer that used to be within this widget has been removed.
|
/// * A pointer that is hovering this widget has been removed.
|
||||||
/// * A pointer that used to be within this widget has moved away.
|
/// * 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
|
/// This means that a [MouseRegion.onExit] might not be matched by a
|
||||||
/// [onEnter].
|
/// [MouseRegion.onEnter].
|
||||||
/// {@macro flutter.mouseTracker.onExit}
|
///
|
||||||
|
/// 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:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [onEnter], which is triggered when a mouse pointer enters the region.
|
/// * [onEnter], which is triggered when a mouse pointer enters the region.
|
||||||
/// * [MouseTrackerAnnotation.onExit], which is how this callback is
|
/// * [RenderMouseRegion] and [MouseTrackerAnnotation.onExit], which are how
|
||||||
/// internally implemented.
|
/// this callback is internally implemented, but without the restriction.
|
||||||
final PointerExitEventListener onExit;
|
final PointerExitEventListener onExit;
|
||||||
|
|
||||||
/// Whether this widget should prevent other [MouseRegion]s visually behind it
|
/// Whether this widget should prevent other [MouseRegion]s visually behind it
|
||||||
@ -5949,27 +6088,13 @@ class MouseRegion extends SingleChildRenderObjectWidget {
|
|||||||
/// This defaults to true.
|
/// This defaults to true.
|
||||||
final bool opaque;
|
final bool opaque;
|
||||||
|
|
||||||
@override
|
/// The widget below this widget in the tree.
|
||||||
_MouseRegionElement createElement() => _MouseRegionElement(this);
|
///
|
||||||
|
/// {@macro flutter.widgets.child}
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RenderMouseRegion createRenderObject(BuildContext context) {
|
_MouseRegionState createState() => _MouseRegionState();
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
@ -5986,21 +6111,46 @@ class MouseRegion extends SingleChildRenderObjectWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MouseRegionElement extends SingleChildRenderObjectElement {
|
class _MouseRegionState extends State<MouseRegion> {
|
||||||
_MouseRegionElement(SingleChildRenderObjectWidget widget) : super(widget);
|
void handleExit(PointerExitEvent event) {
|
||||||
|
if (widget.onExit != null && mounted)
|
||||||
|
widget.onExit(event);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
PointerExitEventListener getHandleExit() {
|
||||||
void activate() {
|
return widget.onExit == null ? null : handleExit;
|
||||||
super.activate();
|
|
||||||
final RenderMouseRegion renderMouseRegion = renderObject as RenderMouseRegion;
|
|
||||||
renderMouseRegion.postActivate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void deactivate() {
|
Widget build(BuildContext context) {
|
||||||
final RenderMouseRegion renderMouseRegion = renderObject as RenderMouseRegion;
|
return _RawMouseRegion(this);
|
||||||
renderMouseRegion.preDeactivate();
|
}
|
||||||
super.deactivate();
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +85,6 @@ void main() {
|
|||||||
yield annotation;
|
yield annotation;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
_mouseTracker.attachAnnotation(annotation);
|
|
||||||
return annotation;
|
return annotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +101,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
annotation1.toString(),
|
annotation1.toString(),
|
||||||
equals('MouseTrackerAnnotation#${shortHash(annotation1)}(callbacks: enter hover exit)'),
|
equals('MouseTrackerAnnotation#${shortHash(annotation1)}(callbacks: [enter, hover, exit])'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const MouseTrackerAnnotation annotation2 = MouseTrackerAnnotation();
|
const MouseTrackerAnnotation annotation2 = MouseTrackerAnnotation();
|
||||||
@ -249,73 +248,6 @@ void main() {
|
|||||||
events.clear();
|
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', () {
|
test('should not handle non-hover events', () {
|
||||||
final List<PointerEvent> events = <PointerEvent>[];
|
final List<PointerEvent> events = <PointerEvent>[];
|
||||||
_setUpWithOneAnnotation(logEvents: events);
|
_setUpWithOneAnnotation(logEvents: events);
|
||||||
@ -346,7 +278,7 @@ void main() {
|
|||||||
events.clear();
|
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;
|
bool isInHitRegion;
|
||||||
final List<Object> events = <PointerEvent>[];
|
final List<Object> events = <PointerEvent>[];
|
||||||
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
|
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
|
||||||
@ -371,13 +303,8 @@ void main() {
|
|||||||
expect(_mouseTracker.mouseIsConnected, isTrue);
|
expect(_mouseTracker.mouseIsConnected, isTrue);
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
// Attaching an annotation should trigger Enter event.
|
// Adding an annotation should trigger Enter event.
|
||||||
isInHitRegion = true;
|
isInHitRegion = true;
|
||||||
_mouseTracker.attachAnnotation(annotation);
|
|
||||||
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
|
|
||||||
]));
|
|
||||||
expect(_binding.postFrameCallbacks, hasLength(0));
|
|
||||||
|
|
||||||
_binding.scheduleMouseTrackerPostFrameCheck();
|
_binding.scheduleMouseTrackerPostFrameCheck();
|
||||||
expect(_binding.postFrameCallbacks, hasLength(1));
|
expect(_binding.postFrameCallbacks, hasLength(1));
|
||||||
|
|
||||||
@ -387,18 +314,14 @@ void main() {
|
|||||||
]));
|
]));
|
||||||
events.clear();
|
events.clear();
|
||||||
|
|
||||||
// Detaching an annotation should not trigger events.
|
// Removing an annotation should trigger events.
|
||||||
isInHitRegion = false;
|
isInHitRegion = false;
|
||||||
_mouseTracker.detachAnnotation(annotation);
|
|
||||||
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
|
|
||||||
]));
|
|
||||||
expect(_binding.postFrameCallbacks, hasLength(0));
|
|
||||||
|
|
||||||
_binding.scheduleMouseTrackerPostFrameCheck();
|
_binding.scheduleMouseTrackerPostFrameCheck();
|
||||||
expect(_binding.postFrameCallbacks, hasLength(1));
|
expect(_binding.postFrameCallbacks, hasLength(1));
|
||||||
|
|
||||||
_binding.flushPostFrameCallbacks(Duration.zero);
|
_binding.flushPostFrameCallbacks(Duration.zero);
|
||||||
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
|
expect(events, _equalToEventsOnCriticalFields(<PointerEvent>[
|
||||||
|
const PointerExitEvent(position: Offset(0.0, 100.0)),
|
||||||
]));
|
]));
|
||||||
expect(_binding.postFrameCallbacks, hasLength(0));
|
expect(_binding.postFrameCallbacks, hasLength(0));
|
||||||
});
|
});
|
||||||
@ -417,8 +340,6 @@ void main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start with an annotation attached.
|
|
||||||
_mouseTracker.attachAnnotation(annotation);
|
|
||||||
isInHitRegion = false;
|
isInHitRegion = false;
|
||||||
|
|
||||||
// Connect a mouse.
|
// Connect a mouse.
|
||||||
@ -468,8 +389,6 @@ void main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start with an annotation attached.
|
|
||||||
_mouseTracker.attachAnnotation(annotation);
|
|
||||||
isInHitRegion = false;
|
isInHitRegion = false;
|
||||||
|
|
||||||
// Connect a mouse in the region. Should trigger Enter.
|
// 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;
|
isInHitRegion = false;
|
||||||
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
|
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
|
||||||
_pointerData(PointerChange.add, const Offset(200.0, 100.0)),
|
_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', () {
|
test('should not schedule postframe callbacks when no mouse is connected', () {
|
||||||
const MouseTrackerAnnotation annotation = MouseTrackerAnnotation();
|
|
||||||
_setUpMouseAnnotationFinder((Offset position) sync* {
|
_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>[
|
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
|
||||||
_pointerData(PointerChange.add, const Offset(0.0, 100.0), kind: PointerDeviceKind.touch),
|
_pointerData(PointerChange.add, const Offset(0.0, 100.0), kind: PointerDeviceKind.touch),
|
||||||
]));
|
]));
|
||||||
expect(_mouseTracker.mouseIsConnected, isFalse);
|
expect(_mouseTracker.mouseIsConnected, isFalse);
|
||||||
|
|
||||||
// Attaching an annotation just in case
|
|
||||||
_mouseTracker.attachAnnotation(annotation);
|
|
||||||
expect(_binding.postFrameCallbacks, hasLength(0));
|
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', () {
|
test('should not flip out if not all mouse events are listened to', () {
|
||||||
@ -628,60 +487,15 @@ void main() {
|
|||||||
yield annotation2;
|
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;
|
isInHitRegionOne = false;
|
||||||
isInHitRegionTwo = true;
|
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>[
|
ui.window.onPointerDataPacket(ui.PointerDataPacket(data: <ui.PointerData>[
|
||||||
_pointerData(PointerChange.add, const Offset(0.0, 101.0)),
|
_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.
|
// Passes if no errors are thrown.
|
||||||
_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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should trigger callbacks between parents and children in correct order', () {
|
test('should trigger callbacks between parents and children in correct order', () {
|
||||||
@ -713,8 +527,6 @@ void main() {
|
|||||||
yield annotationA;
|
yield annotationA;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_mouseTracker.attachAnnotation(annotationA);
|
|
||||||
_mouseTracker.attachAnnotation(annotationB);
|
|
||||||
|
|
||||||
// Starts out of A.
|
// Starts out of A.
|
||||||
isInB = false;
|
isInB = false;
|
||||||
@ -768,8 +580,6 @@ void main() {
|
|||||||
yield annotationB;
|
yield annotationB;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_mouseTracker.attachAnnotation(annotationA);
|
|
||||||
_mouseTracker.attachAnnotation(annotationB);
|
|
||||||
|
|
||||||
// Starts within A.
|
// Starts within A.
|
||||||
isInA = true;
|
isInA = true;
|
||||||
@ -868,13 +678,13 @@ class _EventCriticalFieldsMatcher extends Matcher {
|
|||||||
return mismatchDescription
|
return mismatchDescription
|
||||||
.add('is ')
|
.add('is ')
|
||||||
.addDescriptionOf(item.runtimeType)
|
.addDescriptionOf(item.runtimeType)
|
||||||
.add(' and doesn\'t match ')
|
.add(" and doesn't match ")
|
||||||
.addDescriptionOf(_expected.runtimeType);
|
.addDescriptionOf(_expected.runtimeType);
|
||||||
}
|
}
|
||||||
return mismatchDescription
|
return mismatchDescription
|
||||||
.add('has ')
|
.add('has ')
|
||||||
.addDescriptionOf(matchState['actual'])
|
.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']);
|
.addDescriptionOf(matchState['expected']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -940,7 +750,7 @@ class _EventListCriticalFieldsMatcher extends Matcher {
|
|||||||
mismatchDescription
|
mismatchDescription
|
||||||
.add('has\n ')
|
.add('has\n ')
|
||||||
.addDescriptionOf(matchState['actual'])
|
.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'])
|
.addDescriptionOf(matchState['expected'])
|
||||||
.add('\nsince it ');
|
.add('\nsince it ');
|
||||||
final Description subDescription = StringDescription();
|
final Description subDescription = StringDescription();
|
||||||
|
@ -467,6 +467,20 @@ void main() {
|
|||||||
// transform -> clip
|
// transform -> clip
|
||||||
_testFittedBoxWithClipRectLayer();
|
_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> {
|
class _TestRectClipper extends CustomClipper<Rect> {
|
||||||
|
@ -162,7 +162,6 @@ void main() {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final RenderMouseRegion renderListener = tester.renderObject(find.byType(MouseRegion));
|
|
||||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||||
await gesture.addPointer(location: const Offset(400.0, 300.0));
|
await gesture.addPointer(location: const Offset(400.0, 300.0));
|
||||||
addTearDown(gesture.removePointer);
|
addTearDown(gesture.removePointer);
|
||||||
@ -178,7 +177,6 @@ void main() {
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
expect(exit, isNull);
|
expect(exit, isNull);
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener.hoverAnnotation), isFalse);
|
|
||||||
});
|
});
|
||||||
testWidgets('Hover works with nested listeners', (WidgetTester tester) async {
|
testWidgets('Hover works with nested listeners', (WidgetTester tester) async {
|
||||||
final UniqueKey key1 = UniqueKey();
|
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));
|
Offset center = tester.getCenter(find.byKey(key2));
|
||||||
await gesture.moveTo(center);
|
await gesture.moveTo(center);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -243,8 +238,6 @@ void main() {
|
|||||||
expect(enter1, isNotEmpty);
|
expect(enter1, isNotEmpty);
|
||||||
expect(enter1.last.position, equals(center));
|
expect(enter1.last.position, equals(center));
|
||||||
expect(exit1, isEmpty);
|
expect(exit1, isEmpty);
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
|
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
|
|
||||||
clearLists();
|
clearLists();
|
||||||
|
|
||||||
// Now make sure that exiting the child only triggers the child exit, not
|
// 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(move1.last.position, equals(center));
|
||||||
expect(enter1, isEmpty);
|
expect(enter1, isEmpty);
|
||||||
expect(exit1, isEmpty);
|
expect(exit1, isEmpty);
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
|
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
|
|
||||||
clearLists();
|
clearLists();
|
||||||
});
|
});
|
||||||
testWidgets('Hover transfers between two listeners', (WidgetTester tester) async {
|
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 center1 = tester.getCenter(find.byKey(key1));
|
||||||
final Offset center2 = tester.getCenter(find.byKey(key2));
|
final Offset center2 = tester.getCenter(find.byKey(key2));
|
||||||
await gesture.moveTo(center1);
|
await gesture.moveTo(center1);
|
||||||
@ -329,8 +317,6 @@ void main() {
|
|||||||
expect(move2, isEmpty);
|
expect(move2, isEmpty);
|
||||||
expect(enter2, isEmpty);
|
expect(enter2, isEmpty);
|
||||||
expect(exit2, isEmpty);
|
expect(exit2, isEmpty);
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
|
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
|
|
||||||
clearLists();
|
clearLists();
|
||||||
await gesture.moveTo(center2);
|
await gesture.moveTo(center2);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -343,8 +329,6 @@ void main() {
|
|||||||
expect(enter2, isNotEmpty);
|
expect(enter2, isNotEmpty);
|
||||||
expect(enter2.last.position, equals(center2));
|
expect(enter2.last.position, equals(center2));
|
||||||
expect(exit2, isEmpty);
|
expect(exit2, isEmpty);
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
|
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
|
|
||||||
clearLists();
|
clearLists();
|
||||||
await gesture.moveTo(const Offset(400.0, 450.0));
|
await gesture.moveTo(const Offset(400.0, 450.0));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -355,8 +339,6 @@ void main() {
|
|||||||
expect(enter2, isEmpty);
|
expect(enter2, isEmpty);
|
||||||
expect(exit2, isNotEmpty);
|
expect(exit2, isNotEmpty);
|
||||||
expect(exit2.last.position, equals(const Offset(400.0, 450.0)));
|
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();
|
clearLists();
|
||||||
await tester.pumpWidget(Container());
|
await tester.pumpWidget(Container());
|
||||||
expect(move1, isEmpty);
|
expect(move1, isEmpty);
|
||||||
@ -365,8 +347,6 @@ void main() {
|
|||||||
expect(move2, isEmpty);
|
expect(move2, isEmpty);
|
||||||
expect(enter2, isEmpty);
|
expect(enter2, isEmpty);
|
||||||
expect(exit2, 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 {
|
testWidgets('needsCompositing set when parent class needsCompositing is set', (WidgetTester tester) async {
|
||||||
|
@ -261,7 +261,6 @@ void main() {
|
|||||||
onExit: (PointerExitEvent details) => exit = details,
|
onExit: (PointerExitEvent details) => exit = details,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
final RenderMouseRegion renderListener = tester.renderObject(find.byType(MouseRegion));
|
|
||||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||||
await gesture.addPointer(location: Offset.zero);
|
await gesture.addPointer(location: Offset.zero);
|
||||||
addTearDown(gesture.removePointer);
|
addTearDown(gesture.removePointer);
|
||||||
@ -279,7 +278,6 @@ void main() {
|
|||||||
expect(enter, isNull);
|
expect(enter, isNull);
|
||||||
expect(move, isNull);
|
expect(move, isNull);
|
||||||
expect(exit, isNull);
|
expect(exit, isNull);
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener.hoverAnnotation), isFalse);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('triggers pointer enter when widget moves in', (WidgetTester tester) async {
|
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));
|
Offset center = tester.getCenter(find.byKey(key2));
|
||||||
await gesture.moveTo(center);
|
await gesture.moveTo(center);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -428,8 +424,6 @@ void main() {
|
|||||||
expect(enter1, isNotEmpty);
|
expect(enter1, isNotEmpty);
|
||||||
expect(enter1.last.position, equals(center));
|
expect(enter1.last.position, equals(center));
|
||||||
expect(exit1, isEmpty);
|
expect(exit1, isEmpty);
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
|
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
|
|
||||||
clearLists();
|
clearLists();
|
||||||
|
|
||||||
// Now make sure that exiting the child only triggers the child exit, not
|
// 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(move1.last.position, equals(center));
|
||||||
expect(enter1, isEmpty);
|
expect(enter1, isEmpty);
|
||||||
expect(exit1, isEmpty);
|
expect(exit1, isEmpty);
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
|
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
|
|
||||||
clearLists();
|
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 center1 = tester.getCenter(find.byKey(key1));
|
||||||
final Offset center2 = tester.getCenter(find.byKey(key2));
|
final Offset center2 = tester.getCenter(find.byKey(key2));
|
||||||
await gesture.moveTo(center1);
|
await gesture.moveTo(center1);
|
||||||
@ -514,8 +504,6 @@ void main() {
|
|||||||
expect(move2, isEmpty);
|
expect(move2, isEmpty);
|
||||||
expect(enter2, isEmpty);
|
expect(enter2, isEmpty);
|
||||||
expect(exit2, isEmpty);
|
expect(exit2, isEmpty);
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
|
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
|
|
||||||
clearLists();
|
clearLists();
|
||||||
await gesture.moveTo(center2);
|
await gesture.moveTo(center2);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -528,8 +516,6 @@ void main() {
|
|||||||
expect(enter2, isNotEmpty);
|
expect(enter2, isNotEmpty);
|
||||||
expect(enter2.last.position, equals(center2));
|
expect(enter2.last.position, equals(center2));
|
||||||
expect(exit2, isEmpty);
|
expect(exit2, isEmpty);
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener1.hoverAnnotation), isTrue);
|
|
||||||
expect(tester.binding.mouseTracker.isAnnotationAttached(renderListener2.hoverAnnotation), isTrue);
|
|
||||||
clearLists();
|
clearLists();
|
||||||
await gesture.moveTo(const Offset(400.0, 450.0));
|
await gesture.moveTo(const Offset(400.0, 450.0));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
@ -540,8 +526,6 @@ void main() {
|
|||||||
expect(enter2, isEmpty);
|
expect(enter2, isEmpty);
|
||||||
expect(exit2, isNotEmpty);
|
expect(exit2, isNotEmpty);
|
||||||
expect(exit2.last.position, equals(const Offset(400.0, 450.0)));
|
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();
|
clearLists();
|
||||||
await tester.pumpWidget(Container());
|
await tester.pumpWidget(Container());
|
||||||
expect(move1, isEmpty);
|
expect(move1, isEmpty);
|
||||||
@ -550,8 +534,6 @@ void main() {
|
|||||||
expect(move2, isEmpty);
|
expect(move2, isEmpty);
|
||||||
expect(enter2, isEmpty);
|
expect(enter2, isEmpty);
|
||||||
expect(exit2, 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 {
|
testWidgets('MouseRegion uses updated callbacks', (WidgetTester tester) async {
|
||||||
@ -713,8 +695,8 @@ void main() {
|
|||||||
child: const MouseRegion(opaque: false),
|
child: const MouseRegion(opaque: false),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final RenderMouseRegion listener = tester.renderObject(find.byType(MouseRegion));
|
final RenderMouseRegion mouseRegion = tester.renderObject(find.byType(MouseRegion));
|
||||||
expect(listener.needsCompositing, isFalse);
|
expect(mouseRegion.needsCompositing, isFalse);
|
||||||
// No TransformLayer for `Transform.scale` is added because composting is
|
// No TransformLayer for `Transform.scale` is added because composting is
|
||||||
// not required and therefore the transform is executed on the canvas
|
// not required and therefore the transform is executed on the canvas
|
||||||
// directly. (One TransformLayer is always present for the root
|
// 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
|
// Compositing is required, therefore a dedicated TransformLayer for
|
||||||
// `Transform.scale` is added.
|
// `Transform.scale` is added.
|
||||||
expect(tester.layers.whereType<TransformLayer>(), hasLength(2));
|
expect(tester.layers.whereType<TransformLayer>(), hasLength(2));
|
||||||
@ -742,7 +724,7 @@ void main() {
|
|||||||
child: const MouseRegion(opaque: false),
|
child: const MouseRegion(opaque: false),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(listener.needsCompositing, isFalse);
|
expect(mouseRegion.needsCompositing, isFalse);
|
||||||
// TransformLayer for `Transform.scale` is removed again as transform is
|
// TransformLayer for `Transform.scale` is removed again as transform is
|
||||||
// executed directly on the canvas.
|
// executed directly on the canvas.
|
||||||
expect(tester.layers.whereType<TransformLayer>(), hasLength(1));
|
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
|
// Compositing is required, therefore a dedicated TransformLayer for
|
||||||
// `Transform.scale` is added.
|
// `Transform.scale` is added.
|
||||||
expect(tester.layers.whereType<TransformLayer>(), hasLength(2));
|
expect(tester.layers.whereType<TransformLayer>(), hasLength(2));
|
||||||
@ -767,20 +749,20 @@ void main() {
|
|||||||
addTearDown(gesture.removePointer);
|
addTearDown(gesture.removePointer);
|
||||||
await gesture.addPointer(location: Offset.zero);
|
await gesture.addPointer(location: Offset.zero);
|
||||||
|
|
||||||
int numEntries = 0;
|
int numEntrances = 0;
|
||||||
int numExits = 0;
|
int numExits = 0;
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Center(
|
Center(
|
||||||
child: HoverFeedback(
|
child: HoverFeedback(
|
||||||
onEnter: () => numEntries++,
|
onEnter: () { numEntrances += 1; },
|
||||||
onExit: () => numExits++,
|
onExit: () { numExits += 1; },
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
|
||||||
await gesture.moveTo(tester.getCenter(find.byType(Text)));
|
await gesture.moveTo(tester.getCenter(find.byType(Text)));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(numEntries, equals(1));
|
expect(numEntrances, equals(1));
|
||||||
expect(numExits, equals(0));
|
expect(numExits, equals(0));
|
||||||
expect(find.text('HOVERING'), findsOneWidget);
|
expect(find.text('HOVERING'), findsOneWidget);
|
||||||
|
|
||||||
@ -788,18 +770,18 @@ void main() {
|
|||||||
Container(),
|
Container(),
|
||||||
);
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(numEntries, equals(1));
|
expect(numEntrances, equals(1));
|
||||||
expect(numExits, equals(0));
|
expect(numExits, equals(0));
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Center(
|
Center(
|
||||||
child: HoverFeedback(
|
child: HoverFeedback(
|
||||||
onEnter: () => numEntries++,
|
onEnter: () { numEntrances += 1; },
|
||||||
onExit: () => numExits++,
|
onExit: () { numExits += 1; },
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(numEntries, equals(2));
|
expect(numEntrances, equals(2));
|
||||||
expect(numExits, equals(0));
|
expect(numExits, equals(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -809,21 +791,21 @@ void main() {
|
|||||||
await gesture.addPointer();
|
await gesture.addPointer();
|
||||||
addTearDown(gesture.removePointer);
|
addTearDown(gesture.removePointer);
|
||||||
|
|
||||||
int numEntries = 0;
|
int numEntrances = 0;
|
||||||
int numExits = 0;
|
int numExits = 0;
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Center(
|
Center(
|
||||||
child: HoverFeedback(
|
child: HoverFeedback(
|
||||||
key: feedbackKey,
|
key: feedbackKey,
|
||||||
onEnter: () => numEntries++,
|
onEnter: () { numEntrances += 1; },
|
||||||
onExit: () => numExits++,
|
onExit: () { numExits += 1; },
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
|
||||||
await gesture.moveTo(tester.getCenter(find.byType(Text)));
|
await gesture.moveTo(tester.getCenter(find.byType(Text)));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(numEntries, equals(1));
|
expect(numEntrances, equals(1));
|
||||||
expect(numExits, equals(0));
|
expect(numExits, equals(0));
|
||||||
expect(find.text('HOVERING'), findsOneWidget);
|
expect(find.text('HOVERING'), findsOneWidget);
|
||||||
|
|
||||||
@ -832,18 +814,20 @@ void main() {
|
|||||||
child: Container(
|
child: Container(
|
||||||
child: HoverFeedback(
|
child: HoverFeedback(
|
||||||
key: feedbackKey,
|
key: feedbackKey,
|
||||||
onEnter: () => numEntries++,
|
onEnter: () { numEntrances += 1; },
|
||||||
onExit: () => numExits++,
|
onExit: () { numExits += 1; },
|
||||||
))),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(numEntries, equals(1));
|
expect(numEntrances, equals(1));
|
||||||
expect(numExits, equals(0));
|
expect(numExits, equals(0));
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Container(),
|
Container(),
|
||||||
);
|
);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(numEntries, equals(1));
|
expect(numEntrances, equals(1));
|
||||||
expect(numExits, equals(0));
|
expect(numExits, equals(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -920,8 +904,8 @@ void main() {
|
|||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
onEnter: (PointerEnterEvent e) {},
|
onEnter: (PointerEnterEvent e) {},
|
||||||
child: _PaintDelegateWidget(
|
child: CustomPaint(
|
||||||
onPaint: _VoidDelegate(() => paintCount++),
|
painter: _DelegatedPainter(onPaint: () { paintCount += 1; }),
|
||||||
child: const Text('123'),
|
child: const Text('123'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -943,8 +927,8 @@ void main() {
|
|||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
onEnter: (PointerEnterEvent e) {},
|
onEnter: (PointerEnterEvent e) {},
|
||||||
child: _PaintDelegateWidget(
|
child: CustomPaint(
|
||||||
onPaint: _VoidDelegate(() => paintCount++),
|
painter: _DelegatedPainter(onPaint: () { paintCount += 1; }),
|
||||||
child: const Text('123'),
|
child: const Text('123'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1325,7 +1309,118 @@ void main() {
|
|||||||
expect(bottomRegionIsHovered, isFalse);
|
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();
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||||
RenderMouseRegion().debugFillProperties(builder);
|
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();
|
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
|
||||||
RenderMouseRegion(
|
RenderMouseRegion(
|
||||||
onEnter: (PointerEnterEvent event) {},
|
onEnter: (PointerEnterEvent event) {},
|
||||||
@ -1377,64 +1472,46 @@ void main() {
|
|||||||
await gesture.moveBy(const Offset(10.0, 10.0));
|
await gesture.moveBy(const Offset(10.0, 10.0));
|
||||||
expect(tester.binding.hasScheduledFrame, isFalse);
|
expect(tester.binding.hasScheduledFrame, isFalse);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
testWidgets("MouseTracker's attachAnnotation doesn't schedule any frames", (WidgetTester tester) async {
|
// Render widget `topLeft` at the top-left corner, stacking on top of the widget
|
||||||
// This test is here because MouseTracker can't use testWidgets.
|
// `background`.
|
||||||
final MouseTrackerAnnotation annotation = MouseTrackerAnnotation(
|
class _Scaffold extends StatelessWidget {
|
||||||
onEnter: (PointerEnterEvent event) {},
|
const _Scaffold({this.topLeft, this.background});
|
||||||
onHover: (PointerHoverEvent event) {},
|
|
||||||
onExit: (PointerExitEvent event) {},
|
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`.
|
class _DelegatedPainter extends CustomPainter {
|
||||||
@immutable
|
_DelegatedPainter({this.key, this.onPaint});
|
||||||
class _PaintDelegateWidget extends SingleChildRenderObjectWidget {
|
final Key key;
|
||||||
const _PaintDelegateWidget({
|
final VoidCallback onPaint;
|
||||||
Key key,
|
|
||||||
Widget child,
|
|
||||||
this.onPaint,
|
|
||||||
}) : super(key: key, child: child);
|
|
||||||
|
|
||||||
final _VoidDelegate onPaint;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
RenderObject createRenderObject(BuildContext context) {
|
void paint(Canvas canvas, Size size) {
|
||||||
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;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(PaintingContext context, Offset offset) {
|
|
||||||
if (onPaint != null) {
|
|
||||||
onPaint();
|
onPaint();
|
||||||
}
|
}
|
||||||
super.paint(context, offset);
|
|
||||||
}
|
@override
|
||||||
|
bool shouldRepaint(CustomPainter oldDelegate) =>
|
||||||
|
!(oldDelegate is _DelegatedPainter && key == oldDelegate.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HoverClientWithClosures extends StatefulWidget {
|
class _HoverClientWithClosures extends StatefulWidget {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user