Add opacity control to MouseRegion. Add findAnnotations to Layer. (#37896)

* Adds a new parameter bool opaque to MouseRegion
* The same to RenderMouseRegion and AnnotatedRegionLayer
* Add findAnnotations to Layer
This commit is contained in:
Tong Mu 2019-10-10 12:46:12 -07:00 committed by GitHub
parent 6b4a10ae53
commit a71d69ba67
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1397 additions and 257 deletions

View file

@ -54,7 +54,7 @@ class HitTestEntry {
final HitTestTarget target;
@override
String toString() => '$target';
String toString() => '${describeIdentity(this)}($target)';
/// Returns a matrix describing how [PointerEvent]s delivered to this
/// [HitTestEntry] should be transformed from the global coordinate space of

View file

@ -878,9 +878,6 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
}
}
void _handleMouseEnter(PointerEnterEvent event) => _handleHover(true);
void _handleMouseExit(PointerExitEvent event) => _handleHover(false);
void _handleHover(bool hovering) {
if (hovering != _isHovering) {
setState(() {
@ -1007,8 +1004,8 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
return IgnorePointer(
ignoring: !_isEnabled,
child: MouseRegion(
onEnter: _handleMouseEnter,
onExit: _handleMouseExit,
onEnter: (PointerEnterEvent event) => _handleHover(true),
onExit: (PointerExitEvent event) => _handleHover(false),
child: AnimatedBuilder(
animation: controller, // changes the _currentLength
builder: (BuildContext context, Widget child) {

View file

@ -14,6 +14,69 @@ import 'package:vector_math/vector_math_64.dart';
import 'debug.dart';
/// Information collected for an annotation that is found in the layer tree.
///
/// See also:
///
/// * [Layer.find], [Layer.findAll], and [Layer.findAnnotations], which create
/// and use objects of this class.
@immutable
class AnnotationEntry<T> {
/// Create an entry of found annotation by providing the oject and related
/// information.
const AnnotationEntry({
@required this.annotation,
@required this.localPosition,
}) : assert(localPosition != null);
/// The annotation object that is found.
final T annotation;
/// The target location described by the local coordinate space of the layer
/// that contains the annotation.
final Offset localPosition;
@override
String toString() {
return '$runtimeType(annotation: $annotation, localPostion: $localPosition)';
}
}
/// Information collected about a list of annotations that are found in the
/// layer tree.
///
/// See also:
///
/// * [AnnotationEntry], which are members of this class.
/// * [Layer.findAll], and [Layer.findAnnotations], which create and use an
/// object of this class.
class AnnotationResult<T> {
final List<AnnotationEntry<T>> _entries = <AnnotationEntry<T>>[];
/// Add a new entry to the end of the result.
///
/// Usually, entries should be added in order from most specific to least
/// specific, typically during an upward walk of the tree.
void add(AnnotationEntry<T> entry) => _entries.add(entry);
/// An unmodifiable list of [AnnotationEntry] objects recorded.
///
/// The first entry is the most specific, typically the one at the leaf of
/// tree.
Iterable<AnnotationEntry<T>> get entries => _entries;
/// An unmodifiable list of annotations recorded.
///
/// The first entry is the most specific, typically the one at the leaf of
/// tree.
///
/// It is similar to [entries] but does not contain other information.
Iterable<T> get annotations sync* {
for (AnnotationEntry<T> entry in _entries)
yield entry.annotation;
}
}
/// A composited layer.
///
/// During painting, the render tree generates a tree of composited layers that
@ -213,31 +276,105 @@ abstract class Layer extends AbstractNode with DiagnosticableTreeMixin {
parent?._removeChild(this);
}
/// Returns the value of [S] that corresponds to the point described by
/// [regionOffset].
/// Search this layer and its subtree for annotations of type `S` at the
/// location described by `localPosition`.
///
/// Returns null if no matching region is found.
/// This method is called by the default implementation of [find] and
/// [findAll]. Override this method to customize how the layer should search
/// for annotations, or if the layer has its own annotations to add.
///
/// The main way for a value to be found here is by pushing an
/// [AnnotatedRegionLayer] into the layer tree.
/// ## About layer annotations
///
/// See also:
/// {@template flutter.rendering.layer.findAnnotations.aboutAnnotations}
/// Annotation is an optional object of any type that can be carried with a
/// layer. An annotation can be found at a location as long as the owner layer
/// contains the location and is walked to.
///
/// * [AnnotatedRegionLayer], for placing values in the layer tree.
S find<S>(Offset regionOffset);
/// The annotations are searched by first visitng each child recursively, then
/// this layer, resulting in an order from visually front to back. Annotations
/// must meet the given restrictions, such as type and position.
///
/// The common way for a value to be found here is by pushing an
/// [AnnotatedRegionLayer] into the layer tree, or by adding the desired
/// annotation by overriding `findAnnotations`.
/// {@endtemplate}
///
/// ## Parameters and return value
///
/// The [result] parameter is where the method outputs the resulting
/// annotations. New annotations found during the walk are added to the tail.
///
/// The [onlyFirst] parameter indicates that, if true, the search will stop
/// when it finds the first qualified annotation; otherwise, it will walk the
/// entire subtree.
///
/// The return value indicates the opacity of this layer and its subtree at
/// this position. If it returns true, then this layer's parent should skip
/// the children behind this layer. In other words, it is opaque to this type
/// of annotation and has absorbed the search so that its siblings behind it
/// are not aware of the search. If the return value is false, then the parent
/// might continue with other siblings.
///
/// The return value does not affect whether the parent adds its own
/// annotations; in other words, if a layer is supposed to add an annotation,
/// it will always add it even if its children are opaque to this type of
/// annotation. However, the opacity that the parents return might be affected
/// by their children, hence making all of its ancestors opaque to this type
/// of annotation.
@protected
bool findAnnotations<S>(
AnnotationResult<S> result,
Offset localPosition, {
@required bool onlyFirst,
});
/// Returns an iterable of [S] values that corresponds to the point described
/// by [regionOffset] on all layers under the point.
/// Search this layer and its subtree for the first annotation of type `S`
/// under the point described by `localPosition`.
///
/// Returns an empty list if no matching region is found.
/// Returns null if no matching annotations are found.
///
/// The main way for a value to be found here is by pushing an
/// [AnnotatedRegionLayer] into the layer tree.
/// By default this method simply calls [findAnnotations] with `onlyFirst:
/// true` and returns the first result. It is encouraged to override
/// [findAnnotations] instead of this method.
///
/// ## About layer annotations
///
/// {@macro flutter.rendering.layer.findAnnotations.aboutAnnotations}
///
/// See also:
///
/// * [findAll], which is similar but returns all annotations found at the
/// given position.
/// * [AnnotatedRegionLayer], for placing values in the layer tree.
Iterable<S> findAll<S>(Offset regionOffset);
AnnotationEntry<S> find<S>(Offset localPosition) {
final AnnotationResult<S> result = AnnotationResult<S>();
findAnnotations<S>(result, localPosition, onlyFirst: true);
return result.entries.isEmpty ? null : result.entries.first;
}
/// Search this layer and its subtree for all annotations of type `S` under
/// the point described by `localPosition`.
///
/// Returns a result with empty entries if no matching annotations are found.
///
/// By default this method simply calls [findAnnotations] with `onlyFirst:
/// false` and returns its result. It is encouraged to override
/// [findAnnotations] instead of this method.
///
/// ## About layer annotations
///
/// {@macro flutter.rendering.layer.findAnnotations.aboutAnnotations}
///
/// See also:
///
/// * [find], which is similar but returns the first annotation found at the
/// given position.
/// * [AnnotatedRegionLayer], for placing values in the layer tree.
AnnotationResult<S> findAll<S>(Offset localPosition) {
final AnnotationResult<S> result = AnnotationResult<S>();
findAnnotations<S>(result, localPosition, onlyFirst: false);
return result;
}
/// Override this method to upload this layer to the engine.
///
@ -359,10 +496,10 @@ class PictureLayer extends Layer {
}
@override
S find<S>(Offset regionOffset) => null;
@override
Iterable<S> findAll<S>(Offset regionOffset) => <S>[];
@protected
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
return false;
}
}
/// A composited layer that maps a backend texture to a rectangle.
@ -430,10 +567,10 @@ class TextureLayer extends Layer {
}
@override
S find<S>(Offset regionOffset) => null;
@override
Iterable<S> findAll<S>(Offset regionOffset) => <S>[];
@protected
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
return false;
}
}
/// A layer that shows an embedded [UIView](https://developer.apple.com/documentation/uikit/uiview)
@ -468,10 +605,10 @@ class PlatformViewLayer extends Layer {
}
@override
S find<S>(Offset regionOffset) => null;
@override
Iterable<S> findAll<S>(Offset regionOffset) => <S>[];
@protected
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
return false;
}
}
/// A layer that indicates to the compositor that it should display
@ -544,10 +681,10 @@ class PerformanceOverlayLayer extends Layer {
}
@override
S find<S>(Offset regionOffset) => null;
@override
Iterable<S> findAll<S>(Offset regionOffset) => <S>[];
@protected
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
return false;
}
}
/// A composited layer that has a list of children.
@ -728,31 +865,16 @@ class ContainerLayer extends Layer {
}
@override
S find<S>(Offset regionOffset) {
Layer current = lastChild;
while (current != null) {
final Object value = current.find<S>(regionOffset);
if (value != null) {
return value;
}
current = current.previousSibling;
@protected
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
for (Layer child = lastChild; child != null; child = child.previousSibling) {
final bool isAbsorbed = child.findAnnotations<S>(result, localPosition, onlyFirst: onlyFirst);
if (isAbsorbed)
return true;
if (onlyFirst && result.entries.isNotEmpty)
return isAbsorbed;
}
return null;
}
@override
Iterable<S> findAll<S>(Offset regionOffset) {
Iterable<S> result = Iterable<S>.empty();
if (firstChild == null)
return result;
Layer child = lastChild;
while (true) {
result = result.followedBy(child.findAll<S>(regionOffset));
if (child == firstChild)
break;
child = child.previousSibling;
}
return result;
return false;
}
@override
@ -974,13 +1096,9 @@ class OffsetLayer extends ContainerLayer {
}
@override
S find<S>(Offset regionOffset) {
return super.find<S>(regionOffset - offset);
}
@override
Iterable<S> findAll<S>(Offset regionOffset) {
return super.findAll<S>(regionOffset - offset);
@protected
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
return super.findAnnotations<S>(result, localPosition - offset, onlyFirst: onlyFirst);
}
@override
@ -1102,17 +1220,11 @@ class ClipRectLayer extends ContainerLayer {
}
@override
S find<S>(Offset regionOffset) {
if (!clipRect.contains(regionOffset))
return null;
return super.find<S>(regionOffset);
}
@override
Iterable<S> findAll<S>(Offset regionOffset) {
if (!clipRect.contains(regionOffset))
return Iterable<S>.empty();
return super.findAll<S>(regionOffset);
@protected
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
if (!clipRect.contains(localPosition))
return false;
return super.findAnnotations<S>(result, localPosition, onlyFirst: onlyFirst);
}
@override
@ -1188,17 +1300,11 @@ class ClipRRectLayer extends ContainerLayer {
}
@override
S find<S>(Offset regionOffset) {
if (!clipRRect.contains(regionOffset))
return null;
return super.find<S>(regionOffset);
}
@override
Iterable<S> findAll<S>(Offset regionOffset) {
if (!clipRRect.contains(regionOffset))
return Iterable<S>.empty();
return super.findAll<S>(regionOffset);
@protected
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
if (!clipRRect.contains(localPosition))
return false;
return super.findAnnotations<S>(result, localPosition, onlyFirst: onlyFirst);
}
@override
@ -1274,17 +1380,11 @@ class ClipPathLayer extends ContainerLayer {
}
@override
S find<S>(Offset regionOffset) {
if (!clipPath.contains(regionOffset))
return null;
return super.find<S>(regionOffset);
}
@override
Iterable<S> findAll<S>(Offset regionOffset) {
if (!clipPath.contains(regionOffset))
return Iterable<S>.empty();
return super.findAll<S>(regionOffset);
@protected
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
if (!clipPath.contains(localPosition))
return false;
return super.findAnnotations<S>(result, localPosition, onlyFirst: onlyFirst);
}
@override
@ -1400,7 +1500,7 @@ class TransformLayer extends OffsetLayer {
builder.pop();
}
Offset _transformOffset(Offset regionOffset) {
Offset _transformOffset(Offset localPosition) {
if (_inverseDirty) {
_invertedTransform = Matrix4.tryInvert(
PointerEvent.removePerspectiveTransform(transform)
@ -1409,24 +1509,18 @@ class TransformLayer extends OffsetLayer {
}
if (_invertedTransform == null)
return null;
final Vector4 vector = Vector4(regionOffset.dx, regionOffset.dy, 0.0, 1.0);
final Vector4 vector = Vector4(localPosition.dx, localPosition.dy, 0.0, 1.0);
final Vector4 result = _invertedTransform.transform(vector);
return Offset(result[0], result[1]);
}
@override
S find<S>(Offset regionOffset) {
final Offset transformedOffset = _transformOffset(regionOffset);
return transformedOffset == null ? null : super.find<S>(transformedOffset);
}
@override
Iterable<S> findAll<S>(Offset regionOffset) {
final Offset transformedOffset = _transformOffset(regionOffset);
if (transformedOffset == null) {
return Iterable<S>.empty();
}
return super.findAll<S>(transformedOffset);
@protected
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
final Offset transformedOffset = _transformOffset(localPosition);
if (transformedOffset == null)
return false;
return super.findAnnotations<S>(result, transformedOffset, onlyFirst: onlyFirst);
}
@override
@ -1734,17 +1828,11 @@ class PhysicalModelLayer extends ContainerLayer {
}
@override
S find<S>(Offset regionOffset) {
if (!clipPath.contains(regionOffset))
return null;
return super.find<S>(regionOffset);
}
@override
Iterable<S> findAll<S>(Offset regionOffset) {
if (!clipPath.contains(regionOffset))
return Iterable<S>.empty();
return super.findAll<S>(regionOffset);
@protected
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
if (!clipPath.contains(localPosition))
return false;
return super.findAnnotations<S>(result, localPosition, onlyFirst: onlyFirst);
}
@override
@ -1870,10 +1958,10 @@ class LeaderLayer extends ContainerLayer {
Offset _lastOffset;
@override
S find<S>(Offset regionOffset) => super.find<S>(regionOffset - offset);
@override
Iterable<S> findAll<S>(Offset regionOffset) => super.findAll<S>(regionOffset - offset);
@protected
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
return super.findAnnotations<S>(result, localPosition - offset, onlyFirst: onlyFirst);
}
@override
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
@ -1991,37 +2079,32 @@ class FollowerLayer extends ContainerLayer {
Matrix4 _invertedTransform;
bool _inverseDirty = true;
Offset _transformOffset<S>(Offset regionOffset) {
Offset _transformOffset<S>(Offset localPosition) {
if (_inverseDirty) {
_invertedTransform = Matrix4.tryInvert(getLastTransform());
_inverseDirty = false;
}
if (_invertedTransform == null)
return null;
final Vector4 vector = Vector4(regionOffset.dx, regionOffset.dy, 0.0, 1.0);
final Vector4 vector = Vector4(localPosition.dx, localPosition.dy, 0.0, 1.0);
final Vector4 result = _invertedTransform.transform(vector);
return Offset(result[0] - linkedOffset.dx, result[1] - linkedOffset.dy);
}
@override
S find<S>(Offset regionOffset) {
@protected
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
if (link.leader == null) {
return showWhenUnlinked ? super.find<S>(regionOffset - unlinkedOffset) : null;
if (showWhenUnlinked) {
return super.findAnnotations(result, localPosition - unlinkedOffset, onlyFirst: onlyFirst);
}
return false;
}
final Offset transformedOffset = _transformOffset<S>(regionOffset);
return transformedOffset == null ? null : super.find<S>(transformedOffset);
}
@override
Iterable<S> findAll<S>(Offset regionOffset) {
if (link.leader == null) {
return showWhenUnlinked ? super.findAll<S>(regionOffset - unlinkedOffset) : <S>[];
}
final Offset transformedOffset = _transformOffset<S>(regionOffset);
final Offset transformedOffset = _transformOffset<S>(localPosition);
if (transformedOffset == null) {
return <S>[];
return false;
}
return super.findAll<S>(transformedOffset);
return super.findAnnotations<S>(result, transformedOffset, onlyFirst: onlyFirst);
}
/// The transform that was used during the last composition phase.
@ -2160,67 +2243,127 @@ class FollowerLayer extends ContainerLayer {
}
}
/// A composited layer which annotates its children with a value.
/// A composited layer which annotates its children with a value. Pushing this
/// layer to the tree is the common way of adding an annotation.
///
/// These values can be retrieved using [Layer.find] with a given [Offset]. If
/// a [Size] is provided to this layer, then find will check if the provided
/// offset is within the bounds of the layer.
/// An annotation is an optional object of any type that, when attached with a
/// layer, can be retrieved using [Layer.find] or [Layer.findAll] with a
/// position. The search process is done recursively, controlled by a concept
/// of being opaque to a type of annotation, explained in the document of
/// [Layer.findAnnotations].
///
/// When an annotation search arrives, this layer defers the same search to each
/// of this layer's children, respecting their opacity. Then it adds this
/// layer's [annotation] if all of the following restrictions are met:
///
/// {@template flutter.rendering.annotatedRegionLayer.restrictions}
/// * The target type must be identical to the annotated type `T`.
/// * If [size] is provided, the target position must be contained within the
/// rectangle formed by [size] and [offset].
/// {@endtemplate}
///
/// This layer is opaque to a type of annotation if any child is also opaque, or
/// if [opaque] is true and the layer's annotation is added.
class AnnotatedRegionLayer<T> extends ContainerLayer {
/// Creates a new layer annotated with [value] that clips to rectangle defined
/// by the [size] and [offset] if provided.
/// Creates a new layer that annotates its children with [value].
///
/// The [value] provided cannot be null.
AnnotatedRegionLayer(this.value, {this.size, Offset offset})
: offset = offset ?? Offset.zero,
assert(value != null);
AnnotatedRegionLayer(
this.value, {
this.size,
Offset offset,
this.opaque = false,
}) : assert(value != null),
assert(opaque != null),
offset = offset ?? Offset.zero;
/// The value returned by [find] if the offset is contained within this layer.
/// The annotated object, which is added to the result if all restrictions are
/// met.
final T value;
/// The [size] is optionally used to clip the hit-testing of [find].
/// The size of an optional clipping rectangle, used to control whether a
/// position is contained by the annotation.
///
/// If not provided, all offsets are considered to be contained within this
/// layer, unless an ancestor layer applies a clip.
///
/// If [offset] is set, then the offset is applied to the size region before
/// hit testing in [find].
/// If [size] is provided, then the annotation is only added if the target
/// position is contained by the rectangle formed by [size] and [offset].
/// Otherwise no such restriction is applied, and clipping can only be done by
/// the ancestor layers.
final Size size;
/// The [offset] is optionally used to translate the clip region for the
/// hit-testing of [find] by [offset].
/// The offset of the optional clipping rectangle that is indicated by [size].
///
/// If not provided, offset defaults to [Offset.zero].
/// The [offset] defaults to [Offset.zero] if not provided, and is ignored if
/// [size] is not set.
///
/// Ignored if [size] is not set.
/// The [offset] only offsets the the clipping rectagle, and does not affect
/// how the painting or annotation search is propagated to its children.
final Offset offset;
@override
S find<S>(Offset regionOffset) {
final S result = super.find<S>(regionOffset);
if (result != null)
return result;
if (size != null && !(offset & size).contains(regionOffset))
return null;
if (T == S) {
final Object untypedResult = value;
final S typedResult = untypedResult;
return typedResult;
}
return null;
}
/// Whether the annotation of this layer should be opaque during an annotation
/// search of type `T`, preventing siblings visually behind it from being
/// searched.
///
/// If [opaque] is true, and this layer does add its annotation [value],
/// then the layer will always be opaque during the search.
///
/// If [opaque] is false, or if this layer does not add its annotation,
/// then the opacity of this layer will be the one returned by the children,
/// meaning that it will be opaque if any child is opaque.
///
/// The [opaque] defaults to false.
///
/// The [opaque] is effectively useless during [Layer.find] (more
/// specifically, [Layer.findAnnotations] with `onlyFirst: true`), since the
/// search process then skips the remaining tree after finding the first
/// annotation.
///
/// See also:
///
/// * [Layer.findAnnotations], which explains the concept of being opaque
/// to a type of annotation as the return value.
/// * [HitTestBehavior], which controls similar logic when hit-testing in the
/// render tree.
final bool opaque;
/// Searches the subtree for annotations of type `S` at the location
/// `localPosition`, then adds the annotation [value] if applicable.
///
/// This method always searches its children, and if any child returns `true`,
/// the remaining children are skipped. Regardless of what the children
/// return, this method then adds this layer's annotation if all of the
/// following restrictions are met:
///
/// {@macro flutter.rendering.annotatedRegionLayer.restrictions}
///
/// This search process respects `onlyFirst`, meaning that when `onlyFirst` is
/// true, the search will stop when it finds the first annotation from the
/// children, and the layer's own annotation is checked only when none is
/// given by the children.
///
/// The return value is true if any child returns `true`, or if [opaque] is
/// true and the layer's annotation is added.
///
/// For explanation of layer annotations, parameters and return value, refer
/// to [Layer.findAnnotations].
@override
Iterable<S> findAll<S>(Offset regionOffset) {
final Iterable<S> childResults = super.findAll<S>(regionOffset);
if (size != null && !(offset & size).contains(regionOffset)) {
return childResults;
@protected
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
bool isAbsorbed = super.findAnnotations(result, localPosition, onlyFirst: onlyFirst);
if (result.entries.isNotEmpty && onlyFirst)
return isAbsorbed;
if (size != null && !(offset & size).contains(localPosition)) {
return isAbsorbed;
}
if (T == S) {
final Object untypedResult = value;
final S typedResult = untypedResult;
return childResults.followedBy(<S>[typedResult]);
isAbsorbed = isAbsorbed || opaque;
final Object untypedValue = value;
final S typedValue = untypedValue;
result.add(AnnotationEntry<S>(
annotation: typedValue,
localPosition: localPosition,
));
}
return childResults;
return isAbsorbed;
}
@override
@ -2229,5 +2372,6 @@ class AnnotatedRegionLayer<T> extends ContainerLayer {
properties.add(DiagnosticsProperty<T>('value', value));
properties.add(DiagnosticsProperty<Size>('size', size, defaultValue: null));
properties.add(DiagnosticsProperty<Offset>('offset', offset, defaultValue: null));
properties.add(DiagnosticsProperty<bool>('opaque', opaque, defaultValue: false));
}
}

View file

@ -2591,8 +2591,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
/// Calls callbacks in response to pointer events that are exclusive to mice.
///
/// Simply put, it responds to events that are related to hovering,
/// i.e. when the mouse enters, exits or hovers a region without pressing.
/// It responds to events that are related to hovering, i.e. when the mouse
/// enters, exits (with or without pressing buttons), or moves over a region
/// without pressing buttons.
///
/// It does not respond to common events that construct gestures, such as when
/// the pointer is pressed, moved, then released or canceled. For these events,
@ -2601,14 +2602,21 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
/// If it has a child, it defers to the child for sizing behavior.
///
/// If it does not have a child, it grows to fit the parent-provided constraints.
///
/// See also:
///
/// * [MouseRegion], a widget that listens to hover events using
/// [RenderMouseRegion].
class RenderMouseRegion extends RenderProxyBox {
/// Creates a render object that forwards pointer events to callbacks.
RenderMouseRegion({
PointerEnterEventListener onEnter,
PointerHoverEventListener onHover,
PointerExitEventListener onExit,
this.opaque = true,
RenderBox child,
}) : _onEnter = onEnter,
}) : assert(opaque != null),
_onEnter = onEnter,
_onHover = onHover,
_onExit = onExit,
_annotationIsActive = false,
@ -2620,10 +2628,24 @@ class RenderMouseRegion extends RenderProxyBox {
);
}
/// Called when a hovering pointer enters the region for this widget.
/// Whether this object should prevent [RenderMouseRegion]s visually behind it
/// from detecting the pointer, thus affecting how their [onHover], [onEnter],
/// and [onExit] behave.
///
/// If this is a mouse pointer, this will fire when the mouse pointer enters
/// the region defined by this widget.
/// If [opaque] is true, this object will absorb the mouse pointer and
/// prevent this object's siblings (or any other objects that are not
/// ancestors or descendants of this object) from detecting the mouse
/// pointer even when the pointer is within their areas.
///
/// If [opaque] is false, this object will not affect how [RenderMouseRegion]s
/// behind it behave, which will detect the mouse pointer as long as the
/// pointer is within their areas.
///
/// This defaults to true.
bool opaque;
/// Called when a mouse pointer enters the region (with or without buttons
/// pressed).
PointerEnterEventListener get onEnter => _onEnter;
set onEnter(PointerEnterEventListener value) {
if (_onEnter != value) {
@ -2637,10 +2659,8 @@ class RenderMouseRegion extends RenderProxyBox {
_onEnter(event);
}
/// Called when a pointer that has not triggered an [onPointerDown] changes
/// position.
///
/// Typically only triggered for mouse pointers.
/// Called when a pointer changes position without buttons pressed and the end
/// position is within the region.
PointerHoverEventListener get onHover => _onHover;
set onHover(PointerHoverEventListener value) {
if (_onHover != value) {
@ -2654,10 +2674,7 @@ class RenderMouseRegion extends RenderProxyBox {
_onHover(event);
}
/// Called when a hovering pointer leaves the region for this widget.
///
/// If this is a mouse pointer, this will fire when the mouse pointer leaves
/// the region defined by this widget.
/// Called when a pointer leaves the region (with or without buttons pressed).
PointerExitEventListener get onExit => _onExit;
set onExit(PointerExitEventListener value) {
if (_onExit != value) {
@ -2754,6 +2771,7 @@ class RenderMouseRegion extends RenderProxyBox {
_hoverAnnotation,
size: size,
offset: offset,
opaque: opaque,
);
context.pushLayer(layer, super.paint, offset);
} else {
@ -2778,6 +2796,7 @@ class RenderMouseRegion extends RenderProxyBox {
},
ifEmpty: '<none>',
));
properties.add(DiagnosticsProperty<bool>('opaque', opaque, defaultValue: true));
}
}

View file

@ -196,7 +196,9 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
// Layer hit testing is done using device pixels, so we have to convert
// the logical coordinates of the event location back to device pixels
// here.
return layer.findAll<MouseTrackerAnnotation>(position * configuration.devicePixelRatio);
return layer.findAll<MouseTrackerAnnotation>(
position * configuration.devicePixelRatio
).annotations;
}
@override
@ -241,12 +243,12 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
final Rect bounds = paintBounds;
final Offset top = Offset(bounds.center.dx, _window.padding.top / _window.devicePixelRatio);
final Offset bottom = Offset(bounds.center.dx, bounds.center.dy - _window.padding.bottom / _window.devicePixelRatio);
final SystemUiOverlayStyle upperOverlayStyle = layer.find<SystemUiOverlayStyle>(top);
final SystemUiOverlayStyle upperOverlayStyle = layer.find<SystemUiOverlayStyle>(top)?.annotation;
// Only android has a customizable system navigation bar.
SystemUiOverlayStyle lowerOverlayStyle;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
lowerOverlayStyle = layer.find<SystemUiOverlayStyle>(bottom);
lowerOverlayStyle = layer.find<SystemUiOverlayStyle>(bottom)?.annotation;
break;
case TargetPlatform.iOS:
case TargetPlatform.fuchsia:

View file

@ -5575,11 +5575,11 @@ class Listener extends StatelessWidget {
// TODO(tongmu): After it goes stable, remove these 3 parameters from Listener
// and Listener should no longer need an intermediate class _PointerListener.
// https://github.com/flutter/flutter/issues/36085
@Deprecated('Use MouseRegion.onEnter instead')
@Deprecated('Use MouseRegion.onEnter instead. See MouseRegion.opaque for behavioral difference.')
this.onPointerEnter, // ignore: deprecated_member_use_from_same_package
@Deprecated('Use MouseRegion.onExit instead')
@Deprecated('Use MouseRegion.onExit instead. See MouseRegion.opaque for behavioral difference.')
this.onPointerExit, // ignore: deprecated_member_use_from_same_package
@Deprecated('Use MouseRegion.onHover instead')
@Deprecated('Use MouseRegion.onHover instead. See MouseRegion.opaque for behavioral difference.')
this.onPointerHover, // ignore: deprecated_member_use_from_same_package
this.onPointerUp,
this.onPointerCancel,
@ -5656,6 +5656,7 @@ class Listener extends StatelessWidget {
onEnter: onPointerEnter,
onExit: onPointerExit,
onHover: onPointerHover,
opaque: false,
child: result,
);
}
@ -5815,8 +5816,10 @@ class MouseRegion extends SingleChildRenderObjectWidget {
this.onEnter,
this.onExit,
this.onHover,
this.opaque = true,
Widget child,
}) : super(key: key, child: child);
}) : assert(opaque != null),
super(key: key, child: child);
/// Called when a mouse pointer (with or without buttons pressed) enters the
/// region defined by this widget, or when the widget appears under the
@ -5832,6 +5835,22 @@ class MouseRegion extends SingleChildRenderObjectWidget {
/// the pointer.
final PointerExitEventListener onExit;
/// Whether this widget should prevent other [MouseRegion]s visually behind it
/// from detecting the pointer, thus affecting how their [onHover], [onEnter],
/// and [onExit] behave.
///
/// If [opaque] is true, this widget will absorb the mouse pointer and
/// prevent this widget's siblings (or any other widgets that are not
/// ancestors or descendants of this widget) from detecting the mouse
/// pointer even when the pointer is within their areas.
///
/// If [opaque] is false, this object will not affect how [MouseRegion]s
/// behind it behave, which will detect the mouse pointer as long as the
/// pointer is within their areas.
///
/// This defaults to true.
final bool opaque;
@override
_MouseRegionElement createElement() => _MouseRegionElement(this);
@ -5841,6 +5860,7 @@ class MouseRegion extends SingleChildRenderObjectWidget {
onEnter: onEnter,
onHover: onHover,
onExit: onExit,
opaque: opaque,
);
}
@ -5849,7 +5869,8 @@ class MouseRegion extends SingleChildRenderObjectWidget {
renderObject
..onEnter = onEnter
..onHover = onHover
..onExit = onExit;
..onExit = onExit
..opaque = opaque;
}
@override
@ -5863,6 +5884,7 @@ class MouseRegion extends SingleChildRenderObjectWidget {
if (onHover != null)
listeners.add('hover');
properties.add(IterableProperty<String>('listeners', listeners, ifEmpty: '<none>'));
properties.add(DiagnosticsProperty<bool>('opaque', opaque, defaultValue: true));
}
}

View file

@ -57,10 +57,14 @@ class _ProxyLayer extends Layer {
}
@override
S find<S>(Offset regionOffset) => _layer.find(regionOffset);
@override
Iterable<S> findAll<S>(Offset regionOffset) => <S>[];
@protected
bool findAnnotations<S>(
AnnotationResult<S> result,
Offset localPosition, {
@required bool onlyFirst,
}) {
return _layer.findAnnotations(result, localPosition, onlyFirst: onlyFirst);
}
}
/// A [Canvas] that multicasts all method calls to a main canvas and a
@ -2662,10 +2666,14 @@ class _InspectorOverlayLayer extends Layer {
}
@override
S find<S>(Offset regionOffset) => null;
@override
Iterable<S> findAll<S>(Offset regionOffset) => <S>[];
@protected
bool findAnnotations<S>(
AnnotationResult<S> result,
Offset localPosition, {
bool onlyFirst,
}) {
return false;
}
}
const double _kScreenEdgeMargin = 10.0;

