mirror of
https://github.com/flutter/flutter
synced 2024-10-13 03:32:55 +00:00
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:
parent
6b4a10ae53
commit
a71d69ba67
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
758
packages/flutter/test/rendering/layer_annotations_test.dart
Normal file
758
packages/flutter/test/rendering/layer_annotations_test.dart
Normal 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',
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue