mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
Refactor mouse hit testing system: Direct mouse hit test (#59883)
This commit is contained in:
parent
4b12050112
commit
6f4c4b3cf8
|
@ -908,7 +908,7 @@ class PointerEnterEvent extends PointerEvent {
|
|||
down: event?.down,
|
||||
synthesized: event?.synthesized,
|
||||
transform: event?.transform,
|
||||
original: event?.original as PointerEnterEvent,
|
||||
original: null,
|
||||
);
|
||||
|
||||
@override
|
||||
|
@ -1054,7 +1054,7 @@ class PointerExitEvent extends PointerEvent {
|
|||
down: event?.down,
|
||||
synthesized: event?.synthesized,
|
||||
transform: event?.transform,
|
||||
original: event?.original as PointerExitEvent,
|
||||
original: null,
|
||||
);
|
||||
|
||||
@override
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
|
||||
// @dart = 2.8
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
|
@ -73,12 +71,51 @@ class HitTestEntry {
|
|||
Matrix4 _transform;
|
||||
}
|
||||
|
||||
// A type of data that can be applied to a matrix by left-multiplication.
|
||||
@immutable
|
||||
abstract class _TransformPart {
|
||||
const _TransformPart();
|
||||
|
||||
// Apply this transform part to `rhs` from the left.
|
||||
//
|
||||
// This should work as if this transform part is first converted to a matrix
|
||||
// and then left-multiplied to `rhs`.
|
||||
//
|
||||
// For example, if this transform part is a vector `v1`, whose corresponding
|
||||
// matrix is `m1 = Matrix4.translation(v1)`, then the result of
|
||||
// `_VectorTransformPart(v1).multiply(rhs)` should equal to `m1 * rhs`.
|
||||
Matrix4 multiply(Matrix4 rhs);
|
||||
}
|
||||
|
||||
class _MatrixTransformPart extends _TransformPart {
|
||||
const _MatrixTransformPart(this.matrix);
|
||||
|
||||
final Matrix4 matrix;
|
||||
|
||||
@override
|
||||
Matrix4 multiply(Matrix4 rhs) {
|
||||
return matrix * rhs as Matrix4;
|
||||
}
|
||||
}
|
||||
|
||||
class _OffsetTransformPart extends _TransformPart {
|
||||
const _OffsetTransformPart(this.offset);
|
||||
|
||||
final Offset offset;
|
||||
|
||||
@override
|
||||
Matrix4 multiply(Matrix4 rhs) {
|
||||
return rhs.clone()..leftTranslate(offset.dx, offset.dy);
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of performing a hit test.
|
||||
class HitTestResult {
|
||||
/// Creates an empty hit test result.
|
||||
HitTestResult()
|
||||
: _path = <HitTestEntry>[],
|
||||
_transforms = Queue<Matrix4>();
|
||||
_transforms = <Matrix4>[Matrix4.identity()],
|
||||
_localTransforms = <_TransformPart>[];
|
||||
|
||||
/// Wraps `result` (usually a subtype of [HitTestResult]) to create a
|
||||
/// generic [HitTestResult].
|
||||
|
@ -88,7 +125,8 @@ class HitTestResult {
|
|||
/// structure to store [HitTestEntry]s).
|
||||
HitTestResult.wrap(HitTestResult result)
|
||||
: _path = result._path,
|
||||
_transforms = result._transforms;
|
||||
_transforms = result._transforms,
|
||||
_localTransforms = result._localTransforms;
|
||||
|
||||
/// An unmodifiable list of [HitTestEntry] objects recorded during the hit test.
|
||||
///
|
||||
|
@ -98,7 +136,40 @@ class HitTestResult {
|
|||
Iterable<HitTestEntry> get path => _path;
|
||||
final List<HitTestEntry> _path;
|
||||
|
||||
final Queue<Matrix4> _transforms;
|
||||
// A stack of transform parts.
|
||||
//
|
||||
// The transform part stack leading from global to the current object is stored
|
||||
// in 2 parts:
|
||||
//
|
||||
// * `_transforms` are globalized matrices, meaning they have been multiplied
|
||||
// by the ancestors and are thus relative to the global coordinate space.
|
||||
// * `_localTransforms` are local transform parts, which are relative to the
|
||||
// parent's coordinate space.
|
||||
//
|
||||
// When new transform parts are added they're appended to `_localTransforms`,
|
||||
// and are converted to global ones and moved to `_transforms` only when used.
|
||||
final List<Matrix4> _transforms;
|
||||
final List<_TransformPart> _localTransforms;
|
||||
|
||||
// Globalize all transform parts in `_localTransforms` and move them to
|
||||
// _transforms.
|
||||
void _globalizeTransforms() {
|
||||
if (_localTransforms.isEmpty) {
|
||||
return;
|
||||
}
|
||||
Matrix4 last = _transforms.last;
|
||||
for (final _TransformPart part in _localTransforms) {
|
||||
last = part.multiply(last);
|
||||
_transforms.add(last);
|
||||
}
|
||||
_localTransforms.clear();
|
||||
}
|
||||
|
||||
Matrix4 get _lastTransform {
|
||||
_globalizeTransforms();
|
||||
assert(_localTransforms.isEmpty);
|
||||
return _transforms.last;
|
||||
}
|
||||
|
||||
/// Add a [HitTestEntry] to the path.
|
||||
///
|
||||
|
@ -107,7 +178,7 @@ class HitTestResult {
|
|||
/// upward walk of the tree being hit tested.
|
||||
void add(HitTestEntry entry) {
|
||||
assert(entry._transform == null);
|
||||
entry._transform = _transforms.isEmpty ? null : _transforms.last;
|
||||
entry._transform = _lastTransform;
|
||||
_path.add(entry);
|
||||
}
|
||||
|
||||
|
@ -125,6 +196,9 @@ class HitTestResult {
|
|||
/// through [PointerEvent.removePerspectiveTransform] to remove
|
||||
/// the perspective component.
|
||||
///
|
||||
/// If the provided `transform` is a translation matrix, it is much faster
|
||||
/// to use [pushOffset] with the translation offset instead.
|
||||
///
|
||||
/// [HitTestable]s need to call this method indirectly through a convenience
|
||||
/// method defined on a subclass before hit testing a child that does not
|
||||
/// have the same origin as the parent. After hit testing the child,
|
||||
|
@ -132,10 +206,10 @@ class HitTestResult {
|
|||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [pushOffset], which is similar to [pushTransform] but is limited to
|
||||
/// translations, and is faster in such cases.
|
||||
/// * [BoxHitTestResult.addWithPaintTransform], which is a public wrapper
|
||||
/// around this function for hit testing on [RenderBox]s.
|
||||
/// * [SliverHitTestResult.addWithAxisOffset], which is a public wrapper
|
||||
/// around this function for hit testing on [RenderSliver]s.
|
||||
@protected
|
||||
void pushTransform(Matrix4 transform) {
|
||||
assert(transform != null);
|
||||
|
@ -148,26 +222,60 @@ class HitTestResult {
|
|||
'matrix through PointerEvent.removePerspectiveTransform? '
|
||||
'The provided matrix is:\n$transform'
|
||||
);
|
||||
_transforms.add(_transforms.isEmpty ? transform : (transform * _transforms.last as Matrix4));
|
||||
_localTransforms.add(_MatrixTransformPart(transform));
|
||||
}
|
||||
|
||||
/// Removes the last transform added via [pushTransform].
|
||||
/// Pushes a new translation offset that is to be applied to all future
|
||||
/// [HitTestEntry]s added via [add] until it is removed via [popTransform].
|
||||
///
|
||||
/// This method is only to be used by subclasses, which must provide
|
||||
/// coordinate space specific public wrappers around this function for their
|
||||
/// users (see [BoxHitTestResult.addWithPaintOffset] for such an example).
|
||||
///
|
||||
/// The provided `offset` should describe how to transform [PointerEvent]s from
|
||||
/// the coordinate space of the method caller to the coordinate space of its
|
||||
/// children. Usually `offset` is the inverse of the offset of the child
|
||||
/// relative to the parent.
|
||||
///
|
||||
/// [HitTestable]s need to call this method indirectly through a convenience
|
||||
/// method defined on a subclass before hit testing a child that does not
|
||||
/// have the same origin as the parent. After hit testing the child,
|
||||
/// [popTransform] has to be called to remove the child-specific `transform`.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [pushTransform], which is similar to [pushOffset] but allows general
|
||||
/// transform besides translation.
|
||||
/// * [BoxHitTestResult.addWithPaintOffset], which is a public wrapper
|
||||
/// around this function for hit testing on [RenderBox]s.
|
||||
/// * [SliverHitTestResult.addWithAxisOffset], which is a public wrapper
|
||||
/// around this function for hit testing on [RenderSliver]s.
|
||||
@protected
|
||||
void pushOffset(Offset offset) {
|
||||
assert(offset != null);
|
||||
_localTransforms.add(_OffsetTransformPart(offset));
|
||||
}
|
||||
|
||||
/// Removes the last transform added via [pushTransform] or [pushOffset].
|
||||
///
|
||||
/// This method is only to be used by subclasses, which must provide
|
||||
/// coordinate space specific public wrappers around this function for their
|
||||
/// users (see [BoxHitTestResult.addWithPaintTransform] for such an example).
|
||||
///
|
||||
/// This method must be called after hit testing is done on a child that
|
||||
/// required a call to [pushTransform].
|
||||
/// required a call to [pushTransform] or [pushOffset].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [pushTransform], which describes the use case of this function pair in
|
||||
/// more details.
|
||||
/// * [pushTransform] and [pushOffset], which describes the use case of this
|
||||
/// function pair in more details.
|
||||
@protected
|
||||
void popTransform() {
|
||||
if (_localTransforms.isNotEmpty)
|
||||
_localTransforms.removeLast();
|
||||
else
|
||||
_transforms.removeLast();
|
||||
assert(_transforms.isNotEmpty);
|
||||
_transforms.removeLast();
|
||||
}
|
||||
|
||||
bool _debugVectorMoreOrLessEquals(Vector4 a, Vector4 b, { double epsilon = precisionErrorTolerance }) {
|
||||
|
|
|
@ -1148,12 +1148,12 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
|
|||
},
|
||||
);
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: !_isEnabled,
|
||||
child: MouseRegion(
|
||||
cursor: effectiveMouseCursor,
|
||||
onEnter: (PointerEnterEvent event) => _handleHover(true),
|
||||
onExit: (PointerExitEvent event) => _handleHover(false),
|
||||
return MouseRegion(
|
||||
cursor: effectiveMouseCursor,
|
||||
onEnter: (PointerEnterEvent event) => _handleHover(true),
|
||||
onExit: (PointerExitEvent event) => _handleHover(false),
|
||||
child: IgnorePointer(
|
||||
ignoring: !_isEnabled,
|
||||
child: AnimatedBuilder(
|
||||
animation: controller, // changes the _currentLength
|
||||
builder: (BuildContext context, Widget child) {
|
||||
|
|
|
@ -764,11 +764,17 @@ class BoxHitTestResult extends HitTestResult {
|
|||
@required BoxHitTest hitTest,
|
||||
}) {
|
||||
assert(hitTest != null);
|
||||
return addWithRawTransform(
|
||||
transform: offset != null ? Matrix4.translationValues(-offset.dx, -offset.dy, 0.0) : null,
|
||||
position: position,
|
||||
hitTest: hitTest,
|
||||
);
|
||||
final Offset transformedPosition = position == null || offset == null
|
||||
? position
|
||||
: position - offset;
|
||||
if (offset != null) {
|
||||
pushOffset(-offset);
|
||||
}
|
||||
final bool isHit = hitTest(this, transformedPosition);
|
||||
if (offset != null) {
|
||||
popTransform();
|
||||
}
|
||||
return isHit;
|
||||
}
|
||||
|
||||
/// Transforms `position` to the local coordinate system of a child for
|
||||
|
|
|
@ -14,7 +14,6 @@ import 'package:flutter/painting.dart';
|
|||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'debug.dart';
|
||||
import 'mouse_tracking.dart';
|
||||
|
||||
/// Information collected for an annotation that is found in the layer tree.
|
||||
///
|
||||
|
@ -633,7 +632,6 @@ class PlatformViewLayer extends Layer {
|
|||
PlatformViewLayer({
|
||||
@required this.rect,
|
||||
@required this.viewId,
|
||||
this.hoverAnnotation,
|
||||
}) : assert(rect != null),
|
||||
assert(viewId != null);
|
||||
|
||||
|
@ -645,25 +643,6 @@ class PlatformViewLayer extends Layer {
|
|||
/// A UIView with this identifier must have been created by [PlatformViewsServices.initUiKitView].
|
||||
final int viewId;
|
||||
|
||||
/// [MouseTrackerAnnotation] that handles mouse events for this layer.
|
||||
///
|
||||
/// If [hoverAnnotation] is non-null, [PlatformViewLayer] will annotate the
|
||||
/// region of this platform view such that annotation callbacks will receive
|
||||
/// mouse events, including mouse enter, exit, and hover, but not including
|
||||
/// mouse down, move, and up. The layer will be treated as opaque during an
|
||||
/// annotation search, which will prevent layers behind it from receiving
|
||||
/// these events.
|
||||
///
|
||||
/// By default, [hoverAnnotation] is null, and [PlatformViewLayer] will not
|
||||
/// receive mouse events, and will therefore appear translucent during the
|
||||
/// annotation search.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [MouseRegion], which explains more about the mouse events and opacity
|
||||
/// during annotation search.
|
||||
final MouseTrackerAnnotation hoverAnnotation;
|
||||
|
||||
@override
|
||||
void addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
|
||||
final Rect shiftedRect = layerOffset == Offset.zero ? rect : rect.shift(layerOffset);
|
||||
|
@ -674,24 +653,6 @@ class PlatformViewLayer extends Layer {
|
|||
height: shiftedRect.height,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@protected
|
||||
bool findAnnotations<S>(AnnotationResult<S> result, Offset localPosition, { @required bool onlyFirst }) {
|
||||
if (hoverAnnotation == null || !rect.contains(localPosition)) {
|
||||
return false;
|
||||
}
|
||||
if (S == MouseTrackerAnnotation) {
|
||||
final Object untypedValue = hoverAnnotation;
|
||||
final S typedValue = untypedValue as S;
|
||||
result.add(AnnotationEntry<S>(
|
||||
annotation: typedValue,
|
||||
localPosition: localPosition,
|
||||
));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer that indicates to the compositor that it should display
|
||||
|
|
|
@ -622,7 +622,6 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin {
|
|||
context.addLayer(PlatformViewLayer(
|
||||
rect: offset & size,
|
||||
viewId: _controller.viewId,
|
||||
hoverAnnotation: _hoverAnnotation,
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -636,32 +635,13 @@ class PlatformViewRenderBox extends RenderBox with _PlatformViewGestureMixin {
|
|||
}
|
||||
|
||||
/// The Mixin handling the pointer events and gestures of a platform view render box.
|
||||
mixin _PlatformViewGestureMixin on RenderBox {
|
||||
mixin _PlatformViewGestureMixin on RenderBox implements MouseTrackerAnnotation {
|
||||
|
||||
/// How to behave during hit testing.
|
||||
// The implicit setter is enough here as changing this value will just affect
|
||||
// any newly arriving events there's nothing we need to invalidate.
|
||||
PlatformViewHitTestBehavior hitTestBehavior;
|
||||
|
||||
/// [MouseTrackerAnnotation] associated with the platform view layer.
|
||||
///
|
||||
/// Gesture recognizers don't receive hover events due to the performance
|
||||
/// cost associated with hit testing a sequence of potentially thousands of
|
||||
/// events -- move events only hit-test the down event, then cache the result
|
||||
/// and apply it to all subsequent move events, but there is no down event
|
||||
/// for a hover. To support native hover gesture handling by platform views,
|
||||
/// we attach/detach this layer annotation as necessary.
|
||||
MouseTrackerAnnotation get _hoverAnnotation {
|
||||
return _cachedHoverAnnotation ??= MouseTrackerAnnotation(
|
||||
onHover: (PointerHoverEvent event) {
|
||||
if (_handlePointerEvent != null)
|
||||
_handlePointerEvent(event);
|
||||
},
|
||||
cursor: MouseCursor.uncontrolled,
|
||||
);
|
||||
}
|
||||
MouseTrackerAnnotation _cachedHoverAnnotation;
|
||||
|
||||
_HandlePointerEvent _handlePointerEvent;
|
||||
|
||||
/// {@macro flutter.rendering.platformView.updateGestureRecognizers}
|
||||
|
@ -696,6 +676,22 @@ mixin _PlatformViewGestureMixin on RenderBox {
|
|||
@override
|
||||
bool hitTestSelf(Offset position) => hitTestBehavior != PlatformViewHitTestBehavior.transparent;
|
||||
|
||||
@override
|
||||
PointerEnterEventListener get onEnter => null;
|
||||
|
||||
@override
|
||||
PointerHoverEventListener get onHover => _handleHover;
|
||||
void _handleHover(PointerHoverEvent event) {
|
||||
if (_handlePointerEvent != null)
|
||||
_handlePointerEvent(event);
|
||||
}
|
||||
|
||||
@override
|
||||
PointerExitEventListener get onExit => null;
|
||||
|
||||
@override
|
||||
MouseCursor get cursor => MouseCursor.uncontrolled;
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event, HitTestEntry entry) {
|
||||
if (event is PointerDownEvent) {
|
||||
|
|
|
@ -2720,6 +2720,15 @@ class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation
|
|||
_annotationIsActive = false,
|
||||
super(child);
|
||||
|
||||
@protected
|
||||
@override
|
||||
bool hitTestSelf(Offset position) => true;
|
||||
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, { @required Offset position }) {
|
||||
return super.hitTest(result, position: position) && _opaque;
|
||||
}
|
||||
|
||||
/// Whether this object should prevent [RenderMouseRegion]s visually behind it
|
||||
/// from detecting the pointer, thus affecting how their [onHover], [onEnter],
|
||||
/// and [onExit] behave.
|
||||
|
@ -2838,25 +2847,6 @@ class RenderMouseRegion extends RenderProxyBox implements MouseTrackerAnnotation
|
|||
super.detach();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get needsCompositing => super.needsCompositing || _annotationIsActive;
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (_annotationIsActive) {
|
||||
// Annotated region layers are not retained because they do not create engine layers.
|
||||
final AnnotatedRegionLayer<MouseTrackerAnnotation> layer = AnnotatedRegionLayer<MouseTrackerAnnotation>(
|
||||
this,
|
||||
size: size,
|
||||
offset: offset,
|
||||
opaque: opaque,
|
||||
);
|
||||
context.pushLayer(layer, super.paint, offset);
|
||||
} else {
|
||||
super.paint(context, offset);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void performResize() {
|
||||
size = constraints.biggest;
|
||||
|
|
|
@ -861,7 +861,7 @@ class SliverHitTestResult extends HitTestResult {
|
|||
assert(crossAxisPosition != null);
|
||||
assert(hitTest != null);
|
||||
if (paintOffset != null) {
|
||||
pushTransform(Matrix4.translationValues(-paintOffset.dx, -paintOffset.dy, 0));
|
||||
pushOffset(-paintOffset);
|
||||
}
|
||||
final bool isHit = hitTest(
|
||||
this,
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'dart:io' show Platform;
|
|||
import 'dart:ui' as ui show Scene, SceneBuilder, Window;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
|
@ -201,9 +202,17 @@ 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.findAllAnnotations<MouseTrackerAnnotation>(
|
||||
position * configuration.devicePixelRatio
|
||||
).annotations;
|
||||
final BoxHitTestResult result = BoxHitTestResult();
|
||||
if (child != null)
|
||||
child.hitTest(result, position: position);
|
||||
result.add(HitTestEntry(this));
|
||||
final List<MouseTrackerAnnotation> annotations = <MouseTrackerAnnotation>[];
|
||||
for (final HitTestEntry entry in result.path) {
|
||||
if (entry.target is MouseTrackerAnnotation) {
|
||||
annotations.add(entry.target as MouseTrackerAnnotation);
|
||||
}
|
||||
}
|
||||
return annotations;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -36,6 +36,94 @@ void main() {
|
|||
expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2, entry3]));
|
||||
expect(entry3.transform, transform);
|
||||
});
|
||||
|
||||
test('HitTestResult should correctly push and pop transforms', () {
|
||||
Matrix4 currentTransform(HitTestResult targetResult) {
|
||||
final HitTestEntry entry = HitTestEntry(_DummyHitTestTarget());
|
||||
targetResult.add(entry);
|
||||
return entry.transform;
|
||||
}
|
||||
|
||||
final MyHitTestResult result = MyHitTestResult();
|
||||
|
||||
final Matrix4 m1 = Matrix4.translationValues(10, 20, 0);
|
||||
final Matrix4 m2 = Matrix4.rotationZ(1);
|
||||
final Matrix4 m3 = Matrix4.diagonal3Values(1.1, 1.2, 1.0);
|
||||
|
||||
result.publicPushTransform(m1);
|
||||
expect(currentTransform(result), equals(m1));
|
||||
|
||||
result.publicPushTransform(m2);
|
||||
expect(currentTransform(result), equals(m2 * m1));
|
||||
expect(currentTransform(result), equals(m2 * m1)); // Test repeated add
|
||||
|
||||
// The `wrapped` is wrapped at [m1, m2]
|
||||
final MyHitTestResult wrapped = MyHitTestResult.wrap(result);
|
||||
expect(currentTransform(wrapped), equals(m2 * m1));
|
||||
|
||||
result.publicPushTransform(m3);
|
||||
expect(currentTransform(result), equals(m3 * m2 * m1));
|
||||
expect(currentTransform(wrapped), equals(m3 * m2 * m1));
|
||||
|
||||
result.publicPopTransform();
|
||||
result.publicPopTransform();
|
||||
expect(currentTransform(result), equals(m1));
|
||||
|
||||
result.publicPopTransform();
|
||||
result.publicPushTransform(m3);
|
||||
expect(currentTransform(result), equals(m3));
|
||||
|
||||
result.publicPushTransform(m2);
|
||||
expect(currentTransform(result), equals(m2 * m3));
|
||||
});
|
||||
|
||||
test('HitTestResult should correctly push and pop offsets', () {
|
||||
Matrix4 currentTransform(HitTestResult targetResult) {
|
||||
final HitTestEntry entry = HitTestEntry(_DummyHitTestTarget());
|
||||
targetResult.add(entry);
|
||||
return entry.transform;
|
||||
}
|
||||
|
||||
final MyHitTestResult result = MyHitTestResult();
|
||||
|
||||
final Matrix4 m1 = Matrix4.rotationZ(1);
|
||||
final Matrix4 m2 = Matrix4.diagonal3Values(1.1, 1.2, 1.0);
|
||||
const Offset o3 = Offset(10, 20);
|
||||
final Matrix4 m3 = Matrix4.translationValues(o3.dx, o3.dy, 0.0);
|
||||
|
||||
// Test pushing offset as the first element
|
||||
result.publicPushOffset(o3);
|
||||
expect(currentTransform(result), equals(m3));
|
||||
result.publicPopTransform();
|
||||
|
||||
result.publicPushOffset(o3);
|
||||
result.publicPushTransform(m1);
|
||||
expect(currentTransform(result), equals(m1 * m3));
|
||||
expect(currentTransform(result), equals(m1 * m3)); // Test repeated add
|
||||
|
||||
// The `wrapped` is wrapped at [m1, m2]
|
||||
final MyHitTestResult wrapped = MyHitTestResult.wrap(result);
|
||||
expect(currentTransform(wrapped), equals(m1 * m3));
|
||||
|
||||
result.publicPushTransform(m2);
|
||||
expect(currentTransform(result), equals(m2 * m1 * m3));
|
||||
expect(currentTransform(wrapped), equals(m2 * m1 * m3));
|
||||
|
||||
result.publicPopTransform();
|
||||
result.publicPopTransform();
|
||||
result.publicPopTransform();
|
||||
expect(currentTransform(result), equals(Matrix4.identity()));
|
||||
|
||||
result.publicPushTransform(m2);
|
||||
result.publicPushOffset(o3);
|
||||
result.publicPushTransform(m1);
|
||||
|
||||
expect(currentTransform(result), equals(m1 * m3 * m2));
|
||||
|
||||
result.publicPopTransform();
|
||||
|
||||
expect(currentTransform(result), equals(m3 * m2));
|
||||
});
|
||||
}
|
||||
|
||||
class _DummyHitTestTarget implements HitTestTarget {
|
||||
|
@ -46,5 +134,10 @@ class _DummyHitTestTarget implements HitTestTarget {
|
|||
}
|
||||
|
||||
class MyHitTestResult extends HitTestResult {
|
||||
MyHitTestResult();
|
||||
MyHitTestResult.wrap(HitTestResult result) : super.wrap(result);
|
||||
|
||||
void publicPushTransform(Matrix4 transform) => pushTransform(transform);
|
||||
void publicPushOffset(Offset offset) => pushOffset(offset);
|
||||
void publicPopTransform() => popTransform();
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
// @dart = 2.8
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
@ -398,6 +399,162 @@ void main() {
|
|||
);
|
||||
// TODO(Piinks): Remove skip once web goldens are supported, https://github.com/flutter/flutter/issues/40297
|
||||
}, skip: isBrowser);
|
||||
|
||||
testWidgets('IgnorePointer ignores pointers', (WidgetTester tester) async {
|
||||
final List<String> logs = <String>[];
|
||||
Widget target({bool ignoring}) => Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: SizedBox(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: Listener(
|
||||
onPointerDown: (_) { logs.add('down1'); },
|
||||
child: MouseRegion(
|
||||
onEnter: (_) { logs.add('enter1'); },
|
||||
onExit: (_) { logs.add('exit1'); },
|
||||
cursor: SystemMouseCursors.forbidden,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Listener(
|
||||
onPointerDown: (_) { logs.add('down2'); },
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (_) { logs.add('enter2'); },
|
||||
onExit: (_) { logs.add('exit2'); },
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
ignoring: ignoring,
|
||||
child: Listener(
|
||||
onPointerDown: (_) { logs.add('down3'); },
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.text,
|
||||
onEnter: (_) { logs.add('enter3'); },
|
||||
onExit: (_) { logs.add('exit3'); },
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(pointer: 1, kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer(location: const Offset(200, 200));
|
||||
addTearDown(gesture.removePointer);
|
||||
|
||||
await tester.pumpWidget(target(ignoring: true));
|
||||
expect(logs, isEmpty);
|
||||
|
||||
await gesture.moveTo(const Offset(50, 50));
|
||||
expect(logs, <String>['enter1', 'enter2']);
|
||||
logs.clear();
|
||||
|
||||
await gesture.down(const Offset(50, 50));
|
||||
expect(logs, <String>['down2', 'down1']);
|
||||
logs.clear();
|
||||
|
||||
await gesture.up();
|
||||
expect(logs, isEmpty);
|
||||
|
||||
await tester.pumpWidget(target(ignoring: false));
|
||||
expect(logs, <String>['exit2', 'enter3']);
|
||||
logs.clear();
|
||||
|
||||
await gesture.down(const Offset(50, 50));
|
||||
expect(logs, <String>['down3', 'down1']);
|
||||
logs.clear();
|
||||
|
||||
await gesture.up();
|
||||
expect(logs, isEmpty);
|
||||
|
||||
await tester.pumpWidget(target(ignoring: true));
|
||||
expect(logs, <String>['exit3', 'enter2']);
|
||||
logs.clear();
|
||||
});
|
||||
|
||||
testWidgets('AbsorbPointer absorbs pointers', (WidgetTester tester) async {
|
||||
final List<String> logs = <String>[];
|
||||
Widget target({bool absorbing}) => Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: SizedBox(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: Listener(
|
||||
onPointerDown: (_) { logs.add('down1'); },
|
||||
child: MouseRegion(
|
||||
onEnter: (_) { logs.add('enter1'); },
|
||||
onExit: (_) { logs.add('exit1'); },
|
||||
cursor: SystemMouseCursors.forbidden,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Listener(
|
||||
onPointerDown: (_) { logs.add('down2'); },
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (_) { logs.add('enter2'); },
|
||||
onExit: (_) { logs.add('exit2'); },
|
||||
),
|
||||
),
|
||||
AbsorbPointer(
|
||||
absorbing: absorbing,
|
||||
child: Listener(
|
||||
onPointerDown: (_) { logs.add('down3'); },
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.text,
|
||||
onEnter: (_) { logs.add('enter3'); },
|
||||
onExit: (_) { logs.add('exit3'); },
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(pointer: 1, kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer(location: const Offset(200, 200));
|
||||
addTearDown(gesture.removePointer);
|
||||
|
||||
await tester.pumpWidget(target(absorbing: true));
|
||||
expect(logs, isEmpty);
|
||||
|
||||
await gesture.moveTo(const Offset(50, 50));
|
||||
expect(logs, <String>['enter1']);
|
||||
logs.clear();
|
||||
|
||||
await gesture.down(const Offset(50, 50));
|
||||
expect(logs, <String>['down1']);
|
||||
logs.clear();
|
||||
|
||||
await gesture.up();
|
||||
expect(logs, isEmpty);
|
||||
|
||||
await tester.pumpWidget(target(absorbing: false));
|
||||
expect(logs, <String>['enter3']);
|
||||
logs.clear();
|
||||
|
||||
await gesture.down(const Offset(50, 50));
|
||||
expect(logs, <String>['down3', 'down1']);
|
||||
logs.clear();
|
||||
|
||||
await gesture.up();
|
||||
expect(logs, isEmpty);
|
||||
|
||||
await tester.pumpWidget(target(absorbing: true));
|
||||
expect(logs, <String>['exit3']);
|
||||
logs.clear();
|
||||
});
|
||||
}
|
||||
|
||||
HitsRenderBox hits(RenderBox renderBox) => HitsRenderBox(renderBox);
|
||||
|
|
|
@ -436,7 +436,7 @@ void main() {
|
|||
events.clear();
|
||||
});
|
||||
|
||||
testWidgets('needsCompositing updates correctly and is respected', (WidgetTester tester) async {
|
||||
testWidgets('needsCompositing is always false', (WidgetTester tester) async {
|
||||
// Pretend that we have a mouse connected.
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer(location: Offset.zero);
|
||||
|
@ -467,22 +467,9 @@ void main() {
|
|||
),
|
||||
),
|
||||
);
|
||||
expect(listener.needsCompositing, isTrue);
|
||||
// Compositing is required, therefore a dedicated TransformLayer for
|
||||
// `Transform.scale` is added.
|
||||
expect(tester.layers.whereType<TransformLayer>(), hasLength(2));
|
||||
|
||||
await tester.pumpWidget(
|
||||
Transform.scale(
|
||||
scale: 2.0,
|
||||
child: Listener(
|
||||
onPointerDown: (PointerDownEvent _) { },
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(listener.needsCompositing, isFalse);
|
||||
// TransformLayer for `Transform.scale` is removed again as transform is
|
||||
// executed directly on the canvas.
|
||||
// If compositing was required, a dedicated TransformLayer for
|
||||
// `Transform.scale` would be added.
|
||||
expect(tester.layers.whereType<TransformLayer>(), hasLength(1));
|
||||
});
|
||||
|
||||
|
|
|
@ -168,10 +168,6 @@ void main() {
|
|||
await gesture.addPointer(location: const Offset(400, 300));
|
||||
addTearDown(gesture.removePointer);
|
||||
expect(move, isNull);
|
||||
expect(enter, isNull);
|
||||
expect(exit, isNull);
|
||||
await tester.pump();
|
||||
expect(move, isNull);
|
||||
expect(enter, isNotNull);
|
||||
expect(enter.position, equals(const Offset(400.0, 300.0)));
|
||||
expect(exit, isNull);
|
||||
|
@ -585,13 +581,13 @@ void main() {
|
|||
}
|
||||
|
||||
await tester.pumpWidget(hoverableContainer(
|
||||
onEnter: (PointerEnterEvent details) => logs.add('enter1'),
|
||||
onHover: (PointerHoverEvent details) => logs.add('hover1'),
|
||||
onExit: (PointerExitEvent details) => logs.add('exit1'),
|
||||
onEnter: (PointerEnterEvent details) { logs.add('enter1'); },
|
||||
onHover: (PointerHoverEvent details) { logs.add('hover1'); },
|
||||
onExit: (PointerExitEvent details) { logs.add('exit1'); },
|
||||
));
|
||||
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer();
|
||||
await gesture.addPointer(location: const Offset(150.0, 150.0));
|
||||
addTearDown(gesture.removePointer);
|
||||
|
||||
// Start outside, move inside, then move outside
|
||||
|
@ -709,7 +705,7 @@ void main() {
|
|||
events.clear();
|
||||
});
|
||||
|
||||
testWidgets('needsCompositing updates correctly and is respected', (WidgetTester tester) async {
|
||||
testWidgets('needsCompositing is always false', (WidgetTester tester) async {
|
||||
// Pretend that we have a mouse connected.
|
||||
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await gesture.addPointer();
|
||||
|
@ -729,7 +725,7 @@ void main() {
|
|||
// transform.)
|
||||
expect(tester.layers.whereType<TransformLayer>(), hasLength(1));
|
||||
|
||||
// Test that needsCompositing updates correctly with callback change
|
||||
// Test that needsCompositing stays false with callback change
|
||||
await tester.pumpWidget(
|
||||
Transform.scale(
|
||||
scale: 2.0,
|
||||
|
@ -739,35 +735,10 @@ void main() {
|
|||
),
|
||||
),
|
||||
);
|
||||
expect(mouseRegion.needsCompositing, isTrue);
|
||||
// Compositing is required, therefore a dedicated TransformLayer for
|
||||
// `Transform.scale` is added.
|
||||
expect(tester.layers.whereType<TransformLayer>(), hasLength(2));
|
||||
|
||||
await tester.pumpWidget(
|
||||
Transform.scale(
|
||||
scale: 2.0,
|
||||
child: const MouseRegion(opaque: false),
|
||||
),
|
||||
);
|
||||
expect(mouseRegion.needsCompositing, isFalse);
|
||||
// TransformLayer for `Transform.scale` is removed again as transform is
|
||||
// executed directly on the canvas.
|
||||
// If compositing was required, a dedicated TransformLayer for
|
||||
// `Transform.scale` would be added.
|
||||
expect(tester.layers.whereType<TransformLayer>(), hasLength(1));
|
||||
|
||||
// Test that needsCompositing updates correctly with `opaque` change
|
||||
await tester.pumpWidget(
|
||||
Transform.scale(
|
||||
scale: 2.0,
|
||||
child: const MouseRegion(
|
||||
opaque: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(mouseRegion.needsCompositing, isTrue);
|
||||
// Compositing is required, therefore a dedicated TransformLayer for
|
||||
// `Transform.scale` is added.
|
||||
expect(tester.layers.whereType<TransformLayer>(), hasLength(2));
|
||||
});
|
||||
|
||||
testWidgets("Callbacks aren't called during build", (WidgetTester tester) async {
|
||||
|
|
Loading…
Reference in a new issue