View file

@ -22,9 +22,9 @@ void main() {
i += 1;
}
expect(containerLayer.find<int>(const Offset(0.0, 1.0)), 0);
expect(containerLayer.find<int>(const Offset(0.0, 101.0)), 1);
expect(containerLayer.find<int>(const Offset(0.0, 201.0)), 2);
expect(containerLayer.find<int>(const Offset(0.0, 1.0)).annotation, 0);
expect(containerLayer.find<int>(const Offset(0.0, 101.0)).annotation, 1);
expect(containerLayer.find<int>(const Offset(0.0, 201.0)).annotation, 2);
});
test('finds a value within the clip in a ClipRectLayer', () {
@ -41,9 +41,9 @@ void main() {
i += 1;
}
expect(containerLayer.find<int>(const Offset(0.0, 1.0)), 0);
expect(containerLayer.find<int>(const Offset(0.0, 101.0)), 1);
expect(containerLayer.find<int>(const Offset(0.0, 201.0)), 2);
expect(containerLayer.find<int>(const Offset(0.0, 1.0)).annotation, 0);
expect(containerLayer.find<int>(const Offset(0.0, 101.0)).annotation, 1);
expect(containerLayer.find<int>(const Offset(0.0, 201.0)).annotation, 2);
});
@ -61,9 +61,9 @@ void main() {
i += 1;
}
expect(containerLayer.find<int>(const Offset(5.0, 5.0)), 0);
expect(containerLayer.find<int>(const Offset(5.0, 105.0)), 1);
expect(containerLayer.find<int>(const Offset(5.0, 205.0)), 2);
expect(containerLayer.find<int>(const Offset(5.0, 5.0)).annotation, 0);
expect(containerLayer.find<int>(const Offset(5.0, 105.0)).annotation, 1);
expect(containerLayer.find<int>(const Offset(5.0, 205.0)).annotation, 2);
});
test('finds a value under a TransformLayer', () {
@ -87,11 +87,11 @@ void main() {
i += 1;
}
expect(transformLayer.find<int>(const Offset(0.0, 100.0)), 0);
expect(transformLayer.find<int>(const Offset(0.0, 200.0)), 0);
expect(transformLayer.find<int>(const Offset(0.0, 270.0)), 1);
expect(transformLayer.find<int>(const Offset(0.0, 400.0)), 1);
expect(transformLayer.find<int>(const Offset(0.0, 530.0)), 2);
expect(transformLayer.find<int>(const Offset(0.0, 100.0)).annotation, 0);
expect(transformLayer.find<int>(const Offset(0.0, 200.0)).annotation, 0);
expect(transformLayer.find<int>(const Offset(0.0, 270.0)).annotation, 1);
expect(transformLayer.find<int>(const Offset(0.0, 400.0)).annotation, 1);
expect(transformLayer.find<int>(const Offset(0.0, 530.0)).annotation, 2);
});
test('looks for child AnnotatedRegions before parents', () {
@ -101,7 +101,7 @@ void main() {
parent.append(child);
layer.append(parent);
expect(parent.find<int>(Offset.zero), 2);
expect(parent.find<int>(Offset.zero).annotation, 2);
});
test('looks for correct type', () {
@ -111,7 +111,7 @@ void main() {
layer.append(child2);
layer.append(child1);
expect(layer.find<String>(Offset.zero), 'hello');
expect(layer.find<String>(Offset.zero).annotation, 'hello');
});
test('does not clip Layer.find on an AnnotatedRegion with an unrelated type', () {
@ -121,7 +121,7 @@ void main() {
parent.append(child);
layer.append(parent);
expect(layer.find<int>(const Offset(100.0, 100.0)), 1);
expect(layer.find<int>(const Offset(100.0, 100.0)).annotation, 1);
});
test('handles non-invertable transforms', () {
@ -133,7 +133,7 @@ void main() {
parent.transform = Matrix4.diagonal3Values(1.0, 1.0, 1.0);
expect(parent.find<int>(const Offset(0.0, 0.0)), 1);
expect(parent.find<int>(const Offset(0.0, 0.0)).annotation, 1);
});
});
group('$AnnotatedRegion findAll', () {
@ -151,9 +151,9 @@ void main() {
i += 1;
}
expect(containerLayer.findAll<int>(const Offset(0.0, 1.0)), equals(<int>[0]));
expect(containerLayer.findAll<int>(const Offset(0.0, 101.0)),equals(<int>[1]));
expect(containerLayer.findAll<int>(const Offset(0.0, 201.0)), equals(<int>[2]));
expect(containerLayer.findAll<int>(const Offset(0.0, 1.0)).annotations.toList(), equals(<int>[0]));
expect(containerLayer.findAll<int>(const Offset(0.0, 101.0)).annotations.toList(), equals(<int>[1]));
expect(containerLayer.findAll<int>(const Offset(0.0, 201.0)).annotations.toList(), equals(<int>[2]));
});
test('finds a value within the clip in a ClipRectLayer', () {
@ -170,9 +170,9 @@ void main() {
i += 1;
}
expect(containerLayer.findAll<int>(const Offset(0.0, 1.0)), equals(<int>[0]));
expect(containerLayer.findAll<int>(const Offset(0.0, 101.0)), equals(<int>[1]));
expect(containerLayer.findAll<int>(const Offset(0.0, 201.0)), equals(<int>[2]));
expect(containerLayer.findAll<int>(const Offset(0.0, 1.0)).annotations.toList(), equals(<int>[0]));
expect(containerLayer.findAll<int>(const Offset(0.0, 101.0)).annotations.toList(), equals(<int>[1]));
expect(containerLayer.findAll<int>(const Offset(0.0, 201.0)).annotations.toList(), equals(<int>[2]));
});
@ -190,9 +190,9 @@ void main() {
i += 1;
}
expect(containerLayer.findAll<int>(const Offset(5.0, 5.0)), equals(<int>[0]));
expect(containerLayer.findAll<int>(const Offset(5.0, 105.0)), equals(<int>[1]));
expect(containerLayer.findAll<int>(const Offset(5.0, 205.0)), equals(<int>[2]));
expect(containerLayer.findAll<int>(const Offset(5.0, 5.0)).annotations.toList(), equals(<int>[0]));
expect(containerLayer.findAll<int>(const Offset(5.0, 105.0)).annotations.toList(), equals(<int>[1]));
expect(containerLayer.findAll<int>(const Offset(5.0, 205.0)).annotations.toList(), equals(<int>[2]));
});
test('finds a value under a TransformLayer', () {
@ -216,11 +216,11 @@ void main() {
i += 1;
}
expect(transformLayer.findAll<int>(const Offset(0.0, 100.0)), equals(<int>[0]));
expect(transformLayer.findAll<int>(const Offset(0.0, 200.0)), equals(<int>[0]));
expect(transformLayer.findAll<int>(const Offset(0.0, 270.0)), equals(<int>[1]));
expect(transformLayer.findAll<int>(const Offset(0.0, 400.0)), equals(<int>[1]));
expect(transformLayer.findAll<int>(const Offset(0.0, 530.0)), equals(<int>[2]));
expect(transformLayer.findAll<int>(const Offset(0.0, 100.0)).annotations.toList(), equals(<int>[0]));
expect(transformLayer.findAll<int>(const Offset(0.0, 200.0)).annotations.toList(), equals(<int>[0]));
expect(transformLayer.findAll<int>(const Offset(0.0, 270.0)).annotations.toList(), equals(<int>[1]));
expect(transformLayer.findAll<int>(const Offset(0.0, 400.0)).annotations.toList(), equals(<int>[1]));
expect(transformLayer.findAll<int>(const Offset(0.0, 530.0)).annotations.toList(), equals(<int>[2]));
});
test('finds multiple nested, overlapping regions', () {
@ -237,7 +237,7 @@ void main() {
parent.append(layer);
}
expect(parent.findAll<int>(const Offset(0.0, 0.0)), equals(<int>[3, 1, 2, 0,]));
expect(parent.findAll<int>(const Offset(0.0, 0.0)).annotations.toList(), equals(<int>[3, 1, 2, 0,]));
});
test('looks for child AnnotatedRegions before parents', () {
@ -251,7 +251,7 @@ void main() {
parent.append(child3);
layer.append(parent);
expect(parent.findAll<int>(Offset.zero), equals(<int>[4, 3, 2, 1]));
expect(parent.findAll<int>(Offset.zero).annotations.toList(), equals(<int>[4, 3, 2, 1]));
});
test('looks for correct type', () {
@ -261,7 +261,7 @@ void main() {
layer.append(child2);
layer.append(child1);
expect(layer.findAll<String>(Offset.zero), equals(<String>['hello']));
expect(layer.findAll<String>(Offset.zero).annotations.toList(), equals(<String>['hello']));
});
test('does not clip Layer.find on an AnnotatedRegion with an unrelated type', () {
@ -271,7 +271,7 @@ void main() {
parent.append(child);
layer.append(parent);
expect(layer.findAll<int>(const Offset(100.0, 100.0)), equals(<int>[1]));
expect(layer.findAll<int>(const Offset(100.0, 100.0)).annotations.toList(), equals(<int>[1]));
});
test('handles non-invertable transforms', () {
@ -279,11 +279,11 @@ void main() {
final TransformLayer parent = TransformLayer(transform: Matrix4.diagonal3Values(0.0, 1.0, 1.0));
parent.append(child);
expect(parent.findAll<int>(const Offset(0.0, 0.0)), equals(<int>[]));
expect(parent.findAll<int>(const Offset(0.0, 0.0)).annotations.toList(), equals(<int>[]));
parent.transform = Matrix4.diagonal3Values(1.0, 1.0, 1.0);
expect(parent.findAll<int>(const Offset(0.0, 0.0)), equals(<int>[1]));
expect(parent.findAll<int>(const Offset(0.0, 0.0)).annotations.toList(), equals(<int>[1]));
});
});
}

View file

@ -0,0 +1,758 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vector_math/vector_math_64.dart';
void main() {
test('ContainerLayer.findAll returns all results from its children', () {
final Layer root = _Layers(
ContainerLayer(),
children: <Object>[
_TestAnnotatedLayer(1, opaque: false),
_TestAnnotatedLayer(2, opaque: false),
_TestAnnotatedLayer(3, opaque: false),
]
).build();
expect(
root.findAll<int>(Offset.zero).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 3, localPosition: Offset.zero),
const AnnotationEntry<int>(annotation: 2, localPosition: Offset.zero),
const AnnotationEntry<int>(annotation: 1, localPosition: Offset.zero),
]),
);
});
test('ContainerLayer.find returns the first result from its children', () {
final Layer root = _Layers(
ContainerLayer(),
children: <Object>[
_TestAnnotatedLayer(1, opaque: false),
_TestAnnotatedLayer(2, opaque: false),
_TestAnnotatedLayer(3, opaque: false),
]
).build();
final AnnotationEntry<int> result = root.find<int>(Offset.zero);
expect(result.annotation, 3);
expect(result.localPosition, Offset.zero);
});
test('ContainerLayer.findAll returns empty result when finding nothing', () {
final Layer root = _Layers(
ContainerLayer(),
children: <Object>[
_TestAnnotatedLayer(1, opaque: false),
_TestAnnotatedLayer(2, opaque: false),
_TestAnnotatedLayer(3, opaque: false),
]
).build();
expect(root.findAll<double>(Offset.zero).entries.isEmpty, isTrue);
});
test('ContainerLayer.find returns null when finding nothing', () {
final Layer root = _Layers(
ContainerLayer(),
children: <Object>[
_TestAnnotatedLayer(1, opaque: false),
_TestAnnotatedLayer(2, opaque: false),
_TestAnnotatedLayer(3, opaque: false),
]
).build();
expect(root.find<double>(Offset.zero), isNull);
});
test('ContainerLayer.findAll stops at the first opaque child', () {
final Layer root = _Layers(
ContainerLayer(),
children: <Object>[
_TestAnnotatedLayer(1, opaque: false),
_TestAnnotatedLayer(2, opaque: true),
_TestAnnotatedLayer(3, opaque: false),
]
).build();
expect(
root.findAll<int>(Offset.zero).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 3, localPosition: Offset(0, 0)),
const AnnotationEntry<int>(annotation: 2, localPosition: Offset(0, 0)),
]),
);
});
test('ContainerLayer.findAll returns children\'s opacity (true)', () {
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
ContainerLayer(),
children: <Object>[
_TestAnnotatedLayer(2, opaque: true),
]
).build(),
);
expect(
root.findAll<int>(Offset.zero).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 2, localPosition: Offset(0, 0)),
]),
);
});
test('ContainerLayer.findAll returns children\'s opacity (false)', () {
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
ContainerLayer(),
children: <Object>[
_TestAnnotatedLayer(2, opaque: false),
],
).build(),
);
expect(
root.findAll<int>(Offset.zero).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 2, localPosition: Offset(0, 0)),
const AnnotationEntry<int>(annotation: 1000, localPosition: Offset(0, 0)),
]),
);
});
test('ContainerLayer.findAll returns false as opacity when finding nothing', () {
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
ContainerLayer(),
children: <Object>[
_TestAnnotatedLayer(2, opaque: false, size: Size.zero),
],
).build(),
);
expect(
root.findAll<int>(Offset.zero).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1000, localPosition: Offset(0, 0)),
]),
);
});
test('OffsetLayer.findAll respects offset', () {
const Offset insidePosition = Offset(-5, 5);
const Offset outsidePosition = Offset(5, 5);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
OffsetLayer(offset: const Offset(-10, 0)),
children: <Object>[
_TestAnnotatedLayer(1, opaque: true, size: const Size(10, 10)),
]
).build(),
);
expect(
root.findAll<int>(insidePosition).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1, localPosition: Offset(5, 5)),
]),
);
expect(
root.findAll<int>(outsidePosition).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1000, localPosition: Offset(5, 5)),
]),
);
});
test('ClipRectLayer.findAll respects clipRect', () {
const Offset insidePosition = Offset(11, 11);
const Offset outsidePosition = Offset(19, 19);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
ClipRectLayer(clipRect: const Offset(10, 10) & const Size(5, 5)),
children: <Object>[
_TestAnnotatedLayer(
1,
opaque: true,
size: const Size(10, 10),
offset: const Offset(10, 10),
),
]
).build(),
);
expect(
root.findAll<int>(insidePosition).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1, localPosition: insidePosition),
]),
);
expect(
root.findAll<int>(outsidePosition).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition),
]),
);
});
test('ClipRRectLayer.findAll respects clipRRect', () {
// For a curve of radius 4 centered at (4, 4),
// location (1, 1) is outside, while (2, 2) is inside.
// Here we shift this RRect by (10, 10).
final RRect rrect = RRect.fromRectAndRadius(
const Offset(10, 10) & const Size(10, 10),
const Radius.circular(4),
);
const Offset insidePosition = Offset(12, 12);
const Offset outsidePosition = Offset(11, 11);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
ClipRRectLayer(clipRRect: rrect),
children: <Object>[
_TestAnnotatedLayer(
1,
opaque: true,
size: const Size(10, 10),
offset: const Offset(10, 10),
),
]
).build(),
);
expect(
root.findAll<int>(insidePosition).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1, localPosition: insidePosition),
]),
);
expect(
root.findAll<int>(outsidePosition).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition),
]),
);
});
test('ClipPathLayer.findAll respects clipPath', () {
// For this triangle, location (1, 1) is inside, while (2, 2) is outside.
// 2
//
// | /
// | /
// 2 |/
final Path originalPath = Path();
originalPath.lineTo(2, 0);
originalPath.lineTo(0, 2);
originalPath.close();
// Shift this clip path by (10, 10).
final Path path = originalPath.shift(const Offset(10, 10));
const Offset insidePosition = Offset(11, 11);
const Offset outsidePosition = Offset(12, 12);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
ClipPathLayer(clipPath: path),
children: <Object>[
_TestAnnotatedLayer(
1,
opaque: true,
size: const Size(10, 10),
offset: const Offset(10, 10),
),
]
).build(),
);
expect(
root.findAll<int>(insidePosition).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1, localPosition: insidePosition),
]),
);
expect(
root.findAll<int>(outsidePosition).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition),
]),
);
});
test('TransformLayer.findAll respects transform', () {
// Matrix `transform` enlarges the target by (2x, 4x), then shift it by
// (10, 20).
final Matrix4 transform = Matrix4.diagonal3Values(2, 4, 1)
..setTranslation(Vector3(10, 20, 0));
// The original region is Offset(10, 10) & Size(10, 10)
// The transformed region is Offset(30, 60) & Size(20, 40)
const Offset insidePosition = Offset(40, 80);
const Offset outsidePosition = Offset(20, 40);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
TransformLayer(transform: transform),
children: <Object>[
_TestAnnotatedLayer(
1,
opaque: true,
size: const Size(10, 10),
offset: const Offset(10, 10),
),
]
).build(),
);
expect(
root.findAll<int>(insidePosition).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1, localPosition: Offset(15, 15)),
]),
);
expect(
root.findAll<int>(outsidePosition).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition),
]),
);
});
test('TransformLayer.findAll skips when transform is irreversible', () {
final Matrix4 transform = Matrix4.diagonal3Values(1, 0, 1);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
TransformLayer(transform: transform),
children: <Object>[
_TestAnnotatedLayer(1, opaque: true),
]
).build(),
);
expect(
root.findAll<int>(Offset.zero).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1000, localPosition: Offset.zero),
]),
);
});
test('PhysicalModelLayer.findAll respects clipPath', () {
// For this triangle, location (1, 1) is inside, while (2, 2) is outside.
// 2
//
// | /
// | /
// 2 |/
final Path originalPath = Path();
originalPath.lineTo(2, 0);
originalPath.lineTo(0, 2);
originalPath.close();
// Shift this clip path by (10, 10).
final Path path = originalPath.shift(const Offset(10, 10));
const Offset insidePosition = Offset(11, 11);
const Offset outsidePosition = Offset(12, 12);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
PhysicalModelLayer(
clipPath: path,
elevation: 10,
color: const Color.fromARGB(0, 0, 0, 0),
shadowColor: const Color.fromARGB(0, 0, 0, 0),
),
children: <Object>[
_TestAnnotatedLayer(
1,
opaque: true,
size: const Size(10, 10),
offset: const Offset(10, 10),
),
]
).build(),
);
expect(
root.findAll<int>(insidePosition).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1, localPosition: insidePosition),
]),
);
expect(
root.findAll<int>(outsidePosition).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition),
]),
);
});
test('LeaderLayer.findAll respects offset', () {
const Offset insidePosition = Offset(-5, 5);
const Offset outsidePosition = Offset(5, 5);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
LeaderLayer(
link: LayerLink(),
offset: const Offset(-10, 0),
),
children: <Object>[
_TestAnnotatedLayer(1, opaque: true, size: const Size(10, 10)),
]
).build(),
);
expect(
root.findAll<int>(insidePosition).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1, localPosition: Offset(5, 5)),
]),
);
expect(
root.findAll<int>(outsidePosition).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 1000, localPosition: outsidePosition),
]),
);
});
test('AnnotatedRegionLayer.findAll should append to the list '
'and return the given opacity (false) during a successful hit', () {
const Offset position = Offset(5, 5);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
AnnotatedRegionLayer<int>(1, opaque: false),
children: <Object>[
_TestAnnotatedLayer(2, opaque: false),
]
).build(),
);
expect(
root.findAll<int>(position).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 2, localPosition: position),
const AnnotationEntry<int>(annotation: 1, localPosition: position),
const AnnotationEntry<int>(annotation: 1000, localPosition: position),
]),
);
});
test('AnnotatedRegionLayer.findAll should append to the list '
'and return the given opacity (true) during a successful hit', () {
const Offset position = Offset(5, 5);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
AnnotatedRegionLayer<int>(1, opaque: true),
children: <Object>[
_TestAnnotatedLayer(2, opaque: false),
]
).build(),
);
expect(
root.findAll<int>(position).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 2, localPosition: position),
const AnnotationEntry<int>(annotation: 1, localPosition: position),
]),
);
});
test('AnnotatedRegionLayer.findAll has default opacity as false', () {
const Offset position = Offset(5, 5);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
AnnotatedRegionLayer<int>(1),
children: <Object>[
_TestAnnotatedLayer(2, opaque: false),
]
).build(),
);
expect(
root.findAll<int>(position).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 2, localPosition: position),
const AnnotationEntry<int>(annotation: 1, localPosition: position),
const AnnotationEntry<int>(annotation: 1000, localPosition: position),
]),
);
});
test('AnnotatedRegionLayer.findAll should still check children and return'
'children\'s opacity (false) during a failed hit', () {
const Offset position = Offset(5, 5);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
AnnotatedRegionLayer<int>(1, opaque: true, size: Size.zero),
children: <Object>[
_TestAnnotatedLayer(2, opaque: false),
]
).build(),
);
expect(
root.findAll<int>(position).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 2, localPosition: position),
const AnnotationEntry<int>(annotation: 1000, localPosition: position),
]),
);
});
test('AnnotatedRegionLayer.findAll should still check children and return'
'children\'s opacity (true) during a failed hit', () {
const Offset position = Offset(5, 5);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
AnnotatedRegionLayer<int>(1, opaque: false, size: Size.zero),
children: <Object>[
_TestAnnotatedLayer(2, opaque: true),
]
).build()
);
expect(
root.findAll<int>(position).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 2, localPosition: position),
]),
);
});
test('AnnotatedRegionLayer.findAll should not add to children\'s opacity '
'during a successful hit if it is not opaque', () {
const Offset position = Offset(5, 5);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
AnnotatedRegionLayer<int>(1, opaque: false),
children: <Object>[
_TestAnnotatedLayer(2, opaque: false),
]
).build()
);
expect(
root.findAll<int>(position).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 2, localPosition: position),
const AnnotationEntry<int>(annotation: 1, localPosition: position),
const AnnotationEntry<int>(annotation: 1000, localPosition: position),
]),
);
});
test('AnnotatedRegionLayer.findAll should add to children\'s opacity '
'during a successful hit if it is opaque', () {
const Offset position = Offset(5, 5);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
AnnotatedRegionLayer<int>(1, opaque: true),
children: <Object>[
_TestAnnotatedLayer(2, opaque: false),
]
).build()
);
expect(
root.findAll<int>(position).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 2, localPosition: position),
const AnnotationEntry<int>(annotation: 1, localPosition: position),
]),
);
});
test('AnnotatedRegionLayer.findAll should clip its annotation '
'using size and offset (positive)', () {
// The target position would have fallen outside if not for the offset.
const Offset position = Offset(100, 100);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
AnnotatedRegionLayer<int>(
1,
size: const Size(20, 20),
offset: const Offset(90, 90),
),
children: <Object>[
_TestAnnotatedLayer(
2,
opaque: false,
// Use this offset to make sure AnnotatedRegionLayer's offset
// does not affect its children.
offset: const Offset(20, 20),
size: const Size(110, 110),
),
]
).build()
);
expect(
root.findAll<int>(position).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 2, localPosition: position),
const AnnotationEntry<int>(annotation: 1, localPosition: position),
const AnnotationEntry<int>(annotation: 1000, localPosition: position),
]),
);
});
test('AnnotatedRegionLayer.findAll should clip its annotation '
'using size and offset (negative)', () {
// The target position would have fallen inside if not for the offset.
const Offset position = Offset(10, 10);
final Layer root = _appendAnnotationIfNotOpaque(1000,
_Layers(
AnnotatedRegionLayer<int>(
1,
size: const Size(20, 20),
offset: const Offset(90, 90),
),
children: <Object>[
_TestAnnotatedLayer(2, opaque: false, size: const Size(110, 110)),
]
).build()
);
expect(
root.findAll<int>(position).entries.toList(),
_equalToAnnotationResult<int>(<AnnotationEntry<int>>[
const AnnotationEntry<int>(annotation: 2, localPosition: position),
const AnnotationEntry<int>(annotation: 1000, localPosition: position),
]),
);
});
}
/// Append `value` to the result of the annotations test of `layer` if and only
/// if it is opaque at the given location.
///
/// It is a utility function that helps checking the opacity returned by
/// [Layer.findAnnotations].
/// Technically it is a [ContainerLayer] that contains `layer` followed by
/// another layer annotated with `value`.
Layer _appendAnnotationIfNotOpaque(int value, Layer layer) {
return _Layers(
ContainerLayer(),
children: <Object>[
_TestAnnotatedLayer(value, opaque: false),
layer,
],
).build();
}
// A utility class that helps building a layer tree.
class _Layers {
_Layers(this.root, {this.children});
final ContainerLayer root;
// Each element must be instance of Layer or _Layers.
final List<Object> children;
bool _assigned = false;
// Build the layer tree by calling each child's `build`, then append children
// to [root]. Returns the root.
Layer build() {
assert(!_assigned);
_assigned = true;
if (children != null) {
for (Object child in children) {
Layer layer;
if (child is Layer) {
layer = child;
} else if (child is _Layers) {
layer = child.build();
} else {
assert(false, 'Element of _Layers.children must be instance of Layer or _Layers');
}
root.append(layer);
}
}
return root;
}
}
// This layer's [findAnnotation] can be controlled by the given arguments.
class _TestAnnotatedLayer extends Layer {
_TestAnnotatedLayer(this.value, {
@required this.opaque,
this.offset = Offset.zero,
this.size,
});
// The value added to result in [findAnnotations] during a successful hit.
final int value;
// The return value of [findAnnotations] during a successful hit.
final bool opaque;
/// The [offset] is optionally used to translate the clip region for the
/// hit-testing of [find] by [offset].
///
/// If not provided, offset defaults to [Offset.zero].
///
/// Ignored if [size] is not set.
final Offset offset;
/// The [size] is optionally used to clip the hit-testing of [find].
///
/// If not provided, all offsets are considered to be contained within this
/// layer, unless an ancestor layer applies a clip.
///
/// If [offset] is set, then the offset is applied to the size region before
/// hit testing in [find].
final Size size;
@override
EngineLayer addToScene(SceneBuilder builder, [Offset layerOffset = Offset.zero]) {
return null;
}
// This implementation is hit when the type is `int` and position is within
// [offset] & [size]. If it is hit, it adds [value] to result and returns
// [opaque]; otherwise it directly returns false.
@override
bool findAnnotations<S>(
AnnotationResult<S> result,
Offset localPosition, {
bool onlyFirst,
}) {
if (S != int)
return false;
if (size != null && !(offset & size).contains(localPosition))
return false;
final Object untypedValue = value;
final S typedValue = untypedValue;
result.add(AnnotationEntry<S>(annotation: typedValue, localPosition: localPosition));
return opaque;
}
}
Matcher _equalToAnnotationResult<T>(List<AnnotationEntry<int>> list) {
return pairwiseCompare<AnnotationEntry<int>, AnnotationEntry<int>>(
list,
(AnnotationEntry<int> a, AnnotationEntry<int> b) {
return a.annotation == b.annotation && a.localPosition == b.localPosition;
},
'equal to',
);
}

View file

@ -34,12 +34,12 @@ void main() {
int result = RendererBinding.instance.renderView.debugLayer.find<int>(Offset(
10.0 * window.devicePixelRatio,
10.0 * window.devicePixelRatio,
));
))?.annotation;
expect(result, null);
result = RendererBinding.instance.renderView.debugLayer.find<int>(Offset(
50.0 * window.devicePixelRatio,
50.0 * window.devicePixelRatio,
));
)).annotation;
expect(result, 1);
});
}

View file

@ -752,6 +752,196 @@ void main() {
expect(paintCount, 1);
});
group('MouseRegion respects opacity:', () {
// A widget that contains 3 MouseRegions:
// y
// 0
// | A | 20
// | | B | |
// | | | 50
// | | | C | |
// | | | | 100
// | | | |
// | | 130
// 150
// x 0 20 50 100 130 150
Widget tripleRegions({bool opaqueC, void Function(String) addLog}) {
// Same as MouseRegion, but when opaque is null, use the default value.
Widget mouseRegionWithOptionalOpaque({
void Function(PointerEnterEvent e) onEnter,
void Function(PointerExitEvent e) onExit,
Widget child,
bool opaque,
}) {
if (opaque == null) {
return MouseRegion(onEnter: onEnter, onExit: onExit, child: child);
}
return MouseRegion(onEnter: onEnter, onExit: onExit, child: child, opaque: opaque);
}
return Directionality(
textDirection: TextDirection.ltr,
child: Align(
alignment: Alignment.topLeft,
child: MouseRegion(
onEnter: (PointerEnterEvent e) { addLog('enterA'); },
onExit: (PointerExitEvent e) { addLog('exitA'); },
child: SizedBox(
width: 150,
height: 150,
child: Stack(
children: <Widget>[
Positioned(
left: 20,
top: 20,
width: 80,
height: 80,
child: MouseRegion(
onEnter: (PointerEnterEvent e) { addLog('enterB'); },
onExit: (PointerExitEvent e) { addLog('exitB'); },
),
),
Positioned(
left: 50,
top: 50,
width: 80,
height: 80,
child: mouseRegionWithOptionalOpaque(
opaque: opaqueC,
onEnter: (PointerEnterEvent e) { addLog('enterC'); },
onExit: (PointerExitEvent e) { addLog('exitC'); },
),
),
],
),
),
),
),
);
}
testWidgets('a transparent one should allow MouseRegions behind it to receive pointers', (WidgetTester tester) async {
final List<String> logs = <String>[];
await tester.pumpWidget(tripleRegions(
opaqueC: false,
addLog: (String log) => logs.add(log),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
// Move to the overlapping area
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
expect(logs, <String>['enterA', 'enterC', 'enterB']);
logs.clear();
// Move to the B only area
await gesture.moveTo(const Offset(25, 75));
await tester.pumpAndSettle();
expect(logs, <String>['exitC']);
logs.clear();
// Move back to the overlapping area
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
expect(logs, <String>['enterC']);
logs.clear();
// Move to the C only area
await gesture.moveTo(const Offset(125, 75));
await tester.pumpAndSettle();
expect(logs, <String>['exitB']);
logs.clear();
// Move back to the overlapping area
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
expect(logs, <String>['enterB']);
logs.clear();
// Move out
await gesture.moveTo(const Offset(160, 160));
await tester.pumpAndSettle();
expect(logs, <String>['exitA', 'exitB', 'exitC']);
});
testWidgets('an opaque one should prevent MouseRegions behind it receiving pointers', (WidgetTester tester) async {
final List<String> logs = <String>[];
await tester.pumpWidget(tripleRegions(
opaqueC: true,
addLog: (String log) => logs.add(log),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
// Move to the overlapping area
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
expect(logs, <String>['enterA', 'enterC']);
logs.clear();
// Move to the B only area
await gesture.moveTo(const Offset(25, 75));
await tester.pumpAndSettle();
expect(logs, <String>['enterB', 'exitC']);
logs.clear();
// Move back to the overlapping area
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
expect(logs, <String>['enterC', 'exitB']);
logs.clear();
// Move to the C only area
await gesture.moveTo(const Offset(125, 75));
await tester.pumpAndSettle();
expect(logs, <String>[]);
logs.clear();
// Move back to the overlapping area
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
expect(logs, <String>[]);
logs.clear();
// Move out
await gesture.moveTo(const Offset(160, 160));
await tester.pumpAndSettle();
expect(logs, <String>['exitA', 'exitC']);
});
testWidgets('opaque should default to true', (WidgetTester tester) async {
final List<String> logs = <String>[];
await tester.pumpWidget(tripleRegions(
opaqueC: null,
addLog: (String log) => logs.add(log),
));
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await gesture.addPointer();
addTearDown(gesture.removePointer);
await tester.pumpAndSettle();
// Move to the overlapping area
await gesture.moveTo(const Offset(75, 75));
await tester.pumpAndSettle();
expect(logs, <String>['enterA', 'enterC']);
logs.clear();
// Move out
await gesture.moveTo(const Offset(160, 160));
await tester.pumpAndSettle();
expect(logs, <String>['exitA', 'exitC']);
});
});
testWidgets('RenderMouseRegion\'s debugFillProperties when default', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
RenderMouseRegion().debugFillProperties(builder);