Transform PointerEvents to the local coordinate system of the event receiver (#32192)
This commit is contained in:
parent
a9518da068
commit
ae23d4a490
@ -195,7 +195,7 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
|
||||
}
|
||||
for (HitTestEntry entry in hitTestResult.path) {
|
||||
try {
|
||||
entry.target.handleEvent(event, entry);
|
||||
entry.target.handleEvent(event.transformed(entry.transform), entry);
|
||||
} catch (exception, stack) {
|
||||
FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
|
||||
exception: exception,
|
||||
|
@ -20,14 +20,28 @@ class DragDownDetails {
|
||||
/// Creates details for a [GestureDragDownCallback].
|
||||
///
|
||||
/// The [globalPosition] argument must not be null.
|
||||
DragDownDetails({ this.globalPosition = Offset.zero })
|
||||
: assert(globalPosition != null);
|
||||
DragDownDetails({
|
||||
this.globalPosition = Offset.zero,
|
||||
Offset localPosition,
|
||||
}) : assert(globalPosition != null),
|
||||
localPosition = localPosition ?? globalPosition;
|
||||
|
||||
/// The global position at which the pointer contacted the screen.
|
||||
///
|
||||
/// Defaults to the origin if not specified in the constructor.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [localPosition], which is the [globalPosition] transformed to the
|
||||
/// coordinate space of the event receiver.
|
||||
final Offset globalPosition;
|
||||
|
||||
/// The local position in the coordinate system of the event receiver at
|
||||
/// which the pointer contacted the screen.
|
||||
///
|
||||
/// Defaults to [globalPosition] if not specified in the constructor.
|
||||
final Offset localPosition;
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType($globalPosition)';
|
||||
}
|
||||
@ -52,8 +66,12 @@ class DragStartDetails {
|
||||
/// Creates details for a [GestureDragStartCallback].
|
||||
///
|
||||
/// The [globalPosition] argument must not be null.
|
||||
DragStartDetails({ this.sourceTimeStamp, this.globalPosition = Offset.zero })
|
||||
: assert(globalPosition != null);
|
||||
DragStartDetails({
|
||||
this.sourceTimeStamp,
|
||||
this.globalPosition = Offset.zero,
|
||||
Offset localPosition,
|
||||
}) : assert(globalPosition != null),
|
||||
localPosition = localPosition ?? globalPosition;
|
||||
|
||||
/// Recorded timestamp of the source pointer event that triggered the drag
|
||||
/// event.
|
||||
@ -64,8 +82,19 @@ class DragStartDetails {
|
||||
/// The global position at which the pointer contacted the screen.
|
||||
///
|
||||
/// Defaults to the origin if not specified in the constructor.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [localPosition], which is the [globalPosition] transformed to the
|
||||
/// coordinate space of the event receiver.
|
||||
final Offset globalPosition;
|
||||
|
||||
/// The local position in the coordinate system of the event receiver at
|
||||
/// which the pointer contacted the screen.
|
||||
///
|
||||
/// Defaults to [globalPosition] if not specified in the constructor.
|
||||
final Offset localPosition;
|
||||
|
||||
// TODO(ianh): Expose the current position, so that you can have a no-jump
|
||||
// drag even when disambiguating (though of course it would lag the finger
|
||||
// instead).
|
||||
@ -104,10 +133,12 @@ class DragUpdateDetails {
|
||||
this.delta = Offset.zero,
|
||||
this.primaryDelta,
|
||||
@required this.globalPosition,
|
||||
Offset localPosition,
|
||||
}) : assert(delta != null),
|
||||
assert(primaryDelta == null
|
||||
|| (primaryDelta == delta.dx && delta.dy == 0.0)
|
||||
|| (primaryDelta == delta.dy && delta.dx == 0.0));
|
||||
|| (primaryDelta == delta.dy && delta.dx == 0.0)),
|
||||
localPosition = localPosition ?? globalPosition;
|
||||
|
||||
/// Recorded timestamp of the source pointer event that triggered the drag
|
||||
/// event.
|
||||
@ -115,7 +146,8 @@ class DragUpdateDetails {
|
||||
/// Could be null if triggered from proxied events such as accessibility.
|
||||
final Duration sourceTimeStamp;
|
||||
|
||||
/// The amount the pointer has moved since the previous update.
|
||||
/// The amount the pointer has moved in the coordinate space of the event
|
||||
/// receiver since the previous update.
|
||||
///
|
||||
/// If the [GestureDragUpdateCallback] is for a one-dimensional drag (e.g.,
|
||||
/// a horizontal or vertical drag), then this offset contains only the delta
|
||||
@ -124,7 +156,8 @@ class DragUpdateDetails {
|
||||
/// Defaults to zero if not specified in the constructor.
|
||||
final Offset delta;
|
||||
|
||||
/// The amount the pointer has moved along the primary axis since the previous
|
||||
/// The amount the pointer has moved along the primary axis in the coordinate
|
||||
/// space of the event receiver since the previous
|
||||
/// update.
|
||||
///
|
||||
/// If the [GestureDragUpdateCallback] is for a one-dimensional drag (e.g.,
|
||||
@ -137,8 +170,19 @@ class DragUpdateDetails {
|
||||
final double primaryDelta;
|
||||
|
||||
/// The pointer's global position when it triggered this update.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [localPosition], which is the [globalPosition] transformed to the
|
||||
/// coordinate space of the event receiver.
|
||||
final Offset globalPosition;
|
||||
|
||||
/// The local position in the coordinate system of the event receiver at
|
||||
/// which the pointer contacted the screen.
|
||||
///
|
||||
/// Defaults to [globalPosition] if not specified in the constructor.
|
||||
final Offset localPosition;
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType($delta)';
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ class EagerGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
// We call startTrackingPointer as this is where OneSequenceGestureRecognizer joins the arena.
|
||||
startTrackingPointer(event.pointer);
|
||||
startTrackingPointer(event.pointer, event.transform);
|
||||
resolve(GestureDisposition.accepted);
|
||||
stopTrackingPointer(event.pointer);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
import 'dart:ui' show Offset, PointerDeviceKind;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
export 'dart:ui' show Offset, PointerDeviceKind;
|
||||
|
||||
@ -202,7 +203,9 @@ abstract class PointerEvent extends Diagnosticable {
|
||||
this.kind = PointerDeviceKind.touch,
|
||||
this.device = 0,
|
||||
this.position = Offset.zero,
|
||||
Offset localPosition,
|
||||
this.delta = Offset.zero,
|
||||
Offset localDelta,
|
||||
this.buttons = 0,
|
||||
this.down = false,
|
||||
this.obscured = false,
|
||||
@ -220,7 +223,11 @@ abstract class PointerEvent extends Diagnosticable {
|
||||
this.tilt = 0.0,
|
||||
this.platformData = 0,
|
||||
this.synthesized = false,
|
||||
});
|
||||
this.transform,
|
||||
this.original,
|
||||
}) : localPosition = localPosition ?? position,
|
||||
localDelta = localDelta ?? delta;
|
||||
|
||||
|
||||
/// Time of event dispatch, relative to an arbitrary timeline.
|
||||
final Duration timeStamp;
|
||||
@ -237,14 +244,46 @@ abstract class PointerEvent extends Diagnosticable {
|
||||
|
||||
/// Coordinate of the position of the pointer, in logical pixels in the global
|
||||
/// coordinate space.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [localPosition], which is the [position] transformed into the local
|
||||
/// coordinate system of the event receiver.
|
||||
final Offset position;
|
||||
|
||||
/// The [position] transformed into the event receiver's local coordinate
|
||||
/// system according to [transform].
|
||||
///
|
||||
/// If this event has not been transformed, [position] is returned as-is.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [globalPosition], which is the position in the global coordinate
|
||||
/// system of the screen.
|
||||
final Offset localPosition;
|
||||
|
||||
/// Distance in logical pixels that the pointer moved since the last
|
||||
/// [PointerMoveEvent] or [PointerHoverEvent].
|
||||
///
|
||||
/// This value is always 0.0 for down, up, and cancel events.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [localDelta], which is the [delta] transformed into the local
|
||||
/// coordinate space of the event receiver.
|
||||
final Offset delta;
|
||||
|
||||
/// The [delta] transformed into the event receiver's local coordinate
|
||||
/// system according to [transform].
|
||||
///
|
||||
/// If this event has not been transformed, [delta] is returned as-is.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [delta], which is the distance the pointer moved in the global
|
||||
/// coordinate system of the screen.
|
||||
final Offset localDelta;
|
||||
|
||||
/// Bit field using the *Button constants such as [kPrimaryMouseButton],
|
||||
/// [kSecondaryStylusButton], etc.
|
||||
///
|
||||
@ -390,11 +429,56 @@ abstract class PointerEvent extends Diagnosticable {
|
||||
/// the difference between the 2 events in that case.
|
||||
final bool synthesized;
|
||||
|
||||
/// The transformation used to transform this event from the global coordinate
|
||||
/// space into the coordinate space of the event receiver.
|
||||
///
|
||||
/// This value affects what is returned by [localPosition] and [localDelta].
|
||||
/// If this value is null, it is treated as the identity transformation.
|
||||
///
|
||||
/// Unlike a paint transform, this transform usually does not contain any
|
||||
/// "perspective" components, meaning that the third row and the third column
|
||||
/// of the matrix should be equal to "0, 0, 1, 0". This ensures that
|
||||
/// [localPosition] describes the point in the local coordinate system of the
|
||||
/// event receiver at which the user is actually touching the screen.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [transformed], which transforms this event into a different coordinate
|
||||
/// space.
|
||||
final Matrix4 transform;
|
||||
|
||||
/// The original un-transformed [PointerEvent] before any [transform]s were
|
||||
/// applied.
|
||||
///
|
||||
/// If [transform] is null or the identity transformation this may be null.
|
||||
///
|
||||
/// When multiple event receivers in different coordinate spaces receive an
|
||||
/// event, they all receive the event transformed to their local coordinate
|
||||
/// space. The [original] property can be used to determine if all those
|
||||
/// transformed events actually originated from the same pointer interaction.
|
||||
final PointerEvent original;
|
||||
|
||||
/// Transforms the event from the global coordinate space into the coordinate
|
||||
/// space of an event receiver.
|
||||
///
|
||||
/// The coordinate space of the event receiver is described by `transform`. A
|
||||
/// null value for `transform` is treated as the identity transformation.
|
||||
///
|
||||
/// The method may return the same object instance if for example the
|
||||
/// transformation has no effect on the event.
|
||||
///
|
||||
/// Transforms are not commutative. If this method is called on a
|
||||
/// [PointerEvent] that has a non-null [transform] value, that value will be
|
||||
/// overridden by the provided `transform`.
|
||||
PointerEvent transformed(Matrix4 transform);
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<Offset>('position', position));
|
||||
properties.add(DiagnosticsProperty<Offset>('localPosition', localPosition, defaultValue: position, level: DiagnosticLevel.debug));
|
||||
properties.add(DiagnosticsProperty<Offset>('delta', delta, defaultValue: Offset.zero, level: DiagnosticLevel.debug));
|
||||
properties.add(DiagnosticsProperty<Offset>('localDelta', localDelta, defaultValue: delta, level: DiagnosticLevel.debug));
|
||||
properties.add(DiagnosticsProperty<Duration>('timeStamp', timeStamp, defaultValue: Duration.zero, level: DiagnosticLevel.debug));
|
||||
properties.add(IntProperty('pointer', pointer, level: DiagnosticLevel.debug));
|
||||
properties.add(EnumProperty<PointerDeviceKind>('kind', kind, level: DiagnosticLevel.debug));
|
||||
@ -423,6 +507,61 @@ abstract class PointerEvent extends Diagnosticable {
|
||||
String toStringFull() {
|
||||
return toString(minLevel: DiagnosticLevel.fine);
|
||||
}
|
||||
|
||||
/// Returns the transformation of `position` into the coordinate system
|
||||
/// described by `transform`.
|
||||
///
|
||||
/// The z-value of `position` is assumed to be 0.0. If `transform` is null,
|
||||
/// `position` is returned as-is.
|
||||
static Offset transformPosition(Matrix4 transform, Offset position) {
|
||||
if (transform == null) {
|
||||
return position;
|
||||
}
|
||||
final Vector3 position3 = Vector3(position.dx, position.dy, 0.0);
|
||||
final Vector3 transformed3 = transform.perspectiveTransform(position3);
|
||||
return Offset(transformed3.x, transformed3.y);
|
||||
}
|
||||
|
||||
/// Transforms `untransformedDelta` into the coordinate system described by
|
||||
/// `transform`.
|
||||
///
|
||||
/// It uses the provided `untransformedEndPosition` and
|
||||
/// `transformedEndPosition` of the provided delta to increase accuracy.
|
||||
///
|
||||
/// If `transform` is null, `untransformedDelta` is returned.
|
||||
static Offset transformDeltaViaPositions({
|
||||
@required Offset untransformedEndPosition,
|
||||
Offset transformedEndPosition,
|
||||
@required Offset untransformedDelta,
|
||||
@required Matrix4 transform,
|
||||
}) {
|
||||
if (transform == null) {
|
||||
return untransformedDelta;
|
||||
}
|
||||
// We could transform the delta directly with the transformation matrix.
|
||||
// While that is mathematically equivalent, in practice we are seeing a
|
||||
// greater precision error with that approach. Instead, we are transforming
|
||||
// start and end point of the delta separately and calculate the delta in
|
||||
// the new space for greater accuracy.
|
||||
transformedEndPosition ??= transformPosition(transform, untransformedEndPosition);
|
||||
final Offset transformedStartPosition = transformPosition(transform, untransformedEndPosition - untransformedDelta);
|
||||
return transformedEndPosition - transformedStartPosition;
|
||||
}
|
||||
|
||||
/// Removes the "perspective" component from `transform`.
|
||||
///
|
||||
/// When applying the resulting transform matrix to a point with a
|
||||
/// z-coordinate of zero (which is generally assumed for all points
|
||||
/// represented by an [Offset]), the other coordinates will get transformed as
|
||||
/// before, but the new z-coordinate is going to be zero again. This is
|
||||
/// achieved by setting the third column and third row of the matrix to
|
||||
/// "0, 0, 1, 0".
|
||||
static Matrix4 removePerspectiveTransform(Matrix4 transform) {
|
||||
final Vector4 vector = Vector4(0, 0, 1, 0);
|
||||
return transform.clone()
|
||||
..setColumn(2, vector)
|
||||
..setRow(2, vector);
|
||||
}
|
||||
}
|
||||
|
||||
/// The device has started tracking the pointer.
|
||||
@ -438,6 +577,7 @@ class PointerAddedEvent extends PointerEvent {
|
||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||
int device = 0,
|
||||
Offset position = Offset.zero,
|
||||
Offset localPosition,
|
||||
bool obscured = false,
|
||||
double pressureMin = 1.0,
|
||||
double pressureMax = 1.0,
|
||||
@ -447,11 +587,14 @@ class PointerAddedEvent extends PointerEvent {
|
||||
double radiusMax = 0.0,
|
||||
double orientation = 0.0,
|
||||
double tilt = 0.0,
|
||||
Matrix4 transform,
|
||||
PointerAddedEvent original,
|
||||
}) : super(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: localPosition,
|
||||
obscured: obscured,
|
||||
pressure: 0.0,
|
||||
pressureMin: pressureMin,
|
||||
@ -462,7 +605,34 @@ class PointerAddedEvent extends PointerEvent {
|
||||
radiusMax: radiusMax,
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
transform: transform,
|
||||
original: original,
|
||||
);
|
||||
|
||||
@override
|
||||
PointerAddedEvent transformed(Matrix4 transform) {
|
||||
if (transform == null || transform == this.transform) {
|
||||
return this;
|
||||
}
|
||||
return PointerAddedEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: PointerEvent.transformPosition(transform, position),
|
||||
obscured: obscured,
|
||||
pressureMin: pressureMin,
|
||||
pressureMax: pressureMax,
|
||||
distance: distance,
|
||||
distanceMax: distanceMax,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
transform: transform,
|
||||
original: original ?? this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The device is no longer tracking the pointer.
|
||||
@ -478,17 +648,21 @@ class PointerRemovedEvent extends PointerEvent {
|
||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||
int device = 0,
|
||||
Offset position = Offset.zero,
|
||||
Offset localPosition,
|
||||
bool obscured = false,
|
||||
double pressureMin = 1.0,
|
||||
double pressureMax = 1.0,
|
||||
double distanceMax = 0.0,
|
||||
double radiusMin = 0.0,
|
||||
double radiusMax = 0.0,
|
||||
Matrix4 transform,
|
||||
PointerRemovedEvent original,
|
||||
}) : super(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: localPosition,
|
||||
obscured: obscured,
|
||||
pressure: 0.0,
|
||||
pressureMin: pressureMin,
|
||||
@ -496,7 +670,31 @@ class PointerRemovedEvent extends PointerEvent {
|
||||
distanceMax: distanceMax,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
transform: transform,
|
||||
original: original,
|
||||
);
|
||||
|
||||
@override
|
||||
PointerRemovedEvent transformed(Matrix4 transform) {
|
||||
if (transform == null || transform == this.transform) {
|
||||
return this;
|
||||
}
|
||||
return PointerRemovedEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: PointerEvent.transformPosition(transform, position),
|
||||
obscured: obscured,
|
||||
pressureMin: pressureMin,
|
||||
pressureMax: pressureMax,
|
||||
distanceMax: distanceMax,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
transform: transform,
|
||||
original: original ?? this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The pointer has moved with respect to the device while the pointer is not
|
||||
@ -518,7 +716,9 @@ class PointerHoverEvent extends PointerEvent {
|
||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||
int device = 0,
|
||||
Offset position = Offset.zero,
|
||||
Offset localPosition,
|
||||
Offset delta = Offset.zero,
|
||||
Offset localDelta,
|
||||
int buttons = 0,
|
||||
bool obscured = false,
|
||||
double pressureMin = 1.0,
|
||||
@ -533,12 +733,16 @@ class PointerHoverEvent extends PointerEvent {
|
||||
double orientation = 0.0,
|
||||
double tilt = 0.0,
|
||||
bool synthesized = false,
|
||||
Matrix4 transform,
|
||||
PointerHoverEvent original,
|
||||
}) : super(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: localPosition,
|
||||
delta: delta,
|
||||
localDelta: localDelta,
|
||||
buttons: buttons,
|
||||
down: false,
|
||||
obscured: obscured,
|
||||
@ -555,7 +759,47 @@ class PointerHoverEvent extends PointerEvent {
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
synthesized: synthesized,
|
||||
transform: transform,
|
||||
original: original,
|
||||
);
|
||||
|
||||
@override
|
||||
PointerHoverEvent transformed(Matrix4 transform) {
|
||||
if (transform == null || transform == this.transform) {
|
||||
return this;
|
||||
}
|
||||
final Offset transformedPosition = PointerEvent.transformPosition(transform, position);
|
||||
return PointerHoverEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: transformedPosition,
|
||||
delta: delta,
|
||||
localDelta: PointerEvent.transformDeltaViaPositions(
|
||||
transform: transform,
|
||||
untransformedDelta: delta,
|
||||
untransformedEndPosition: position,
|
||||
transformedEndPosition: transformedPosition,
|
||||
),
|
||||
buttons: buttons,
|
||||
obscured: obscured,
|
||||
pressureMin: pressureMin,
|
||||
pressureMax: pressureMax,
|
||||
distance: distance,
|
||||
distanceMax: distanceMax,
|
||||
size: size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
synthesized: synthesized,
|
||||
transform: transform,
|
||||
original: original ?? this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The pointer has moved with respect to the device while the pointer is not
|
||||
@ -577,7 +821,9 @@ class PointerEnterEvent extends PointerEvent {
|
||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||
int device = 0,
|
||||
Offset position = Offset.zero,
|
||||
Offset localPosition,
|
||||
Offset delta = Offset.zero,
|
||||
Offset localDelta,
|
||||
int buttons = 0,
|
||||
bool obscured = false,
|
||||
double pressureMin = 1.0,
|
||||
@ -592,12 +838,16 @@ class PointerEnterEvent extends PointerEvent {
|
||||
double orientation = 0.0,
|
||||
double tilt = 0.0,
|
||||
bool synthesized = false,
|
||||
Matrix4 transform,
|
||||
PointerEnterEvent original,
|
||||
}) : super(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: localPosition,
|
||||
delta: delta,
|
||||
localDelta: localDelta,
|
||||
buttons: buttons,
|
||||
down: false,
|
||||
obscured: obscured,
|
||||
@ -614,6 +864,8 @@ class PointerEnterEvent extends PointerEvent {
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
synthesized: synthesized,
|
||||
transform: transform,
|
||||
original: original,
|
||||
);
|
||||
|
||||
/// Creates an enter event from a [PointerHoverEvent].
|
||||
@ -630,7 +882,9 @@ class PointerEnterEvent extends PointerEvent {
|
||||
kind: event?.kind,
|
||||
device: event?.device,
|
||||
position: event?.position,
|
||||
localPosition: event?.localPosition,
|
||||
delta: event?.delta,
|
||||
localDelta: event?.localDelta,
|
||||
buttons: event?.buttons,
|
||||
obscured: event?.obscured,
|
||||
pressureMin: event?.pressureMin,
|
||||
@ -645,7 +899,47 @@ class PointerEnterEvent extends PointerEvent {
|
||||
orientation: event?.orientation,
|
||||
tilt: event?.tilt,
|
||||
synthesized: event?.synthesized,
|
||||
transform: event?.transform,
|
||||
original: event?.original,
|
||||
);
|
||||
|
||||
@override
|
||||
PointerEnterEvent transformed(Matrix4 transform) {
|
||||
if (transform == null || transform == this.transform) {
|
||||
return this;
|
||||
}
|
||||
final Offset transformedPosition = PointerEvent.transformPosition(transform, position);
|
||||
return PointerEnterEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: transformedPosition,
|
||||
delta: delta,
|
||||
localDelta: PointerEvent.transformDeltaViaPositions(
|
||||
transform: transform,
|
||||
untransformedDelta: delta,
|
||||
untransformedEndPosition: position,
|
||||
transformedEndPosition: transformedPosition,
|
||||
),
|
||||
buttons: buttons,
|
||||
obscured: obscured,
|
||||
pressureMin: pressureMin,
|
||||
pressureMax: pressureMax,
|
||||
distance: distance,
|
||||
distanceMax: distanceMax,
|
||||
size: size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
synthesized: synthesized,
|
||||
transform: transform,
|
||||
original: original ?? this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The pointer has moved with respect to the device while the pointer is not
|
||||
@ -667,7 +961,9 @@ class PointerExitEvent extends PointerEvent {
|
||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||
int device = 0,
|
||||
Offset position = Offset.zero,
|
||||
Offset localPosition,
|
||||
Offset delta = Offset.zero,
|
||||
Offset localDelta,
|
||||
int buttons = 0,
|
||||
bool obscured = false,
|
||||
double pressureMin = 1.0,
|
||||
@ -682,12 +978,16 @@ class PointerExitEvent extends PointerEvent {
|
||||
double orientation = 0.0,
|
||||
double tilt = 0.0,
|
||||
bool synthesized = false,
|
||||
Matrix4 transform,
|
||||
PointerExitEvent original,
|
||||
}) : super(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: localPosition,
|
||||
delta: delta,
|
||||
localDelta: localDelta,
|
||||
buttons: buttons,
|
||||
down: false,
|
||||
obscured: obscured,
|
||||
@ -704,6 +1004,8 @@ class PointerExitEvent extends PointerEvent {
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
synthesized: synthesized,
|
||||
transform: transform,
|
||||
original: original,
|
||||
);
|
||||
|
||||
/// Creates an exit event from a [PointerHoverEvent].
|
||||
@ -720,7 +1022,9 @@ class PointerExitEvent extends PointerEvent {
|
||||
kind: event?.kind,
|
||||
device: event?.device,
|
||||
position: event?.position,
|
||||
localPosition: event?.localPosition,
|
||||
delta: event?.delta,
|
||||
localDelta: event?.localDelta,
|
||||
buttons: event?.buttons,
|
||||
obscured: event?.obscured,
|
||||
pressureMin: event?.pressureMin,
|
||||
@ -735,7 +1039,47 @@ class PointerExitEvent extends PointerEvent {
|
||||
orientation: event?.orientation,
|
||||
tilt: event?.tilt,
|
||||
synthesized: event?.synthesized,
|
||||
transform: event?.transform,
|
||||
original: event?.original,
|
||||
);
|
||||
|
||||
@override
|
||||
PointerExitEvent transformed(Matrix4 transform) {
|
||||
if (transform == null || transform == this.transform) {
|
||||
return this;
|
||||
}
|
||||
final Offset transformedPosition = PointerEvent.transformPosition(transform, position);
|
||||
return PointerExitEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: transformedPosition,
|
||||
delta: delta,
|
||||
localDelta: PointerEvent.transformDeltaViaPositions(
|
||||
transform: transform,
|
||||
untransformedDelta: delta,
|
||||
untransformedEndPosition: position,
|
||||
transformedEndPosition: transformedPosition,
|
||||
),
|
||||
buttons: buttons,
|
||||
obscured: obscured,
|
||||
pressureMin: pressureMin,
|
||||
pressureMax: pressureMax,
|
||||
distance: distance,
|
||||
distanceMax: distanceMax,
|
||||
size: size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
synthesized: synthesized,
|
||||
transform: transform,
|
||||
original: original ?? this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The pointer has made contact with the device.
|
||||
@ -749,6 +1093,7 @@ class PointerDownEvent extends PointerEvent {
|
||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||
int device = 0,
|
||||
Offset position = Offset.zero,
|
||||
Offset localPosition,
|
||||
int buttons = kPrimaryButton,
|
||||
bool obscured = false,
|
||||
double pressure = 1.0,
|
||||
@ -762,12 +1107,15 @@ class PointerDownEvent extends PointerEvent {
|
||||
double radiusMax = 0.0,
|
||||
double orientation = 0.0,
|
||||
double tilt = 0.0,
|
||||
Matrix4 transform,
|
||||
PointerDownEvent original,
|
||||
}) : super(
|
||||
timeStamp: timeStamp,
|
||||
pointer: pointer,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: localPosition,
|
||||
buttons: buttons,
|
||||
down: true,
|
||||
obscured: obscured,
|
||||
@ -783,7 +1131,39 @@ class PointerDownEvent extends PointerEvent {
|
||||
radiusMax: radiusMax,
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
transform: transform,
|
||||
original: original,
|
||||
);
|
||||
|
||||
@override
|
||||
PointerDownEvent transformed(Matrix4 transform) {
|
||||
if (transform == null || transform == this.transform) {
|
||||
return this;
|
||||
}
|
||||
return PointerDownEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: pointer,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: PointerEvent.transformPosition(transform, position),
|
||||
buttons: buttons,
|
||||
obscured: obscured,
|
||||
pressure: pressure,
|
||||
pressureMin: pressureMin,
|
||||
pressureMax: pressureMax,
|
||||
distanceMax: distanceMax,
|
||||
size: size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
transform: transform,
|
||||
original: original ?? this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The pointer has moved with respect to the device while the pointer is in
|
||||
@ -803,7 +1183,9 @@ class PointerMoveEvent extends PointerEvent {
|
||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||
int device = 0,
|
||||
Offset position = Offset.zero,
|
||||
Offset localPosition,
|
||||
Offset delta = Offset.zero,
|
||||
Offset localDelta,
|
||||
int buttons = kPrimaryButton,
|
||||
bool obscured = false,
|
||||
double pressure = 1.0,
|
||||
@ -819,13 +1201,17 @@ class PointerMoveEvent extends PointerEvent {
|
||||
double tilt = 0.0,
|
||||
int platformData = 0,
|
||||
bool synthesized = false,
|
||||
Matrix4 transform,
|
||||
PointerMoveEvent original,
|
||||
}) : super(
|
||||
timeStamp: timeStamp,
|
||||
pointer: pointer,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: localPosition,
|
||||
delta: delta,
|
||||
localDelta: localDelta,
|
||||
buttons: buttons,
|
||||
down: true,
|
||||
obscured: obscured,
|
||||
@ -843,7 +1229,50 @@ class PointerMoveEvent extends PointerEvent {
|
||||
tilt: tilt,
|
||||
platformData: platformData,
|
||||
synthesized: synthesized,
|
||||
transform: transform,
|
||||
original: original,
|
||||
);
|
||||
|
||||
@override
|
||||
PointerMoveEvent transformed(Matrix4 transform) {
|
||||
if (transform == null || transform == this.transform) {
|
||||
return this;
|
||||
}
|
||||
final Offset transformedPosition = PointerEvent.transformPosition(transform, position);
|
||||
|
||||
return PointerMoveEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: pointer,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: transformedPosition,
|
||||
delta: delta,
|
||||
localDelta: PointerEvent.transformDeltaViaPositions(
|
||||
transform: transform,
|
||||
untransformedDelta: delta,
|
||||
untransformedEndPosition: position,
|
||||
transformedEndPosition: transformedPosition,
|
||||
),
|
||||
buttons: buttons,
|
||||
obscured: obscured,
|
||||
pressure: pressure,
|
||||
pressureMin: pressureMin,
|
||||
pressureMax: pressureMax,
|
||||
distanceMax: distanceMax,
|
||||
size: size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
platformData: platformData,
|
||||
synthesized: synthesized,
|
||||
transform: transform,
|
||||
original: original ?? this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The pointer has stopped making contact with the device.
|
||||
@ -857,6 +1286,7 @@ class PointerUpEvent extends PointerEvent {
|
||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||
int device = 0,
|
||||
Offset position = Offset.zero,
|
||||
Offset localPosition,
|
||||
int buttons = 0,
|
||||
bool obscured = false,
|
||||
// Allow pressure customization here because PointerUpEvent can contain
|
||||
@ -873,12 +1303,15 @@ class PointerUpEvent extends PointerEvent {
|
||||
double radiusMax = 0.0,
|
||||
double orientation = 0.0,
|
||||
double tilt = 0.0,
|
||||
Matrix4 transform,
|
||||
PointerUpEvent original,
|
||||
}) : super(
|
||||
timeStamp: timeStamp,
|
||||
pointer: pointer,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: localPosition,
|
||||
buttons: buttons,
|
||||
down: false,
|
||||
obscured: obscured,
|
||||
@ -894,7 +1327,40 @@ class PointerUpEvent extends PointerEvent {
|
||||
radiusMax: radiusMax,
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
transform: transform,
|
||||
original: original,
|
||||
);
|
||||
|
||||
@override
|
||||
PointerUpEvent transformed(Matrix4 transform) {
|
||||
if (transform == null || transform == this.transform) {
|
||||
return this;
|
||||
}
|
||||
return PointerUpEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: pointer,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: PointerEvent.transformPosition(transform, position),
|
||||
buttons: buttons,
|
||||
obscured: obscured,
|
||||
pressure: pressure,
|
||||
pressureMin: pressureMin,
|
||||
pressureMax: pressureMax,
|
||||
distance: distance,
|
||||
distanceMax: distanceMax,
|
||||
size: size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
transform: transform,
|
||||
original: original ?? this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An event that corresponds to a discrete pointer signal.
|
||||
@ -911,12 +1377,18 @@ abstract class PointerSignalEvent extends PointerEvent {
|
||||
PointerDeviceKind kind = PointerDeviceKind.mouse,
|
||||
int device = 0,
|
||||
Offset position = Offset.zero,
|
||||
Offset localPosition,
|
||||
Matrix4 transform,
|
||||
PointerSignalEvent original,
|
||||
}) : super(
|
||||
timeStamp: timeStamp,
|
||||
pointer: pointer,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: localPosition,
|
||||
transform: transform,
|
||||
original: original,
|
||||
);
|
||||
}
|
||||
|
||||
@ -933,7 +1405,10 @@ class PointerScrollEvent extends PointerSignalEvent {
|
||||
PointerDeviceKind kind = PointerDeviceKind.mouse,
|
||||
int device = 0,
|
||||
Offset position = Offset.zero,
|
||||
Offset localPosition,
|
||||
this.scrollDelta = Offset.zero,
|
||||
Matrix4 transform,
|
||||
PointerScrollEvent original,
|
||||
}) : assert(timeStamp != null),
|
||||
assert(kind != null),
|
||||
assert(device != null),
|
||||
@ -944,11 +1419,31 @@ class PointerScrollEvent extends PointerSignalEvent {
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: localPosition,
|
||||
transform: transform,
|
||||
original: original,
|
||||
);
|
||||
|
||||
/// The amount to scroll, in logical pixels.
|
||||
final Offset scrollDelta;
|
||||
|
||||
@override
|
||||
PointerScrollEvent transformed(Matrix4 transform) {
|
||||
if (transform == null || transform == this.transform) {
|
||||
return this;
|
||||
}
|
||||
return PointerScrollEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: PointerEvent.transformPosition(transform, position),
|
||||
scrollDelta: scrollDelta,
|
||||
transform: transform,
|
||||
original: original ?? this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
@ -967,6 +1462,7 @@ class PointerCancelEvent extends PointerEvent {
|
||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||
int device = 0,
|
||||
Offset position = Offset.zero,
|
||||
Offset localPosition,
|
||||
int buttons = 0,
|
||||
bool obscured = false,
|
||||
double pressureMin = 1.0,
|
||||
@ -980,12 +1476,15 @@ class PointerCancelEvent extends PointerEvent {
|
||||
double radiusMax = 0.0,
|
||||
double orientation = 0.0,
|
||||
double tilt = 0.0,
|
||||
Matrix4 transform,
|
||||
PointerCancelEvent original,
|
||||
}) : super(
|
||||
timeStamp: timeStamp,
|
||||
pointer: pointer,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: localPosition,
|
||||
buttons: buttons,
|
||||
down: false,
|
||||
obscured: obscured,
|
||||
@ -1001,5 +1500,37 @@ class PointerCancelEvent extends PointerEvent {
|
||||
radiusMax: radiusMax,
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
transform: transform,
|
||||
original: original,
|
||||
);
|
||||
|
||||
@override
|
||||
PointerCancelEvent transformed(Matrix4 transform) {
|
||||
if (transform == null || transform == this.transform) {
|
||||
return this;
|
||||
}
|
||||
return PointerCancelEvent(
|
||||
timeStamp: timeStamp,
|
||||
pointer: pointer,
|
||||
kind: kind,
|
||||
device: device,
|
||||
position: position,
|
||||
localPosition: PointerEvent.transformPosition(transform, position),
|
||||
buttons: buttons,
|
||||
obscured: obscured,
|
||||
pressureMin: pressureMin,
|
||||
pressureMax: pressureMax,
|
||||
distance: distance,
|
||||
distanceMax: distanceMax,
|
||||
size: size,
|
||||
radiusMajor: radiusMajor,
|
||||
radiusMinor: radiusMinor,
|
||||
radiusMin: radiusMin,
|
||||
radiusMax: radiusMax,
|
||||
orientation: orientation,
|
||||
tilt: tilt,
|
||||
transform: transform,
|
||||
original: original ?? this,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -51,13 +51,18 @@ class ForcePressDetails {
|
||||
/// The [globalPosition] argument must not be null.
|
||||
ForcePressDetails({
|
||||
@required this.globalPosition,
|
||||
Offset localPosition,
|
||||
@required this.pressure,
|
||||
}) : assert(globalPosition != null),
|
||||
assert(pressure != null);
|
||||
assert(pressure != null),
|
||||
localPosition = localPosition ?? globalPosition;
|
||||
|
||||
/// The global position at which the function was called.
|
||||
final Offset globalPosition;
|
||||
|
||||
/// The local position at which the function was called.
|
||||
final Offset localPosition;
|
||||
|
||||
/// The pressure of the pointer on the screen.
|
||||
final double pressure;
|
||||
}
|
||||
@ -203,7 +208,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
/// ```
|
||||
final GestureForceInterpolation interpolation;
|
||||
|
||||
Offset _lastPosition;
|
||||
OffsetPair _lastPosition;
|
||||
double _lastPressure;
|
||||
_ForceState _state = _ForceState.ready;
|
||||
|
||||
@ -215,10 +220,10 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
if (!(event is PointerUpEvent) && event.pressureMax <= 1.0) {
|
||||
resolve(GestureDisposition.rejected);
|
||||
} else {
|
||||
startTrackingPointer(event.pointer);
|
||||
startTrackingPointer(event.pointer, event.transform);
|
||||
if (_state == _ForceState.ready) {
|
||||
_state = _ForceState.possible;
|
||||
_lastPosition = event.position;
|
||||
_lastPosition = OffsetPair.fromEventPosition(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -242,7 +247,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
pressure.isNaN // and interpolation may return NaN for values it doesn't want to support...
|
||||
);
|
||||
|
||||
_lastPosition = event.position;
|
||||
_lastPosition = OffsetPair.fromEventPosition(event);
|
||||
_lastPressure = pressure;
|
||||
|
||||
if (_state == _ForceState.possible) {
|
||||
@ -260,7 +265,8 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
if (onStart != null) {
|
||||
invokeCallback<void>('onStart', () => onStart(ForcePressDetails(
|
||||
pressure: pressure,
|
||||
globalPosition: _lastPosition,
|
||||
globalPosition: _lastPosition.global,
|
||||
localPosition: _lastPosition.local,
|
||||
)));
|
||||
}
|
||||
}
|
||||
@ -271,6 +277,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
invokeCallback<void>('onPeak', () => onPeak(ForcePressDetails(
|
||||
pressure: pressure,
|
||||
globalPosition: event.position,
|
||||
localPosition: event.localPosition,
|
||||
)));
|
||||
}
|
||||
}
|
||||
@ -280,6 +287,7 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
invokeCallback<void>('onUpdate', () => onUpdate(ForcePressDetails(
|
||||
pressure: pressure,
|
||||
globalPosition: event.position,
|
||||
localPosition: event.localPosition,
|
||||
)));
|
||||
}
|
||||
}
|
||||
@ -295,7 +303,8 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
if (onStart != null && _state == _ForceState.started) {
|
||||
invokeCallback<void>('onStart', () => onStart(ForcePressDetails(
|
||||
pressure: _lastPressure,
|
||||
globalPosition: _lastPosition,
|
||||
globalPosition: _lastPosition.global,
|
||||
localPosition: _lastPosition.local,
|
||||
)));
|
||||
}
|
||||
}
|
||||
@ -311,7 +320,8 @@ class ForcePressGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
if (onEnd != null) {
|
||||
invokeCallback<void>('onEnd', () => onEnd(ForcePressDetails(
|
||||
pressure: 0.0,
|
||||
globalPosition: _lastPosition,
|
||||
globalPosition: _lastPosition.global,
|
||||
localPosition: _lastPosition.local,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'events.dart';
|
||||
|
||||
/// An object that can hit-test pointers.
|
||||
@ -43,19 +48,32 @@ abstract class HitTestTarget {
|
||||
/// to the event propagation phase.
|
||||
class HitTestEntry {
|
||||
/// Creates a hit test entry.
|
||||
const HitTestEntry(this.target);
|
||||
HitTestEntry(this.target);
|
||||
|
||||
/// The [HitTestTarget] encountered during the hit test.
|
||||
final HitTestTarget target;
|
||||
|
||||
@override
|
||||
String toString() => '$target';
|
||||
|
||||
/// Returns a matrix describing how [PointerEvent]s delivered to this
|
||||
/// [HitTestEntry] should be transformed from the global coordinate space of
|
||||
/// the screen to the local coordinate space of [target].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [HitTestResult.addWithPaintTransform], which is used during hit testing
|
||||
/// to build up the transform returned by this method.
|
||||
Matrix4 get transform => _transform;
|
||||
Matrix4 _transform;
|
||||
}
|
||||
|
||||
/// The result of performing a hit test.
|
||||
class HitTestResult {
|
||||
/// Creates an empty hit test result.
|
||||
HitTestResult() : _path = <HitTestEntry>[];
|
||||
HitTestResult()
|
||||
: _path = <HitTestEntry>[],
|
||||
_transforms = Queue<Matrix4>();
|
||||
|
||||
/// Wraps `result` (usually a subtype of [HitTestResult]) to create a
|
||||
/// generic [HitTestResult].
|
||||
@ -63,7 +81,9 @@ class HitTestResult {
|
||||
/// The [HitTestEntry]s added to the returned [HitTestResult] are also
|
||||
/// added to the wrapped `result` (both share the same underlying data
|
||||
/// structure to store [HitTestEntry]s).
|
||||
HitTestResult.wrap(HitTestResult result) : _path = result._path;
|
||||
HitTestResult.wrap(HitTestResult result)
|
||||
: _path = result._path,
|
||||
_transforms = result._transforms;
|
||||
|
||||
/// An unmodifiable list of [HitTestEntry] objects recorded during the hit test.
|
||||
///
|
||||
@ -73,15 +93,87 @@ class HitTestResult {
|
||||
Iterable<HitTestEntry> get path => _path;
|
||||
final List<HitTestEntry> _path;
|
||||
|
||||
final Queue<Matrix4> _transforms;
|
||||
|
||||
/// Add a [HitTestEntry] to the path.
|
||||
///
|
||||
/// The new entry is added at the end of the path, which means entries should
|
||||
/// be added in order from most specific to least specific, typically during an
|
||||
/// upward walk of the tree being hit tested.
|
||||
void add(HitTestEntry entry) {
|
||||
assert(entry._transform == null);
|
||||
entry._transform = _transforms.isEmpty ? null : _transforms.last;
|
||||
_path.add(entry);
|
||||
}
|
||||
|
||||
/// Pushes a new transform matrix 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.addWithPaintTransform] for such an example).
|
||||
///
|
||||
/// The provided `transform` matrix should describe how to transform
|
||||
/// [PointerEvent]s from the coordinate space of the method caller to the
|
||||
/// coordinate space of its children. In most cases `transform` is derived
|
||||
/// from running the inverted result of [RenderObject.applyPaintTransform]
|
||||
/// through [PointerEvent.removePerspectiveTransform] to remove
|
||||
/// the perspective component.
|
||||
///
|
||||
/// [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:
|
||||
/// * [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 [RenderSlivers]s.
|
||||
@protected
|
||||
void pushTransform(Matrix4 transform) {
|
||||
assert(transform != null);
|
||||
assert(
|
||||
_debugVectorMoreOrLessEquals(transform.getRow(2), Vector4(0, 0, 1, 0)) &&
|
||||
_debugVectorMoreOrLessEquals(transform.getColumn(2), Vector4(0, 0, 1, 0)),
|
||||
'The third row and third column of a transform matrix for pointer '
|
||||
'events must be Vector4(0, 0, 1, 0) to ensure that a transformed '
|
||||
'point is directly under the pointer device. Did you forget to run the paint '
|
||||
'matrix through PointerEvent.removePerspectiveTransform?'
|
||||
'The provided matrix is:\n$transform'
|
||||
);
|
||||
_transforms.add(_transforms.isEmpty ? transform : transform * _transforms.last);
|
||||
}
|
||||
|
||||
/// Removes the last transform added via [pushTransform].
|
||||
///
|
||||
/// 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].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [pushTransform], which describes the use case of this function pair in
|
||||
/// more details.
|
||||
@protected
|
||||
void popTransform() {
|
||||
assert(_transforms.isNotEmpty);
|
||||
_transforms.removeLast();
|
||||
}
|
||||
|
||||
bool _debugVectorMoreOrLessEquals(Vector4 a, Vector4 b, { double epsilon = precisionErrorTolerance }) {
|
||||
bool result = true;
|
||||
assert(() {
|
||||
final Vector4 difference = a - b;
|
||||
result = difference.storage.every((double component) => component.abs() < epsilon);
|
||||
return true;
|
||||
}());
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'HitTestResult(${_path.isEmpty ? "<empty path>" : _path.join(", ")})';
|
||||
}
|
||||
|
@ -51,11 +51,17 @@ class LongPressStartDetails {
|
||||
/// Creates the details for a [GestureLongPressStartCallback].
|
||||
///
|
||||
/// The [globalPosition] argument must not be null.
|
||||
const LongPressStartDetails({ this.globalPosition = Offset.zero })
|
||||
: assert(globalPosition != null);
|
||||
const LongPressStartDetails({
|
||||
this.globalPosition = Offset.zero,
|
||||
Offset localPosition,
|
||||
}) : assert(globalPosition != null),
|
||||
localPosition = localPosition ?? globalPosition;
|
||||
|
||||
/// The global position at which the pointer contacted the screen.
|
||||
final Offset globalPosition;
|
||||
|
||||
/// The local position at which the pointer contacted the screen.
|
||||
final Offset localPosition;
|
||||
}
|
||||
|
||||
/// Details for callbacks that use [GestureLongPressMoveUpdateCallback].
|
||||
@ -71,17 +77,29 @@ class LongPressMoveUpdateDetails {
|
||||
/// The [globalPosition] and [offsetFromOrigin] arguments must not be null.
|
||||
const LongPressMoveUpdateDetails({
|
||||
this.globalPosition = Offset.zero,
|
||||
Offset localPosition,
|
||||
this.offsetFromOrigin = Offset.zero,
|
||||
Offset localOffsetFromOrigin,
|
||||
}) : assert(globalPosition != null),
|
||||
assert(offsetFromOrigin != null);
|
||||
assert(offsetFromOrigin != null),
|
||||
localPosition = localPosition ?? globalPosition,
|
||||
localOffsetFromOrigin = localOffsetFromOrigin ?? offsetFromOrigin;
|
||||
|
||||
/// The global position of the pointer when it triggered this update.
|
||||
final Offset globalPosition;
|
||||
|
||||
/// The local position of the pointer when it triggered this update.
|
||||
final Offset localPosition;
|
||||
|
||||
/// A delta offset from the point where the long press drag initially contacted
|
||||
/// the screen to the point where the pointer is currently located (the
|
||||
/// present [globalPosition]) when this callback is triggered.
|
||||
final Offset offsetFromOrigin;
|
||||
|
||||
/// A local delta offset from the point where the long press drag initially contacted
|
||||
/// the screen to the point where the pointer is currently located (the
|
||||
/// present [localPosition]) when this callback is triggered.
|
||||
final Offset localOffsetFromOrigin;
|
||||
}
|
||||
|
||||
/// Details for callbacks that use [GestureLongPressEndCallback].
|
||||
@ -95,11 +113,17 @@ class LongPressEndDetails {
|
||||
/// Creates the details for a [GestureLongPressEndCallback].
|
||||
///
|
||||
/// The [globalPosition] argument must not be null.
|
||||
const LongPressEndDetails({ this.globalPosition = Offset.zero })
|
||||
: assert(globalPosition != null);
|
||||
const LongPressEndDetails({
|
||||
this.globalPosition = Offset.zero,
|
||||
Offset localPosition,
|
||||
}) : assert(globalPosition != null),
|
||||
localPosition = localPosition ?? globalPosition;
|
||||
|
||||
/// The global position at which the pointer lifted from the screen.
|
||||
final Offset globalPosition;
|
||||
|
||||
/// The local position at which the pointer contacted the screen.
|
||||
final Offset localPosition;
|
||||
}
|
||||
|
||||
/// Recognizes when the user has pressed down at the same location for a long
|
||||
@ -137,7 +161,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
);
|
||||
|
||||
bool _longPressAccepted = false;
|
||||
Offset _longPressOrigin;
|
||||
OffsetPair _longPressOrigin;
|
||||
// The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
|
||||
// different set of buttons, the gesture is canceled.
|
||||
int _initialButtons;
|
||||
@ -230,7 +254,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
_reset();
|
||||
} else if (event is PointerDownEvent) {
|
||||
// The first touch.
|
||||
_longPressOrigin = event.position;
|
||||
_longPressOrigin = OffsetPair.fromEventPosition(event);
|
||||
_initialButtons = event.buttons;
|
||||
} else if (event is PointerMoveEvent) {
|
||||
if (event.buttons != _initialButtons) {
|
||||
@ -245,7 +269,8 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
void _checkLongPressStart() {
|
||||
assert(_initialButtons == kPrimaryButton);
|
||||
final LongPressStartDetails details = LongPressStartDetails(
|
||||
globalPosition: _longPressOrigin,
|
||||
globalPosition: _longPressOrigin.global,
|
||||
localPosition: _longPressOrigin.local,
|
||||
);
|
||||
if (onLongPressStart != null)
|
||||
invokeCallback<void>('onLongPressStart',
|
||||
@ -258,7 +283,9 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
assert(_initialButtons == kPrimaryButton);
|
||||
final LongPressMoveUpdateDetails details = LongPressMoveUpdateDetails(
|
||||
globalPosition: event.position,
|
||||
offsetFromOrigin: event.position - _longPressOrigin,
|
||||
localPosition: event.localPosition,
|
||||
offsetFromOrigin: event.position - _longPressOrigin.global,
|
||||
localOffsetFromOrigin: event.localPosition - _longPressOrigin.local,
|
||||
);
|
||||
if (onLongPressMoveUpdate != null)
|
||||
invokeCallback<void>('onLongPressMoveUpdate',
|
||||
@ -269,6 +296,7 @@ class LongPressGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
assert(_initialButtons == kPrimaryButton);
|
||||
final LongPressEndDetails details = LongPressEndDetails(
|
||||
globalPosition: event.position,
|
||||
localPosition: event.localPosition,
|
||||
);
|
||||
if (onLongPressEnd != null)
|
||||
invokeCallback<void>('onLongPressEnd', () => onLongPressEnd(details));
|
||||
|
@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'arena.dart';
|
||||
import 'constants.dart';
|
||||
@ -169,17 +170,24 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
double maxFlingVelocity;
|
||||
|
||||
_DragState _state = _DragState.ready;
|
||||
Offset _initialPosition;
|
||||
Offset _pendingDragOffset;
|
||||
OffsetPair _initialPosition;
|
||||
OffsetPair _pendingDragOffset;
|
||||
Duration _lastPendingEventTimestamp;
|
||||
// The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
|
||||
// different set of buttons, the gesture is canceled.
|
||||
int _initialButtons;
|
||||
Matrix4 _lastTransform;
|
||||
|
||||
/// Distance moved in the global coordinate space of the screen in drag direction.
|
||||
///
|
||||
/// If drag is only allowed along a defined axis, this value may be negative to
|
||||
/// differentiate the direction of the drag.
|
||||
double _globalDistanceMoved;
|
||||
|
||||
bool _isFlingGesture(VelocityEstimate estimate);
|
||||
Offset _getDeltaForDetails(Offset delta);
|
||||
double _getPrimaryValueFromOffset(Offset value);
|
||||
bool get _hasSufficientPendingDragDeltaToAccept;
|
||||
bool get _hasSufficientGlobalDistanceToAccept;
|
||||
|
||||
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
|
||||
|
||||
@ -209,14 +217,16 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
|
||||
@override
|
||||
void addAllowedPointer(PointerEvent event) {
|
||||
startTrackingPointer(event.pointer);
|
||||
startTrackingPointer(event.pointer, event.transform);
|
||||
_velocityTrackers[event.pointer] = VelocityTracker();
|
||||
if (_state == _DragState.ready) {
|
||||
_state = _DragState.possible;
|
||||
_initialPosition = event.position;
|
||||
_initialPosition = OffsetPair(global: event.position, local: event.localPosition);
|
||||
_initialButtons = event.buttons;
|
||||
_pendingDragOffset = Offset.zero;
|
||||
_pendingDragOffset = OffsetPair.zero;
|
||||
_globalDistanceMoved = 0.0;
|
||||
_lastPendingEventTimestamp = event.timeStamp;
|
||||
_lastTransform = event.transform;
|
||||
_checkDown();
|
||||
} else if (_state == _DragState.accepted) {
|
||||
resolve(GestureDisposition.accepted);
|
||||
@ -230,7 +240,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
&& (event is PointerDownEvent || event is PointerMoveEvent)) {
|
||||
final VelocityTracker tracker = _velocityTrackers[event.pointer];
|
||||
assert(tracker != null);
|
||||
tracker.addPosition(event.timeStamp, event.position);
|
||||
tracker.addPosition(event.timeStamp, event.localPosition);
|
||||
}
|
||||
|
||||
if (event is PointerMoveEvent) {
|
||||
@ -239,18 +249,26 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
stopTrackingPointer(event.pointer);
|
||||
return;
|
||||
}
|
||||
final Offset delta = event.delta;
|
||||
if (_state == _DragState.accepted) {
|
||||
_checkUpdate(
|
||||
sourceTimeStamp: event.timeStamp,
|
||||
delta: _getDeltaForDetails(delta),
|
||||
primaryDelta: _getPrimaryValueFromOffset(delta),
|
||||
delta: _getDeltaForDetails(event.localDelta),
|
||||
primaryDelta: _getPrimaryValueFromOffset(event.localDelta),
|
||||
globalPosition: event.position,
|
||||
localPosition: event.localPosition,
|
||||
);
|
||||
} else {
|
||||
_pendingDragOffset += delta;
|
||||
_pendingDragOffset += OffsetPair(local: event.localDelta, global: event.delta);
|
||||
_lastPendingEventTimestamp = event.timeStamp;
|
||||
if (_hasSufficientPendingDragDeltaToAccept)
|
||||
_lastTransform = event.transform;
|
||||
final Offset movedLocally = _getDeltaForDetails(event.localDelta);
|
||||
final Matrix4 localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform);
|
||||
_globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
|
||||
transform: localToGlobalTransform,
|
||||
untransformedDelta: movedLocally,
|
||||
untransformedEndPosition: event.localPosition,
|
||||
).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
|
||||
if (_hasSufficientGlobalDistanceToAccept)
|
||||
resolve(GestureDisposition.accepted);
|
||||
}
|
||||
}
|
||||
@ -261,27 +279,39 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
void acceptGesture(int pointer) {
|
||||
if (_state != _DragState.accepted) {
|
||||
_state = _DragState.accepted;
|
||||
final Offset delta = _pendingDragOffset;
|
||||
final OffsetPair delta = _pendingDragOffset;
|
||||
final Duration timestamp = _lastPendingEventTimestamp;
|
||||
Offset updateDelta;
|
||||
final Matrix4 transform = _lastTransform;
|
||||
Offset localUpdateDelta;
|
||||
switch (dragStartBehavior) {
|
||||
case DragStartBehavior.start:
|
||||
_initialPosition = _initialPosition + delta;
|
||||
updateDelta = Offset.zero;
|
||||
localUpdateDelta = Offset.zero;
|
||||
break;
|
||||
case DragStartBehavior.down:
|
||||
updateDelta = _getDeltaForDetails(delta);
|
||||
localUpdateDelta = _getDeltaForDetails(delta.local);
|
||||
break;
|
||||
}
|
||||
_pendingDragOffset = Offset.zero;
|
||||
_pendingDragOffset = OffsetPair.zero;
|
||||
_lastPendingEventTimestamp = null;
|
||||
_lastTransform = null;
|
||||
_checkStart(timestamp);
|
||||
if (updateDelta != Offset.zero) {
|
||||
if (localUpdateDelta != Offset.zero && onUpdate != null) {
|
||||
final Matrix4 localToGlobal = transform != null ? Matrix4.tryInvert(transform) : null;
|
||||
final Offset correctedLocalPosition = _initialPosition.local + localUpdateDelta;
|
||||
final Offset globalUpdateDelta = PointerEvent.transformDeltaViaPositions(
|
||||
untransformedEndPosition: correctedLocalPosition,
|
||||
untransformedDelta: localUpdateDelta,
|
||||
transform: localToGlobal,
|
||||
);
|
||||
final OffsetPair updateDelta = OffsetPair(local: localUpdateDelta, global: globalUpdateDelta);
|
||||
final OffsetPair correctedPosition = _initialPosition + updateDelta; // Only adds delta for down behaviour
|
||||
_checkUpdate(
|
||||
sourceTimeStamp: timestamp,
|
||||
delta: updateDelta,
|
||||
primaryDelta: _getPrimaryValueFromOffset(updateDelta),
|
||||
globalPosition: _initialPosition + updateDelta, // Only adds delta for down behavior
|
||||
delta: localUpdateDelta,
|
||||
primaryDelta: _getPrimaryValueFromOffset(localUpdateDelta),
|
||||
globalPosition: correctedPosition.global,
|
||||
localPosition: correctedPosition.local,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -316,7 +346,8 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
void _checkDown() {
|
||||
assert(_initialButtons == kPrimaryButton);
|
||||
final DragDownDetails details = DragDownDetails(
|
||||
globalPosition: _initialPosition,
|
||||
globalPosition: _initialPosition.global,
|
||||
localPosition: _initialPosition.local,
|
||||
);
|
||||
if (onDown != null)
|
||||
invokeCallback<void>('onDown', () => onDown(details));
|
||||
@ -326,7 +357,8 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
assert(_initialButtons == kPrimaryButton);
|
||||
final DragStartDetails details = DragStartDetails(
|
||||
sourceTimeStamp: timestamp,
|
||||
globalPosition: _initialPosition,
|
||||
globalPosition: _initialPosition.global,
|
||||
localPosition: _initialPosition.local,
|
||||
);
|
||||
if (onStart != null)
|
||||
invokeCallback<void>('onStart', () => onStart(details));
|
||||
@ -337,6 +369,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
Offset delta,
|
||||
double primaryDelta,
|
||||
Offset globalPosition,
|
||||
Offset localPosition,
|
||||
}) {
|
||||
assert(_initialButtons == kPrimaryButton);
|
||||
final DragUpdateDetails details = DragUpdateDetails(
|
||||
@ -344,6 +377,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
delta: delta,
|
||||
primaryDelta: primaryDelta,
|
||||
globalPosition: globalPosition,
|
||||
localPosition: localPosition,
|
||||
);
|
||||
if (onUpdate != null)
|
||||
invokeCallback<void>('onUpdate', () => onUpdate(details));
|
||||
@ -430,7 +464,7 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _hasSufficientPendingDragDeltaToAccept => _pendingDragOffset.dy.abs() > kTouchSlop;
|
||||
bool get _hasSufficientGlobalDistanceToAccept => _globalDistanceMoved.abs() > kTouchSlop;
|
||||
|
||||
@override
|
||||
Offset _getDeltaForDetails(Offset delta) => Offset(0.0, delta.dy);
|
||||
@ -469,7 +503,7 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _hasSufficientPendingDragDeltaToAccept => _pendingDragOffset.dx.abs() > kTouchSlop;
|
||||
bool get _hasSufficientGlobalDistanceToAccept => _globalDistanceMoved.abs() > kTouchSlop;
|
||||
|
||||
@override
|
||||
Offset _getDeltaForDetails(Offset delta) => Offset(delta.dx, 0.0);
|
||||
@ -503,8 +537,8 @@ class PanGestureRecognizer extends DragGestureRecognizer {
|
||||
}
|
||||
|
||||
@override
|
||||
bool get _hasSufficientPendingDragDeltaToAccept {
|
||||
return _pendingDragOffset.distance > kPanSlop;
|
||||
bool get _hasSufficientGlobalDistanceToAccept {
|
||||
return _globalDistanceMoved.abs() > kPanSlop;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -5,6 +5,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui' show Offset;
|
||||
import 'package:flutter/foundation.dart' show required;
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'arena.dart';
|
||||
import 'binding.dart';
|
||||
@ -66,22 +67,22 @@ class _TapTracker {
|
||||
assert(event != null),
|
||||
assert(event.buttons != null),
|
||||
pointer = event.pointer,
|
||||
_initialPosition = event.position,
|
||||
_initialGlobalPosition = event.position,
|
||||
initialButtons = event.buttons,
|
||||
_doubleTapMinTimeCountdown = _CountdownZoned(duration: doubleTapMinTime);
|
||||
|
||||
final int pointer;
|
||||
final GestureArenaEntry entry;
|
||||
final Offset _initialPosition;
|
||||
final Offset _initialGlobalPosition;
|
||||
final int initialButtons;
|
||||
final _CountdownZoned _doubleTapMinTimeCountdown;
|
||||
|
||||
bool _isTrackingPointer = false;
|
||||
|
||||
void startTrackingPointer(PointerRoute route) {
|
||||
void startTrackingPointer(PointerRoute route, Matrix4 transform) {
|
||||
if (!_isTrackingPointer) {
|
||||
_isTrackingPointer = true;
|
||||
GestureBinding.instance.pointerRouter.addRoute(pointer, route);
|
||||
GestureBinding.instance.pointerRouter.addRoute(pointer, route, transform);
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,8 +93,8 @@ class _TapTracker {
|
||||
}
|
||||
}
|
||||
|
||||
bool isWithinTolerance(PointerEvent event, double tolerance) {
|
||||
final Offset offset = event.position - _initialPosition;
|
||||
bool isWithinGlobalTolerance(PointerEvent event, double tolerance) {
|
||||
final Offset offset = event.position - _initialGlobalPosition;
|
||||
return offset.distance <= tolerance;
|
||||
}
|
||||
|
||||
@ -174,7 +175,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
|
||||
@override
|
||||
void addAllowedPointer(PointerEvent event) {
|
||||
if (_firstTap != null) {
|
||||
if (!_firstTap.isWithinTolerance(event, kDoubleTapSlop)) {
|
||||
if (!_firstTap.isWithinGlobalTolerance(event, kDoubleTapSlop)) {
|
||||
// Ignore out-of-bounds second taps.
|
||||
return;
|
||||
} else if (!_firstTap.hasElapsedMinTime() || !_firstTap.hasSameButton(event)) {
|
||||
@ -195,7 +196,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
|
||||
doubleTapMinTime: kDoubleTapMinTime,
|
||||
);
|
||||
_trackers[event.pointer] = tracker;
|
||||
tracker.startTrackingPointer(_handleEvent);
|
||||
tracker.startTrackingPointer(_handleEvent, event.transform);
|
||||
}
|
||||
|
||||
void _handleEvent(PointerEvent event) {
|
||||
@ -207,7 +208,7 @@ class DoubleTapGestureRecognizer extends GestureRecognizer {
|
||||
else
|
||||
_registerSecondTap(tracker);
|
||||
} else if (event is PointerMoveEvent) {
|
||||
if (!tracker.isWithinTolerance(event, kDoubleTapTouchSlop))
|
||||
if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop))
|
||||
_reject(tracker);
|
||||
} else if (event is PointerCancelEvent) {
|
||||
_reject(tracker);
|
||||
@ -320,13 +321,13 @@ class _TapGesture extends _TapTracker {
|
||||
this.gestureRecognizer,
|
||||
PointerEvent event,
|
||||
Duration longTapDelay,
|
||||
}) : _lastPosition = event.position,
|
||||
}) : _lastPosition = OffsetPair.fromEventPosition(event),
|
||||
super(
|
||||
event: event,
|
||||
entry: GestureBinding.instance.gestureArena.add(event.pointer, gestureRecognizer),
|
||||
doubleTapMinTime: kDoubleTapMinTime,
|
||||
) {
|
||||
startTrackingPointer(handleEvent);
|
||||
startTrackingPointer(handleEvent, event.transform);
|
||||
if (longTapDelay > Duration.zero) {
|
||||
_timer = Timer(longTapDelay, () {
|
||||
_timer = null;
|
||||
@ -340,21 +341,21 @@ class _TapGesture extends _TapTracker {
|
||||
bool _wonArena = false;
|
||||
Timer _timer;
|
||||
|
||||
Offset _lastPosition;
|
||||
Offset _finalPosition;
|
||||
OffsetPair _lastPosition;
|
||||
OffsetPair _finalPosition;
|
||||
|
||||
void handleEvent(PointerEvent event) {
|
||||
assert(event.pointer == pointer);
|
||||
if (event is PointerMoveEvent) {
|
||||
if (!isWithinTolerance(event, kTouchSlop))
|
||||
if (!isWithinGlobalTolerance(event, kTouchSlop))
|
||||
cancel();
|
||||
else
|
||||
_lastPosition = event.position;
|
||||
_lastPosition = OffsetPair.fromEventPosition(event);
|
||||
} else if (event is PointerCancelEvent) {
|
||||
cancel();
|
||||
} else if (event is PointerUpEvent) {
|
||||
stopTrackingPointer(handleEvent);
|
||||
_finalPosition = event.position;
|
||||
_finalPosition = OffsetPair.fromEventPosition(event);
|
||||
_check();
|
||||
}
|
||||
}
|
||||
@ -447,6 +448,7 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
|
||||
invokeCallback<void>('onTapDown', () {
|
||||
onTapDown(event.pointer, TapDownDetails(
|
||||
globalPosition: event.position,
|
||||
localPosition: event.localPosition,
|
||||
kind: event.kind,
|
||||
));
|
||||
});
|
||||
@ -472,23 +474,29 @@ class MultiTapGestureRecognizer extends GestureRecognizer {
|
||||
invokeCallback<void>('onTapCancel', () => onTapCancel(pointer));
|
||||
}
|
||||
|
||||
void _dispatchTap(int pointer, Offset globalPosition) {
|
||||
void _dispatchTap(int pointer, OffsetPair position) {
|
||||
assert(_gestureMap.containsKey(pointer));
|
||||
_gestureMap.remove(pointer);
|
||||
if (onTapUp != null)
|
||||
invokeCallback<void>('onTapUp', () => onTapUp(pointer, TapUpDetails(globalPosition: globalPosition)));
|
||||
invokeCallback<void>('onTapUp', () {
|
||||
onTapUp(pointer, TapUpDetails(
|
||||
localPosition: position.local,
|
||||
globalPosition: position.global,
|
||||
));
|
||||
});
|
||||
if (onTap != null)
|
||||
invokeCallback<void>('onTap', () => onTap(pointer));
|
||||
}
|
||||
|
||||
void _dispatchLongTap(int pointer, Offset lastPosition) {
|
||||
void _dispatchLongTap(int pointer, OffsetPair lastPosition) {
|
||||
assert(_gestureMap.containsKey(pointer));
|
||||
if (onLongTapDown != null)
|
||||
invokeCallback<void>('onLongTapDown', () {
|
||||
onLongTapDown(
|
||||
pointer,
|
||||
TapDownDetails(
|
||||
globalPosition: lastPosition,
|
||||
globalPosition: lastPosition.global,
|
||||
localPosition: lastPosition.local,
|
||||
kind: getKindForPointer(pointer),
|
||||
),
|
||||
);
|
||||
|
@ -5,6 +5,7 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'events.dart';
|
||||
|
||||
@ -13,8 +14,8 @@ typedef PointerRoute = void Function(PointerEvent event);
|
||||
|
||||
/// A routing table for [PointerEvent] events.
|
||||
class PointerRouter {
|
||||
final Map<int, LinkedHashSet<PointerRoute>> _routeMap = <int, LinkedHashSet<PointerRoute>>{};
|
||||
final LinkedHashSet<PointerRoute> _globalRoutes = LinkedHashSet<PointerRoute>();
|
||||
final Map<int, LinkedHashSet<_RouteEntry>> _routeMap = <int, LinkedHashSet<_RouteEntry>>{};
|
||||
final LinkedHashSet<_RouteEntry> _globalRoutes = LinkedHashSet<_RouteEntry>();
|
||||
|
||||
/// Adds a route to the routing table.
|
||||
///
|
||||
@ -23,10 +24,10 @@ class PointerRouter {
|
||||
///
|
||||
/// Routes added reentrantly within [PointerRouter.route] will take effect when
|
||||
/// routing the next event.
|
||||
void addRoute(int pointer, PointerRoute route) {
|
||||
final LinkedHashSet<PointerRoute> routes = _routeMap.putIfAbsent(pointer, () => LinkedHashSet<PointerRoute>());
|
||||
assert(!routes.contains(route));
|
||||
routes.add(route);
|
||||
void addRoute(int pointer, PointerRoute route, [Matrix4 transform]) {
|
||||
final LinkedHashSet<_RouteEntry> routes = _routeMap.putIfAbsent(pointer, () => LinkedHashSet<_RouteEntry>());
|
||||
assert(!routes.any(_RouteEntry.isRoutePredicate(route)));
|
||||
routes.add(_RouteEntry(route: route, transform: transform));
|
||||
}
|
||||
|
||||
/// Removes a route from the routing table.
|
||||
@ -38,9 +39,9 @@ class PointerRouter {
|
||||
/// immediately.
|
||||
void removeRoute(int pointer, PointerRoute route) {
|
||||
assert(_routeMap.containsKey(pointer));
|
||||
final LinkedHashSet<PointerRoute> routes = _routeMap[pointer];
|
||||
assert(routes.contains(route));
|
||||
routes.remove(route);
|
||||
final LinkedHashSet<_RouteEntry> routes = _routeMap[pointer];
|
||||
assert(routes.any(_RouteEntry.isRoutePredicate(route)));
|
||||
routes.removeWhere(_RouteEntry.isRoutePredicate(route));
|
||||
if (routes.isEmpty)
|
||||
_routeMap.remove(pointer);
|
||||
}
|
||||
@ -51,9 +52,9 @@ class PointerRouter {
|
||||
///
|
||||
/// Routes added reentrantly within [PointerRouter.route] will take effect when
|
||||
/// routing the next event.
|
||||
void addGlobalRoute(PointerRoute route) {
|
||||
assert(!_globalRoutes.contains(route));
|
||||
_globalRoutes.add(route);
|
||||
void addGlobalRoute(PointerRoute route, [Matrix4 transform]) {
|
||||
assert(!_globalRoutes.any(_RouteEntry.isRoutePredicate(route)));
|
||||
_globalRoutes.add(_RouteEntry(route: route, transform: transform));
|
||||
}
|
||||
|
||||
/// Removes a route from the global entry in the routing table.
|
||||
@ -64,13 +65,14 @@ class PointerRouter {
|
||||
/// Routes removed reentrantly within [PointerRouter.route] will take effect
|
||||
/// immediately.
|
||||
void removeGlobalRoute(PointerRoute route) {
|
||||
assert(_globalRoutes.contains(route));
|
||||
_globalRoutes.remove(route);
|
||||
assert(_globalRoutes.any(_RouteEntry.isRoutePredicate(route)));
|
||||
_globalRoutes.removeWhere(_RouteEntry.isRoutePredicate(route));
|
||||
}
|
||||
|
||||
void _dispatch(PointerEvent event, PointerRoute route) {
|
||||
void _dispatch(PointerEvent event, _RouteEntry entry) {
|
||||
try {
|
||||
route(event);
|
||||
event = event.transformed(entry.transform);
|
||||
entry.route(event);
|
||||
} catch (exception, stack) {
|
||||
FlutterError.reportError(FlutterErrorDetailsForPointerRouter(
|
||||
exception: exception,
|
||||
@ -78,7 +80,7 @@ class PointerRouter {
|
||||
library: 'gesture library',
|
||||
context: ErrorDescription('while routing a pointer event'),
|
||||
router: this,
|
||||
route: route,
|
||||
route: entry.route,
|
||||
event: event,
|
||||
informationCollector: () sync* {
|
||||
yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
|
||||
@ -92,17 +94,17 @@ class PointerRouter {
|
||||
/// Routes are called in the order in which they were added to the
|
||||
/// PointerRouter object.
|
||||
void route(PointerEvent event) {
|
||||
final LinkedHashSet<PointerRoute> routes = _routeMap[event.pointer];
|
||||
final List<PointerRoute> globalRoutes = List<PointerRoute>.from(_globalRoutes);
|
||||
final LinkedHashSet<_RouteEntry> routes = _routeMap[event.pointer];
|
||||
final List<_RouteEntry> globalRoutes = List<_RouteEntry>.from(_globalRoutes);
|
||||
if (routes != null) {
|
||||
for (PointerRoute route in List<PointerRoute>.from(routes)) {
|
||||
if (routes.contains(route))
|
||||
_dispatch(event, route);
|
||||
for (_RouteEntry entry in List<_RouteEntry>.from(routes)) {
|
||||
if (routes.any(_RouteEntry.isRoutePredicate(entry.route)))
|
||||
_dispatch(event, entry);
|
||||
}
|
||||
}
|
||||
for (PointerRoute route in globalRoutes) {
|
||||
if (_globalRoutes.contains(route))
|
||||
_dispatch(event, route);
|
||||
for (_RouteEntry entry in globalRoutes) {
|
||||
if (_globalRoutes.any(_RouteEntry.isRoutePredicate(entry.route)))
|
||||
_dispatch(event, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -149,3 +151,19 @@ class FlutterErrorDetailsForPointerRouter extends FlutterErrorDetails {
|
||||
/// The pointer event that was being routed when the exception was raised.
|
||||
final PointerEvent event;
|
||||
}
|
||||
|
||||
typedef _RouteEntryPredicate = bool Function(_RouteEntry entry);
|
||||
|
||||
class _RouteEntry {
|
||||
const _RouteEntry({
|
||||
@required this.route,
|
||||
@required this.transform,
|
||||
});
|
||||
|
||||
final PointerRoute route;
|
||||
final Matrix4 transform;
|
||||
|
||||
static _RouteEntryPredicate isRoutePredicate(PointerRoute route) {
|
||||
return (_RouteEntry entry) => entry.route == route;
|
||||
}
|
||||
}
|
||||
|
@ -47,9 +47,9 @@ class PointerSignalResolver {
|
||||
assert(_currentEvent == null);
|
||||
return;
|
||||
}
|
||||
assert(_currentEvent == event);
|
||||
assert((_currentEvent.original ?? _currentEvent) == event);
|
||||
try {
|
||||
_firstRegisteredCallback(event);
|
||||
_firstRegisteredCallback(_currentEvent);
|
||||
} catch (exception, stack) {
|
||||
FlutterError.reportError(FlutterErrorDetails(
|
||||
exception: exception,
|
||||
|
@ -6,6 +6,7 @@ import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:ui' show Offset;
|
||||
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'arena.dart';
|
||||
@ -293,12 +294,16 @@ abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
|
||||
|
||||
/// Causes events related to the given pointer ID to be routed to this recognizer.
|
||||
///
|
||||
/// The pointer events are delivered to [handleEvent].
|
||||
/// The pointer events are transformed according to `transform` and then delivered
|
||||
/// to [handleEvent]. The value for the `transform` argument is usually obtained
|
||||
/// from [PointerDownEvent.transform] to transform the events from the global
|
||||
/// coordinate space into the coordinate space of the event receiver. It may be
|
||||
/// null if no transformation is necessary.
|
||||
///
|
||||
/// Use [stopTrackingPointer] to remove the route added by this function.
|
||||
@protected
|
||||
void startTrackingPointer(int pointer) {
|
||||
GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent);
|
||||
void startTrackingPointer(int pointer, [Matrix4 transform]) {
|
||||
GestureBinding.instance.pointerRouter.addRoute(pointer, handleEvent, transform);
|
||||
_trackedPointers.add(pointer);
|
||||
assert(!_entries.containsValue(pointer));
|
||||
_entries[pointer] = _addPointerToArena(pointer);
|
||||
@ -410,8 +415,8 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
|
||||
/// The ID of the primary pointer this recognizer is tracking.
|
||||
int primaryPointer;
|
||||
|
||||
/// The global location at which the primary pointer contacted the screen.
|
||||
Offset initialPosition;
|
||||
/// The location at which the primary pointer contacted the screen.
|
||||
OffsetPair initialPosition;
|
||||
|
||||
// Whether this pointer is accepted by winning the arena or as defined by
|
||||
// a subclass calling acceptGesture.
|
||||
@ -420,11 +425,11 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
|
||||
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
startTrackingPointer(event.pointer);
|
||||
startTrackingPointer(event.pointer, event.transform);
|
||||
if (state == GestureRecognizerState.ready) {
|
||||
state = GestureRecognizerState.possible;
|
||||
primaryPointer = event.pointer;
|
||||
initialPosition = event.position;
|
||||
initialPosition = OffsetPair(local: event.localPosition, global: event.position);
|
||||
if (deadline != null)
|
||||
_timer = Timer(deadline, () => didExceedDeadlineWithEvent(event));
|
||||
}
|
||||
@ -437,11 +442,11 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
|
||||
final bool isPreAcceptSlopPastTolerance =
|
||||
!_gestureAccepted &&
|
||||
preAcceptSlopTolerance != null &&
|
||||
_getDistance(event) > preAcceptSlopTolerance;
|
||||
_getGlobalDistance(event) > preAcceptSlopTolerance;
|
||||
final bool isPostAcceptSlopPastTolerance =
|
||||
_gestureAccepted &&
|
||||
postAcceptSlopTolerance != null &&
|
||||
_getDistance(event) > postAcceptSlopTolerance;
|
||||
_getGlobalDistance(event) > postAcceptSlopTolerance;
|
||||
|
||||
if (event is PointerMoveEvent && (isPreAcceptSlopPastTolerance || isPostAcceptSlopPastTolerance)) {
|
||||
resolve(GestureDisposition.rejected);
|
||||
@ -509,8 +514,8 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
|
||||
}
|
||||
}
|
||||
|
||||
double _getDistance(PointerEvent event) {
|
||||
final Offset offset = event.position - initialPosition;
|
||||
double _getGlobalDistance(PointerEvent event) {
|
||||
final Offset offset = event.position - initialPosition.global;
|
||||
return offset.distance;
|
||||
}
|
||||
|
||||
@ -520,3 +525,57 @@ abstract class PrimaryPointerGestureRecognizer extends OneSequenceGestureRecogni
|
||||
properties.add(EnumProperty<GestureRecognizerState>('state', state));
|
||||
}
|
||||
}
|
||||
|
||||
/// A container for a [local] and [global] [Offset] pair.
|
||||
///
|
||||
/// Usually, the [global] [Offset] is in the coordinate space of the screen
|
||||
/// after conversion to logical pixels and the [local] offset is the same
|
||||
/// [Offset], but transformed to a local coordinate space.
|
||||
class OffsetPair {
|
||||
/// Creates a [OffsetPair] combining a [local] and [global] [Offset].
|
||||
const OffsetPair({
|
||||
@required this.local,
|
||||
@required this.global,
|
||||
});
|
||||
|
||||
/// Creates a [OffsetPair] from [PointerEvent.localPosition] and
|
||||
/// [PointerEvent.position].
|
||||
factory OffsetPair.fromEventPosition(PointerEvent event) {
|
||||
return OffsetPair(local: event.localPosition, global: event.position);
|
||||
}
|
||||
|
||||
/// Creates a [OffsetPair] from [PointerEvent.localDelta] and
|
||||
/// [PointerEvent.delta].
|
||||
factory OffsetPair.fromEventDelta(PointerEvent event) {
|
||||
return OffsetPair(local: event.localDelta, global: event.delta);
|
||||
}
|
||||
|
||||
/// A [OffsetPair] where both [Offset]s are [Offset.zero].
|
||||
static const OffsetPair zero = OffsetPair(local: Offset.zero, global: Offset.zero);
|
||||
|
||||
/// The [Offset] in the local coordinate space.
|
||||
final Offset local;
|
||||
|
||||
/// The [Offset] in the global coordinate space after conversion to logical
|
||||
/// pixels.
|
||||
final Offset global;
|
||||
|
||||
/// Adds the `other.global` to [global] and `other.local` to [local].
|
||||
OffsetPair operator+(OffsetPair other) {
|
||||
return OffsetPair(
|
||||
local: local + other.local,
|
||||
global: global + other.global,
|
||||
);
|
||||
}
|
||||
|
||||
/// Subtracts the `other.global` from [global] and `other.local` from [local].
|
||||
OffsetPair operator-(OffsetPair other) {
|
||||
return OffsetPair(
|
||||
local: local - other.local,
|
||||
global: global - other.global,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType(local: $local, global: $global)';
|
||||
}
|
||||
|
@ -21,14 +21,19 @@ class TapDownDetails {
|
||||
/// The [globalPosition] argument must not be null.
|
||||
TapDownDetails({
|
||||
this.globalPosition = Offset.zero,
|
||||
Offset localPosition,
|
||||
this.kind,
|
||||
}) : assert(globalPosition != null);
|
||||
}) : assert(globalPosition != null),
|
||||
localPosition = localPosition ?? globalPosition;
|
||||
|
||||
/// The global position at which the pointer contacted the screen.
|
||||
final Offset globalPosition;
|
||||
|
||||
/// The kind of the device that initiated the event.
|
||||
final PointerDeviceKind kind;
|
||||
|
||||
/// The local position at which the pointer contacted the screen.
|
||||
final Offset localPosition;
|
||||
}
|
||||
|
||||
/// Signature for when a pointer that might cause a tap has contacted the
|
||||
@ -51,11 +56,17 @@ typedef GestureTapDownCallback = void Function(TapDownDetails details);
|
||||
/// * [TapGestureRecognizer], which passes this information to one of its callbacks.
|
||||
class TapUpDetails {
|
||||
/// The [globalPosition] argument must not be null.
|
||||
TapUpDetails({ this.globalPosition = Offset.zero })
|
||||
: assert(globalPosition != null);
|
||||
TapUpDetails({
|
||||
this.globalPosition = Offset.zero,
|
||||
Offset localPosition,
|
||||
}) : assert(globalPosition != null),
|
||||
localPosition = localPosition ?? globalPosition;
|
||||
|
||||
/// The global position at which the pointer contacted the screen.
|
||||
final Offset globalPosition;
|
||||
|
||||
/// The local position at which the pointer contacted the screen.
|
||||
final Offset localPosition;
|
||||
}
|
||||
|
||||
/// Signature for when a pointer that will trigger a tap has stopped contacting
|
||||
@ -221,7 +232,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
|
||||
bool _sentTapDown = false;
|
||||
bool _wonArenaForPrimaryPointer = false;
|
||||
Offset _finalPosition;
|
||||
OffsetPair _finalPosition;
|
||||
// The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
|
||||
// different set of buttons, the gesture is canceled.
|
||||
int _initialButtons;
|
||||
@ -260,7 +271,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
@override
|
||||
void handlePrimaryPointer(PointerEvent event) {
|
||||
if (event is PointerUpEvent) {
|
||||
_finalPosition = event.position;
|
||||
_finalPosition = OffsetPair(global: event.position, local: event.localPosition);
|
||||
_checkUp();
|
||||
} else if (event is PointerCancelEvent) {
|
||||
resolve(GestureDisposition.rejected);
|
||||
@ -319,7 +330,8 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
return;
|
||||
}
|
||||
final TapDownDetails details = TapDownDetails(
|
||||
globalPosition: initialPosition,
|
||||
globalPosition: initialPosition.global,
|
||||
localPosition: initialPosition.local,
|
||||
kind: getKindForPointer(pointer),
|
||||
);
|
||||
switch (_initialButtons) {
|
||||
@ -342,7 +354,8 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
return;
|
||||
}
|
||||
final TapUpDetails details = TapUpDetails(
|
||||
globalPosition: _finalPosition,
|
||||
globalPosition: _finalPosition.global,
|
||||
localPosition: _finalPosition.local,
|
||||
);
|
||||
switch (_initialButtons) {
|
||||
case kPrimaryButton:
|
||||
@ -390,7 +403,8 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena'));
|
||||
properties.add(DiagnosticsProperty<Offset>('finalPosition', _finalPosition, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<Offset>('finalPosition', _finalPosition?.global, defaultValue: null));
|
||||
properties.add(DiagnosticsProperty<Offset>('finalLocalPosition', _finalPosition?.local, defaultValue: _finalPosition?.global));
|
||||
properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down'));
|
||||
// TODO(tongmu): Add property _initialButtons and update related tests
|
||||
}
|
||||
|
@ -657,8 +657,10 @@ class BoxHitTestResult extends HitTestResult {
|
||||
/// provided `hitTest` callback, which is invoked with the transformed
|
||||
/// `position` as argument.
|
||||
///
|
||||
/// Since the provided paint `transform` describes the transform from the
|
||||
/// child to the parent, the matrix is inverted before it is used to transform
|
||||
/// The provided paint `transform` (which describes the transform from the
|
||||
/// child to the parent in 3D) is processed by
|
||||
/// [PointerEvent.removePerspectiveTransform] to remove the
|
||||
/// perspective component and inverted before it is used to transform
|
||||
/// `position` from the coordinate system of the parent to the system of the
|
||||
/// child.
|
||||
///
|
||||
@ -674,7 +676,8 @@ class BoxHitTestResult extends HitTestResult {
|
||||
/// position is not required to do the actual hit testing in that protocol.
|
||||
///
|
||||
/// {@tool sample}
|
||||
/// This method is used in [RenderBox.hitTestChildren]:
|
||||
/// This method is used in [RenderBox.hitTestChildren] when the child and
|
||||
/// parent don't share the same origin.
|
||||
///
|
||||
/// ```dart
|
||||
/// abstract class Foo extends RenderBox {
|
||||
@ -713,7 +716,7 @@ class BoxHitTestResult extends HitTestResult {
|
||||
}) {
|
||||
assert(hitTest != null);
|
||||
if (transform != null) {
|
||||
transform = Matrix4.tryInvert(transform);
|
||||
transform = Matrix4.tryInvert(PointerEvent.removePerspectiveTransform(transform));
|
||||
if (transform == null) {
|
||||
// Objects are not visible on screen and cannot be hit-tested.
|
||||
return false;
|
||||
@ -788,7 +791,14 @@ class BoxHitTestResult extends HitTestResult {
|
||||
final Offset transformedPosition = position == null || transform == null
|
||||
? position
|
||||
: MatrixUtils.transformPoint(transform, position);
|
||||
return hitTest(this, transformedPosition);
|
||||
if (transform != null) {
|
||||
pushTransform(transform);
|
||||
}
|
||||
final bool isHit = hitTest(this, transformedPosition);
|
||||
if (transform != null) {
|
||||
popTransform();
|
||||
}
|
||||
return isHit;
|
||||
}
|
||||
}
|
||||
|
||||
@ -797,7 +807,7 @@ class BoxHitTestEntry extends HitTestEntry {
|
||||
/// Creates a box hit test entry.
|
||||
///
|
||||
/// The [localPosition] argument must not be null.
|
||||
const BoxHitTestEntry(RenderBox target, this.localPosition)
|
||||
BoxHitTestEntry(RenderBox target, this.localPosition)
|
||||
: assert(localPosition != null),
|
||||
super(target);
|
||||
|
||||
@ -2057,10 +2067,11 @@ abstract class RenderBox extends RenderObject {
|
||||
/// This [RenderBox] is responsible for checking whether the given position is
|
||||
/// within its bounds.
|
||||
///
|
||||
/// If transforming is necessary, [BoxHitTestResult.addWithPaintTransform],
|
||||
/// [BoxHitTestResult.addWithPaintOffset], or
|
||||
/// [BoxHitTestResult.addWithRawTransform] should be used to transform
|
||||
/// `position` to the local coordinate system.
|
||||
/// If transforming is necessary, [HitTestResult.addWithPaintTransform],
|
||||
/// [HitTestResult.addWithPaintOffset], or [HitTestResult.addWithRawTransform] need
|
||||
/// to be invoked by the caller to record the required transform operations
|
||||
/// in the [HitTestResult]. These methods will also help with applying the
|
||||
/// transform to `position`.
|
||||
///
|
||||
/// Hit testing requires layout to be up-to-date but does not require painting
|
||||
/// to be up-to-date. That means a render object can rely upon [performLayout]
|
||||
@ -2130,10 +2141,11 @@ abstract class RenderBox extends RenderObject {
|
||||
/// This [RenderBox] is responsible for checking whether the given position is
|
||||
/// within its bounds.
|
||||
///
|
||||
/// If transforming is necessary, [BoxHitTestResult.addWithPaintTransform],
|
||||
/// [BoxHitTestResult.addWithPaintOffset], or
|
||||
/// [BoxHitTestResult.addWithRawTransform] should be used to transform
|
||||
/// `position` to the local coordinate system.
|
||||
/// If transforming is necessary, [HitTestResult.addWithPaintTransform],
|
||||
/// [HitTestResult.addWithPaintOffset], or [HitTestResult.addWithRawTransform] need
|
||||
/// to be invoked by the caller to record the required transform operations
|
||||
/// in the [HitTestResult]. These methods will also help with applying the
|
||||
/// transform to `position`.
|
||||
///
|
||||
/// Used by [hitTest]. If you override [hitTest] and do not call this
|
||||
/// function, then you don't need to implement this function.
|
||||
|
@ -8,6 +8,7 @@ import 'dart:ui' as ui show EngineLayer, Image, ImageFilter, PathMetric,
|
||||
Picture, PictureRecorder, Scene, SceneBuilder;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
@ -1237,7 +1238,9 @@ class TransformLayer extends OffsetLayer {
|
||||
|
||||
Offset _transformOffset(Offset regionOffset) {
|
||||
if (_inverseDirty) {
|
||||
_invertedTransform = Matrix4.tryInvert(transform);
|
||||
_invertedTransform = Matrix4.tryInvert(
|
||||
PointerEvent.removePerspectiveTransform(transform)
|
||||
);
|
||||
_inverseDirty = false;
|
||||
}
|
||||
if (_invertedTransform == null)
|
||||
|
@ -984,9 +984,7 @@ class RenderListWheelViewport
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
|
||||
return false;
|
||||
}
|
||||
bool hitTestChildren(BoxHitTestResult result, { Offset position }) => false;
|
||||
|
||||
@override
|
||||
RevealedOffset getOffsetToReveal(RenderObject target, double alignment, { Rect rect }) {
|
||||
|
@ -381,7 +381,7 @@ class RenderUiKitView extends RenderBox {
|
||||
return;
|
||||
}
|
||||
_gestureRecognizer.addPointer(event);
|
||||
_lastPointerDownEvent = event;
|
||||
_lastPointerDownEvent = event.original ?? event;
|
||||
}
|
||||
|
||||
// This is registered as a global PointerRoute while the render object is attached.
|
||||
@ -389,11 +389,10 @@ class RenderUiKitView extends RenderBox {
|
||||
if (event is! PointerDownEvent) {
|
||||
return;
|
||||
}
|
||||
final Offset localOffset = globalToLocal(event.position);
|
||||
if (!(Offset.zero & size).contains(localOffset)) {
|
||||
if (!(Offset.zero & size).contains(event.localPosition)) {
|
||||
return;
|
||||
}
|
||||
if (event != _lastPointerDownEvent) {
|
||||
if ((event.original ?? event) != _lastPointerDownEvent) {
|
||||
// The pointer event is in the bounds of this render box, but we didn't get it in handleEvent.
|
||||
// This means that the pointer event was absorbed by a different render object.
|
||||
// Since on the platform side the FlutterTouchIntercepting view is seeing all events that are
|
||||
@ -455,7 +454,7 @@ class _UiKitViewGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
startTrackingPointer(event.pointer);
|
||||
startTrackingPointer(event.pointer, event.transform);
|
||||
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
|
||||
recognizer.addPointer(event);
|
||||
}
|
||||
@ -528,7 +527,7 @@ class _AndroidViewGestureRecognizer extends OneSequenceGestureRecognizer {
|
||||
|
||||
@override
|
||||
void addAllowedPointer(PointerDownEvent event) {
|
||||
startTrackingPointer(event.pointer);
|
||||
startTrackingPointer(event.pointer, event.transform);
|
||||
for (OneSequenceGestureRecognizer recognizer in _gestureRecognizers) {
|
||||
recognizer.addPointer(event);
|
||||
}
|
||||
|
@ -846,12 +846,18 @@ class SliverHitTestResult extends HitTestResult {
|
||||
assert(mainAxisPosition != null);
|
||||
assert(crossAxisPosition != null);
|
||||
assert(hitTest != null);
|
||||
// TODO(goderbauer): use paintOffset when transforming pointer events is implemented.
|
||||
return hitTest(
|
||||
if (paintOffset != null) {
|
||||
pushTransform(Matrix4.translationValues(paintOffset.dx, paintOffset.dy, 0));
|
||||
}
|
||||
final bool isHit = hitTest(
|
||||
this,
|
||||
mainAxisPosition: mainAxisPosition - mainAxisOffset,
|
||||
crossAxisPosition: crossAxisPosition - crossAxisOffset,
|
||||
);
|
||||
if (paintOffset != null) {
|
||||
popTransform();
|
||||
}
|
||||
return isHit;
|
||||
}
|
||||
}
|
||||
|
||||
@ -863,7 +869,7 @@ class SliverHitTestEntry extends HitTestEntry {
|
||||
/// Creates a sliver hit test entry.
|
||||
///
|
||||
/// The [mainAxisPosition] and [crossAxisPosition] arguments must not be null.
|
||||
const SliverHitTestEntry(
|
||||
SliverHitTestEntry(
|
||||
RenderSliver target, {
|
||||
@required this.mainAxisPosition,
|
||||
@required this.crossAxisPosition,
|
||||
|
@ -66,9 +66,7 @@ class TextureBox extends RenderBox {
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestSelf(Offset position) {
|
||||
return true;
|
||||
}
|
||||
bool hitTestSelf(Offset position) => true;
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
|
@ -663,7 +663,7 @@ class _DragAvatar<T> extends Drag {
|
||||
_activeTarget = newTarget;
|
||||
}
|
||||
|
||||
Iterable<_DragTargetState<T>> _getDragTargets(List<HitTestEntry> path) sync* {
|
||||
Iterable<_DragTargetState<T>> _getDragTargets(Iterable<HitTestEntry> path) sync* {
|
||||
// Look for the RenderBoxes that corresponds to the hit target (the hit target
|
||||
// widgets build RenderMetaData boxes for us for this purpose).
|
||||
for (HitTestEntry entry in path) {
|
||||
|
@ -2,8 +2,10 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'gesture_tester.dart';
|
||||
|
||||
@ -124,4 +126,344 @@ void main() {
|
||||
expect(event.buttons, kPrimaryButton);
|
||||
});
|
||||
});
|
||||
|
||||
test('paintTransformToPointerEventTransform', () {
|
||||
Matrix4 original = Matrix4.identity();
|
||||
Matrix4 changed = PointerEvent.removePerspectiveTransform(original);
|
||||
expect(changed, original);
|
||||
|
||||
original = Matrix4.identity()..scale(3.0);
|
||||
changed = PointerEvent.removePerspectiveTransform(original);
|
||||
expect(changed, isNot(original));
|
||||
original
|
||||
..setColumn(2, Vector4(0, 0, 1, 0))
|
||||
..setRow(2, Vector4(0, 0, 1, 0));
|
||||
expect(changed, original);
|
||||
});
|
||||
|
||||
test('transformPosition', () {
|
||||
const Offset position = Offset(20, 30);
|
||||
expect(PointerEvent.transformPosition(null, position), position);
|
||||
expect(PointerEvent.transformPosition(Matrix4.identity(), position), position);
|
||||
final Matrix4 transform = Matrix4.translationValues(10, 20, 0);
|
||||
expect(PointerEvent.transformPosition(transform, position), const Offset(20.0 + 10.0, 30.0 + 20.0));
|
||||
});
|
||||
|
||||
test('transformDeltaViaPositions', () {
|
||||
Offset transformedDelta = PointerEvent.transformDeltaViaPositions(
|
||||
untransformedEndPosition: const Offset(20, 30),
|
||||
untransformedDelta: const Offset(5, 5),
|
||||
transform: Matrix4.identity()..scale(2.0, 2.0, 1.0),
|
||||
);
|
||||
expect(transformedDelta, const Offset(10.0, 10.0));
|
||||
|
||||
transformedDelta = PointerEvent.transformDeltaViaPositions(
|
||||
untransformedEndPosition: const Offset(20, 30),
|
||||
transformedEndPosition: const Offset(40, 60),
|
||||
untransformedDelta: const Offset(5, 5),
|
||||
transform: Matrix4.identity()..scale(2.0, 2.0, 1.0),
|
||||
);
|
||||
expect(transformedDelta, const Offset(10.0, 10.0));
|
||||
|
||||
transformedDelta = PointerEvent.transformDeltaViaPositions(
|
||||
untransformedEndPosition: const Offset(20, 30),
|
||||
transformedEndPosition: const Offset(40, 60),
|
||||
untransformedDelta: const Offset(5, 5),
|
||||
transform: null,
|
||||
);
|
||||
expect(transformedDelta, const Offset(5, 5));
|
||||
});
|
||||
|
||||
test('transforming events', () {
|
||||
final Matrix4 transform = (Matrix4.identity()..scale(2.0, 2.0, 1.0)) * Matrix4.translationValues(10.0, 20.0, 0.0);
|
||||
const Offset localPosition = Offset(60, 100);
|
||||
const Offset localDelta = Offset(10, 10);
|
||||
|
||||
const PointerAddedEvent added = PointerAddedEvent(
|
||||
timeStamp: Duration(seconds: 2),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
device: 1,
|
||||
position: Offset(20, 30),
|
||||
obscured: true,
|
||||
pressureMin: 10,
|
||||
pressureMax: 60,
|
||||
distance: 12,
|
||||
distanceMax: 24,
|
||||
radiusMin: 10,
|
||||
radiusMax: 50,
|
||||
orientation: 2,
|
||||
tilt: 4,
|
||||
);
|
||||
_expectTransformedEvent(
|
||||
original: added,
|
||||
transform: transform,
|
||||
localPosition: localPosition,
|
||||
);
|
||||
|
||||
const PointerCancelEvent cancel = PointerCancelEvent(
|
||||
timeStamp: Duration(seconds: 2),
|
||||
pointer: 45,
|
||||
kind: PointerDeviceKind.mouse,
|
||||
device: 1,
|
||||
position: Offset(20, 30),
|
||||
buttons: 4,
|
||||
obscured: true,
|
||||
pressureMin: 10,
|
||||
pressureMax: 60,
|
||||
distance: 12,
|
||||
distanceMax: 24,
|
||||
size: 10,
|
||||
radiusMajor: 33,
|
||||
radiusMinor: 44,
|
||||
radiusMin: 10,
|
||||
radiusMax: 50,
|
||||
orientation: 2,
|
||||
tilt: 4,
|
||||
);
|
||||
_expectTransformedEvent(
|
||||
original: cancel,
|
||||
transform: transform,
|
||||
localPosition: localPosition,
|
||||
);
|
||||
|
||||
const PointerDownEvent down = PointerDownEvent(
|
||||
timeStamp: Duration(seconds: 2),
|
||||
pointer: 45,
|
||||
kind: PointerDeviceKind.mouse,
|
||||
device: 1,
|
||||
position: Offset(20, 30),
|
||||
buttons: 4,
|
||||
obscured: true,
|
||||
pressure: 34,
|
||||
pressureMin: 10,
|
||||
pressureMax: 60,
|
||||
distanceMax: 24,
|
||||
size: 10,
|
||||
radiusMajor: 33,
|
||||
radiusMinor: 44,
|
||||
radiusMin: 10,
|
||||
radiusMax: 50,
|
||||
orientation: 2,
|
||||
tilt: 4,
|
||||
);
|
||||
_expectTransformedEvent(
|
||||
original: down,
|
||||
transform: transform,
|
||||
localPosition: localPosition,
|
||||
);
|
||||
|
||||
const PointerEnterEvent enter = PointerEnterEvent(
|
||||
timeStamp: Duration(seconds: 2),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
device: 1,
|
||||
position: Offset(20, 30),
|
||||
delta: Offset(5, 5),
|
||||
buttons: 4,
|
||||
obscured: true,
|
||||
pressureMin: 10,
|
||||
pressureMax: 60,
|
||||
distance: 12,
|
||||
distanceMax: 24,
|
||||
size: 10,
|
||||
radiusMajor: 33,
|
||||
radiusMinor: 44,
|
||||
radiusMin: 10,
|
||||
radiusMax: 50,
|
||||
orientation: 2,
|
||||
tilt: 4,
|
||||
synthesized: true,
|
||||
);
|
||||
_expectTransformedEvent(
|
||||
original: enter,
|
||||
transform: transform,
|
||||
localPosition: localPosition,
|
||||
localDelta: localDelta,
|
||||
);
|
||||
|
||||
const PointerExitEvent exit = PointerExitEvent(
|
||||
timeStamp: Duration(seconds: 2),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
device: 1,
|
||||
position: Offset(20, 30),
|
||||
delta: Offset(5, 5),
|
||||
buttons: 4,
|
||||
obscured: true,
|
||||
pressureMin: 10,
|
||||
pressureMax: 60,
|
||||
distance: 12,
|
||||
distanceMax: 24,
|
||||
size: 10,
|
||||
radiusMajor: 33,
|
||||
radiusMinor: 44,
|
||||
radiusMin: 10,
|
||||
radiusMax: 50,
|
||||
orientation: 2,
|
||||
tilt: 4,
|
||||
synthesized: true,
|
||||
);
|
||||
_expectTransformedEvent(
|
||||
original: exit,
|
||||
transform: transform,
|
||||
localPosition: localPosition,
|
||||
localDelta: localDelta,
|
||||
);
|
||||
|
||||
const PointerHoverEvent hover = PointerHoverEvent(
|
||||
timeStamp: Duration(seconds: 2),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
device: 1,
|
||||
position: Offset(20, 30),
|
||||
delta: Offset(5, 5),
|
||||
buttons: 4,
|
||||
obscured: true,
|
||||
pressureMin: 10,
|
||||
pressureMax: 60,
|
||||
distance: 12,
|
||||
distanceMax: 24,
|
||||
size: 10,
|
||||
radiusMajor: 33,
|
||||
radiusMinor: 44,
|
||||
radiusMin: 10,
|
||||
radiusMax: 50,
|
||||
orientation: 2,
|
||||
tilt: 4,
|
||||
synthesized: true,
|
||||
);
|
||||
_expectTransformedEvent(
|
||||
original: hover,
|
||||
transform: transform,
|
||||
localPosition: localPosition,
|
||||
localDelta: localDelta,
|
||||
);
|
||||
|
||||
const PointerMoveEvent move = PointerMoveEvent(
|
||||
timeStamp: Duration(seconds: 2),
|
||||
pointer: 45,
|
||||
kind: PointerDeviceKind.mouse,
|
||||
device: 1,
|
||||
position: Offset(20, 30),
|
||||
delta: Offset(5, 5),
|
||||
buttons: 4,
|
||||
obscured: true,
|
||||
pressure: 34,
|
||||
pressureMin: 10,
|
||||
pressureMax: 60,
|
||||
distanceMax: 24,
|
||||
size: 10,
|
||||
radiusMajor: 33,
|
||||
radiusMinor: 44,
|
||||
radiusMin: 10,
|
||||
radiusMax: 50,
|
||||
orientation: 2,
|
||||
tilt: 4,
|
||||
platformData: 10,
|
||||
synthesized: true,
|
||||
);
|
||||
_expectTransformedEvent(
|
||||
original: move,
|
||||
transform: transform,
|
||||
localPosition: localPosition,
|
||||
localDelta: localDelta,
|
||||
);
|
||||
|
||||
const PointerRemovedEvent removed = PointerRemovedEvent(
|
||||
timeStamp: Duration(seconds: 2),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
device: 1,
|
||||
position: Offset(20, 30),
|
||||
obscured: true,
|
||||
pressureMin: 10,
|
||||
pressureMax: 60,
|
||||
distanceMax: 24,
|
||||
radiusMin: 10,
|
||||
radiusMax: 50,
|
||||
);
|
||||
_expectTransformedEvent(
|
||||
original: removed,
|
||||
transform: transform,
|
||||
localPosition: localPosition,
|
||||
);
|
||||
|
||||
const PointerScrollEvent scroll = PointerScrollEvent(
|
||||
timeStamp: Duration(seconds: 2),
|
||||
kind: PointerDeviceKind.mouse,
|
||||
device: 1,
|
||||
position: Offset(20, 30),
|
||||
);
|
||||
_expectTransformedEvent(
|
||||
original: scroll,
|
||||
transform: transform,
|
||||
localPosition: localPosition,
|
||||
);
|
||||
|
||||
const PointerUpEvent up = PointerUpEvent(
|
||||
timeStamp: Duration(seconds: 2),
|
||||
pointer: 45,
|
||||
kind: PointerDeviceKind.mouse,
|
||||
device: 1,
|
||||
position: Offset(20, 30),
|
||||
buttons: 4,
|
||||
obscured: true,
|
||||
pressure: 34,
|
||||
pressureMin: 10,
|
||||
pressureMax: 60,
|
||||
distance: 12,
|
||||
distanceMax: 24,
|
||||
size: 10,
|
||||
radiusMajor: 33,
|
||||
radiusMinor: 44,
|
||||
radiusMin: 10,
|
||||
radiusMax: 50,
|
||||
orientation: 2,
|
||||
tilt: 4,
|
||||
);
|
||||
_expectTransformedEvent(
|
||||
original: up,
|
||||
transform: transform,
|
||||
localPosition: localPosition,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _expectTransformedEvent({
|
||||
@required PointerEvent original,
|
||||
@required Matrix4 transform,
|
||||
Offset localDelta,
|
||||
Offset localPosition,
|
||||
}) {
|
||||
expect(original.position, original.localPosition);
|
||||
expect(original.delta, original.localDelta);
|
||||
expect(original.original, isNull);
|
||||
expect(original.transform, isNull);
|
||||
|
||||
final PointerEvent transformed = original.transformed(transform);
|
||||
expect(transformed.original, same(original));
|
||||
expect(transformed.transform, transform);
|
||||
expect(transformed.localDelta, localDelta ?? original.localDelta);
|
||||
expect(transformed.localPosition, localPosition ?? original.localPosition);
|
||||
|
||||
expect(transformed.buttons, original.buttons);
|
||||
expect(transformed.delta, original.delta);
|
||||
expect(transformed.device, original.device);
|
||||
expect(transformed.distance, original.distance);
|
||||
expect(transformed.distanceMax, original.distanceMax);
|
||||
expect(transformed.distanceMin, original.distanceMin);
|
||||
expect(transformed.down, original.down);
|
||||
expect(transformed.kind, original.kind);
|
||||
expect(transformed.obscured, original.obscured);
|
||||
expect(transformed.orientation, original.orientation);
|
||||
expect(transformed.platformData, original.platformData);
|
||||
expect(transformed.pointer, original.pointer);
|
||||
expect(transformed.position, original.position);
|
||||
expect(transformed.pressure, original.pressure);
|
||||
expect(transformed.pressureMax, original.pressureMax);
|
||||
expect(transformed.pressureMin, original.pressureMin);
|
||||
expect(transformed.radiusMajor, original.radiusMajor);
|
||||
expect(transformed.radiusMax, original.radiusMax);
|
||||
expect(transformed.radiusMin, original.radiusMin);
|
||||
expect(transformed.radiusMinor, original.radiusMinor);
|
||||
expect(transformed.size, original.size);
|
||||
expect(transformed.synthesized, original.synthesized);
|
||||
expect(transformed.tilt, original.tilt);
|
||||
expect(transformed.timeStamp, original.timeStamp);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import '../flutter_test_alternative.dart';
|
||||
|
||||
@ -11,10 +12,13 @@ void main() {
|
||||
final HitTestEntry entry1 = HitTestEntry(_DummyHitTestTarget());
|
||||
final HitTestEntry entry2 = HitTestEntry(_DummyHitTestTarget());
|
||||
final HitTestEntry entry3 = HitTestEntry(_DummyHitTestTarget());
|
||||
final Matrix4 transform = Matrix4.translationValues(40.0, 150.0, 0.0);
|
||||
|
||||
final HitTestResult wrapped = HitTestResult();
|
||||
final HitTestResult wrapped = MyHitTestResult()
|
||||
..publicPushTransform(transform);
|
||||
wrapped.add(entry1);
|
||||
expect(wrapped.path, equals(<HitTestEntry>[entry1]));
|
||||
expect(entry1.transform, transform);
|
||||
|
||||
final HitTestResult wrapping = HitTestResult.wrap(wrapped);
|
||||
expect(wrapping.path, equals(<HitTestEntry>[entry1]));
|
||||
@ -23,10 +27,12 @@ void main() {
|
||||
wrapping.add(entry2);
|
||||
expect(wrapping.path, equals(<HitTestEntry>[entry1, entry2]));
|
||||
expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2]));
|
||||
expect(entry2.transform, transform);
|
||||
|
||||
wrapped.add(entry3);
|
||||
expect(wrapping.path, equals(<HitTestEntry>[entry1, entry2, entry3]));
|
||||
expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2, entry3]));
|
||||
expect(entry3.transform, transform);
|
||||
});
|
||||
}
|
||||
|
||||
@ -36,3 +42,7 @@ class _DummyHitTestTarget implements HitTestTarget {
|
||||
// Nothing to do.
|
||||
}
|
||||
}
|
||||
|
||||
class MyHitTestResult extends HitTestResult {
|
||||
void publicPushTransform(Matrix4 transform) => pushTransform(transform);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
void main() {
|
||||
test('Should route pointers', () {
|
||||
@ -149,4 +150,53 @@ void main() {
|
||||
|
||||
FlutterError.onError = previousErrorHandler;
|
||||
});
|
||||
|
||||
test('Should transform events', () {
|
||||
final List<PointerEvent> events = <PointerEvent>[];
|
||||
final List<PointerEvent> globalEvents = <PointerEvent>[];
|
||||
final PointerRouter router = PointerRouter();
|
||||
final Matrix4 transform = (Matrix4.identity()..scale(1 / 2.0, 1 / 2.0, 1.0)) * Matrix4.translationValues(-10, -30, 0);
|
||||
|
||||
router.addRoute(1, (PointerEvent event) {
|
||||
events.add(event);
|
||||
}, transform);
|
||||
|
||||
router.addGlobalRoute((PointerEvent event) {
|
||||
globalEvents.add(event);
|
||||
}, transform);
|
||||
|
||||
final TestPointer pointer1 = TestPointer(1);
|
||||
const Offset firstPosition = Offset(16, 36);
|
||||
router.route(pointer1.down(firstPosition));
|
||||
|
||||
expect(events.single.transform, transform);
|
||||
expect(events.single.position, firstPosition);
|
||||
expect(events.single.delta, Offset.zero);
|
||||
expect(events.single.localPosition, const Offset(3, 3));
|
||||
expect(events.single.localDelta, Offset.zero);
|
||||
|
||||
expect(globalEvents.single.transform, transform);
|
||||
expect(globalEvents.single.position, firstPosition);
|
||||
expect(globalEvents.single.delta, Offset.zero);
|
||||
expect(globalEvents.single.localPosition, const Offset(3, 3));
|
||||
expect(globalEvents.single.localDelta, Offset.zero);
|
||||
|
||||
events.clear();
|
||||
globalEvents.clear();
|
||||
|
||||
const Offset newPosition = Offset(20, 40);
|
||||
router.route(pointer1.move(newPosition));
|
||||
|
||||
expect(events.single.transform, transform);
|
||||
expect(events.single.position, newPosition);
|
||||
expect(events.single.delta, newPosition - firstPosition);
|
||||
expect(events.single.localPosition, const Offset(5, 5));
|
||||
expect(events.single.localDelta, const Offset(2, 2));
|
||||
|
||||
expect(globalEvents.single.transform, transform);
|
||||
expect(globalEvents.single.position, newPosition);
|
||||
expect(globalEvents.single.delta, newPosition - firstPosition);
|
||||
expect(globalEvents.single.localPosition, const Offset(5, 5));
|
||||
expect(globalEvents.single.localDelta, const Offset(2, 2));
|
||||
});
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import '../flutter_test_alternative.dart';
|
||||
|
||||
@ -67,4 +68,22 @@ void main() {
|
||||
expect(first.callbackRan, isTrue);
|
||||
expect(second.callbackRan, isFalse);
|
||||
});
|
||||
|
||||
test('works with transformed events', () {
|
||||
final PointerSignalResolver resolver = PointerSignalResolver();
|
||||
const PointerSignalEvent originalEvent = PointerScrollEvent();
|
||||
final PointerSignalEvent transformedEvent = originalEvent
|
||||
.transformed(Matrix4.translationValues(10.0, 20.0, 0.0));
|
||||
|
||||
expect(originalEvent, isNot(same(transformedEvent)));
|
||||
expect(transformedEvent.original, same(originalEvent));
|
||||
|
||||
final List<PointerSignalEvent> events = <PointerSignalEvent>[];
|
||||
resolver.register(transformedEvent, (PointerSignalEvent event) {
|
||||
events.add(event);
|
||||
});
|
||||
resolver.resolve(originalEvent);
|
||||
|
||||
expect(events.single, same(transformedEvent));
|
||||
});
|
||||
}
|
||||
|
@ -26,4 +26,28 @@ void main() {
|
||||
final TestGestureRecognizer recognizer = TestGestureRecognizer(debugOwner: 0);
|
||||
expect(recognizer, hasAGoodToStringDeep);
|
||||
});
|
||||
|
||||
test('OffsetPair', () {
|
||||
const OffsetPair offset1 = OffsetPair(
|
||||
local: Offset(10, 20),
|
||||
global: Offset(30, 40),
|
||||
);
|
||||
|
||||
expect(offset1.local, const Offset(10, 20));
|
||||
expect(offset1.global, const Offset(30, 40));
|
||||
|
||||
const OffsetPair offset2 = OffsetPair(
|
||||
local: Offset(50, 60),
|
||||
global: Offset(70, 80),
|
||||
);
|
||||
|
||||
final OffsetPair sum = offset2 + offset1;
|
||||
expect(sum.local, const Offset(60, 80));
|
||||
expect(sum.global, const Offset(100, 120));
|
||||
|
||||
final OffsetPair difference = offset2 - offset1;
|
||||
expect(difference.local, const Offset(40, 40));
|
||||
expect(difference.global, const Offset(40, 40));
|
||||
|
||||
});
|
||||
}
|
||||
|
98
packages/flutter/test/gestures/transformed_double_tap.dart
Normal file
98
packages/flutter/test/gestures/transformed_double_tap.dart
Normal file
@ -0,0 +1,98 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('kTouchSlop is evaluated in the global coordinate space when scaled up', (WidgetTester tester) async {
|
||||
int doubleTapCount = 0;
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Transform.scale(
|
||||
scale: 2.0,
|
||||
child: GestureDetector(
|
||||
onDoubleTap: () {
|
||||
doubleTapCount++;
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Move just below kTouchSlop should recognize tap.
|
||||
final Offset center = tester.getCenter(find.byKey(redContainer));
|
||||
TestGesture gesture = await tester.startGesture(center);
|
||||
await gesture.up();
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
gesture = await tester.startGesture(center + const Offset(kDoubleTapSlop - 1, 0));
|
||||
await gesture.up();
|
||||
|
||||
expect(doubleTapCount, 1);
|
||||
|
||||
doubleTapCount = 0;
|
||||
|
||||
gesture = await tester.startGesture(center);
|
||||
await gesture.up();
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
gesture = await tester.startGesture(center + const Offset(kDoubleTapSlop + 1, 0));
|
||||
await gesture.up();
|
||||
|
||||
expect(doubleTapCount, 0);
|
||||
});
|
||||
|
||||
testWidgets('kTouchSlop is evaluated in the global coordinate space when scaled down', (WidgetTester tester) async {
|
||||
int doubleTapCount = 0;
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Transform.scale(
|
||||
scale: 0.5,
|
||||
child: GestureDetector(
|
||||
onDoubleTap: () {
|
||||
doubleTapCount++;
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 500,
|
||||
height: 500,
|
||||
color: Colors.red,
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Move just below kTouchSlop should recognize tap.
|
||||
final Offset center = tester.getCenter(find.byKey(redContainer));
|
||||
TestGesture gesture = await tester.startGesture(center);
|
||||
await gesture.up();
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
gesture = await tester.startGesture(center + const Offset(kDoubleTapSlop - 1, 0));
|
||||
await gesture.up();
|
||||
|
||||
expect(doubleTapCount, 1);
|
||||
|
||||
doubleTapCount = 0;
|
||||
|
||||
gesture = await tester.startGesture(center);
|
||||
await gesture.up();
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
gesture = await tester.startGesture(center + const Offset(kDoubleTapSlop + 1, 0));
|
||||
await gesture.up();
|
||||
|
||||
expect(doubleTapCount, 0);
|
||||
});
|
||||
}
|
206
packages/flutter/test/gestures/transformed_long_press_test.dart
Normal file
206
packages/flutter/test/gestures/transformed_long_press_test.dart
Normal file
@ -0,0 +1,206 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('gets local corrdinates', (WidgetTester tester) async {
|
||||
int longPressCount = 0;
|
||||
int longPressUpCount = 0;
|
||||
final List<LongPressEndDetails> endDetails = <LongPressEndDetails>[];
|
||||
final List<LongPressMoveUpdateDetails> moveDetails = <LongPressMoveUpdateDetails>[];
|
||||
final List<LongPressStartDetails> startDetails = <LongPressStartDetails>[];
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onLongPress: () {
|
||||
longPressCount++;
|
||||
},
|
||||
onLongPressEnd: (LongPressEndDetails details) {
|
||||
endDetails.add(details);
|
||||
},
|
||||
onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) {
|
||||
moveDetails.add(details);
|
||||
},
|
||||
onLongPressStart: (LongPressStartDetails details) {
|
||||
startDetails.add(details);
|
||||
},
|
||||
onLongPressUp: () {
|
||||
longPressUpCount++;
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
await tester.longPressAt(tester.getCenter(find.byKey(redContainer)));
|
||||
expect(longPressCount, 1);
|
||||
expect(longPressUpCount, 1);
|
||||
expect(moveDetails, isEmpty);
|
||||
expect(startDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(startDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(endDetails.single.globalPosition, const Offset(400, 300));
|
||||
});
|
||||
|
||||
testWidgets('scaled up', (WidgetTester tester) async {
|
||||
int longPressCount = 0;
|
||||
int longPressUpCount = 0;
|
||||
final List<LongPressEndDetails> endDetails = <LongPressEndDetails>[];
|
||||
final List<LongPressMoveUpdateDetails> moveDetails = <LongPressMoveUpdateDetails>[];
|
||||
final List<LongPressStartDetails> startDetails = <LongPressStartDetails>[];
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Transform.scale(
|
||||
scale: 2.0,
|
||||
child: GestureDetector(
|
||||
onLongPress: () {
|
||||
longPressCount++;
|
||||
},
|
||||
onLongPressEnd: (LongPressEndDetails details) {
|
||||
endDetails.add(details);
|
||||
},
|
||||
onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) {
|
||||
moveDetails.add(details);
|
||||
},
|
||||
onLongPressStart: (LongPressStartDetails details) {
|
||||
startDetails.add(details);
|
||||
},
|
||||
onLongPressUp: () {
|
||||
longPressUpCount++;
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
|
||||
await gesture.moveBy(const Offset(0, 10.0));
|
||||
await tester.pump(kLongPressTimeout);
|
||||
await gesture.up();
|
||||
|
||||
expect(longPressCount, 1);
|
||||
expect(longPressUpCount, 1);
|
||||
expect(startDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(startDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails.single.localPosition, const Offset(50, 75 + 10.0 / 2.0));
|
||||
expect(endDetails.single.globalPosition, const Offset(400, 300.0 + 10.0));
|
||||
expect(moveDetails, isEmpty); // moved before long press was detected.
|
||||
|
||||
startDetails.clear();
|
||||
endDetails.clear();
|
||||
longPressCount = 0;
|
||||
longPressUpCount = 0;
|
||||
|
||||
// Move after recognized.
|
||||
gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
|
||||
await tester.pump(kLongPressTimeout);
|
||||
await gesture.moveBy(const Offset(0, 100));
|
||||
await gesture.up();
|
||||
|
||||
expect(longPressCount, 1);
|
||||
expect(longPressUpCount, 1);
|
||||
expect(startDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(startDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails.single.localPosition, const Offset(50, 75 + 100.0 / 2.0));
|
||||
expect(endDetails.single.globalPosition, const Offset(400, 300.0 + 100.0));
|
||||
expect(moveDetails.single.localPosition, const Offset(50, 75 + 100.0 / 2.0));
|
||||
expect(moveDetails.single.globalPosition, const Offset(400, 300.0 + 100.0));
|
||||
expect(moveDetails.single.offsetFromOrigin, const Offset(0, 100.0));
|
||||
expect(moveDetails.single.localOffsetFromOrigin, const Offset(0, 100.0 / 2.0));
|
||||
});
|
||||
|
||||
testWidgets('scaled down', (WidgetTester tester) async {
|
||||
int longPressCount = 0;
|
||||
int longPressUpCount = 0;
|
||||
final List<LongPressEndDetails> endDetails = <LongPressEndDetails>[];
|
||||
final List<LongPressMoveUpdateDetails> moveDetails = <LongPressMoveUpdateDetails>[];
|
||||
final List<LongPressStartDetails> startDetails = <LongPressStartDetails>[];
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Transform.scale(
|
||||
scale: 0.5,
|
||||
child: GestureDetector(
|
||||
onLongPress: () {
|
||||
longPressCount++;
|
||||
},
|
||||
onLongPressEnd: (LongPressEndDetails details) {
|
||||
endDetails.add(details);
|
||||
},
|
||||
onLongPressMoveUpdate: (LongPressMoveUpdateDetails details) {
|
||||
moveDetails.add(details);
|
||||
},
|
||||
onLongPressStart: (LongPressStartDetails details) {
|
||||
startDetails.add(details);
|
||||
},
|
||||
onLongPressUp: () {
|
||||
longPressUpCount++;
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
|
||||
await gesture.moveBy(const Offset(0, 10.0));
|
||||
await tester.pump(kLongPressTimeout);
|
||||
await gesture.up();
|
||||
|
||||
expect(longPressCount, 1);
|
||||
expect(longPressUpCount, 1);
|
||||
expect(startDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(startDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails.single.localPosition, const Offset(50, 75 + 10.0 * 2.0));
|
||||
expect(endDetails.single.globalPosition, const Offset(400, 300.0 + 10.0));
|
||||
expect(moveDetails, isEmpty); // moved before long press was detected.
|
||||
|
||||
startDetails.clear();
|
||||
endDetails.clear();
|
||||
longPressCount = 0;
|
||||
longPressUpCount = 0;
|
||||
|
||||
// Move after recognized.
|
||||
gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
|
||||
await tester.pump(kLongPressTimeout);
|
||||
await gesture.moveBy(const Offset(0, 100));
|
||||
await gesture.up();
|
||||
|
||||
expect(longPressCount, 1);
|
||||
expect(longPressUpCount, 1);
|
||||
expect(startDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(startDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails.single.localPosition, const Offset(50, 75 + 100.0 * 2.0));
|
||||
expect(endDetails.single.globalPosition, const Offset(400, 300.0 + 100.0));
|
||||
expect(moveDetails.single.localPosition, const Offset(50, 75 + 100.0 * 2.0));
|
||||
expect(moveDetails.single.globalPosition, const Offset(400, 300.0 + 100.0));
|
||||
expect(moveDetails.single.offsetFromOrigin, const Offset(0, 100.0));
|
||||
expect(moveDetails.single.localOffsetFromOrigin, const Offset(0, 100.0 * 2.0));
|
||||
});
|
||||
}
|
668
packages/flutter/test/gestures/transformed_monodrag_test.dart
Normal file
668
packages/flutter/test/gestures/transformed_monodrag_test.dart
Normal file
@ -0,0 +1,668 @@
|
||||
// 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:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
void main() {
|
||||
group('Horizontal', () {
|
||||
testWidgets('gets local corrdinates', (WidgetTester tester) async {
|
||||
int dragCancelCount = 0;
|
||||
final List<DragDownDetails> downDetails = <DragDownDetails>[];
|
||||
final List<DragEndDetails> endDetails = <DragEndDetails>[];
|
||||
final List<DragStartDetails> startDetails = <DragStartDetails>[];
|
||||
final List<DragUpdateDetails> updateDetails = <DragUpdateDetails>[];
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onHorizontalDragCancel: () {
|
||||
dragCancelCount++;
|
||||
},
|
||||
onHorizontalDragDown: (DragDownDetails details) {
|
||||
downDetails.add(details);
|
||||
},
|
||||
onHorizontalDragEnd: (DragEndDetails details) {
|
||||
endDetails.add(details);
|
||||
},
|
||||
onHorizontalDragStart: (DragStartDetails details) {
|
||||
startDetails.add(details);
|
||||
},
|
||||
onHorizontalDragUpdate: (DragUpdateDetails details) {
|
||||
updateDetails.add(details);
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.drag(find.byKey(redContainer), const Offset(100, 0));
|
||||
expect(dragCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails, hasLength(1));
|
||||
expect(startDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(startDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(updateDetails.last.localPosition, const Offset(50 + 100.0, 75));
|
||||
expect(updateDetails.last.globalPosition, const Offset(400 + 100.0, 300));
|
||||
expect(
|
||||
updateDetails.fold(Offset.zero, (Offset offset, DragUpdateDetails details) => offset + details.delta),
|
||||
const Offset(100, 0),
|
||||
);
|
||||
expect(
|
||||
updateDetails.fold(0.0, (double offset, DragUpdateDetails details) => offset + details.primaryDelta),
|
||||
100.0,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('kTouchSlop is evaluated in the global coordinate space when scaled up', (WidgetTester tester) async {
|
||||
int dragCancelCount = 0;
|
||||
final List<DragDownDetails> downDetails = <DragDownDetails>[];
|
||||
final List<DragEndDetails> endDetails = <DragEndDetails>[];
|
||||
final List<DragStartDetails> startDetails = <DragStartDetails>[];
|
||||
final List<DragUpdateDetails> updateDetails = <DragUpdateDetails>[];
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Transform.scale(
|
||||
scale: 2.0,
|
||||
child: GestureDetector(
|
||||
onHorizontalDragCancel: () {
|
||||
dragCancelCount++;
|
||||
},
|
||||
onHorizontalDragDown: (DragDownDetails details) {
|
||||
downDetails.add(details);
|
||||
},
|
||||
onHorizontalDragEnd: (DragEndDetails details) {
|
||||
endDetails.add(details);
|
||||
},
|
||||
onHorizontalDragStart: (DragStartDetails details) {
|
||||
startDetails.add(details);
|
||||
},
|
||||
onHorizontalDragUpdate: (DragUpdateDetails details) {
|
||||
updateDetails.add(details);
|
||||
},
|
||||
onTap: () {
|
||||
// Competing gesture detector.
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Move just above kTouchSlop should recognize drag.
|
||||
await tester.drag(find.byKey(redContainer), const Offset(kTouchSlop + 1, 0));
|
||||
|
||||
expect(dragCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails, hasLength(1));
|
||||
expect(startDetails.single.localPosition, const Offset(50 + (kTouchSlop + 1) / 2, 75));
|
||||
expect(startDetails.single.globalPosition, const Offset(400 + (kTouchSlop + 1), 300));
|
||||
expect(updateDetails, isEmpty);
|
||||
|
||||
dragCancelCount = 0;
|
||||
downDetails.clear();
|
||||
endDetails.clear();
|
||||
startDetails.clear();
|
||||
updateDetails.clear();
|
||||
|
||||
// Move just below kTouchSlop does not recognize drag.
|
||||
await tester.drag(find.byKey(redContainer), const Offset(kTouchSlop - 1, 0));
|
||||
expect(dragCancelCount, 1);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails, isEmpty);
|
||||
expect(startDetails, isEmpty);
|
||||
expect(updateDetails, isEmpty);
|
||||
|
||||
dragCancelCount = 0;
|
||||
downDetails.clear();
|
||||
endDetails.clear();
|
||||
startDetails.clear();
|
||||
updateDetails.clear();
|
||||
|
||||
// Move in two separate movements
|
||||
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
|
||||
await gesture.moveBy(const Offset(kTouchSlop + 1, 30));
|
||||
await gesture.moveBy(const Offset(100, 10));
|
||||
await gesture.up();
|
||||
|
||||
expect(dragCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails, hasLength(1));
|
||||
expect(startDetails.single.localPosition, const Offset(50 + (kTouchSlop + 1) / 2, 75.0 + 30.0 / 2));
|
||||
expect(startDetails.single.globalPosition, const Offset(400 + (kTouchSlop + 1), 300 + 30.0));
|
||||
expect(updateDetails.single.localPosition, startDetails.single.localPosition + const Offset(100.0 / 2, 10 / 2));
|
||||
expect(updateDetails.single.globalPosition, startDetails.single.globalPosition + const Offset(100.0, 10.0));
|
||||
expect(updateDetails.single.delta, const Offset(100.0 / 2, 0.0));
|
||||
expect(updateDetails.single.primaryDelta, 100.0 / 2);
|
||||
|
||||
dragCancelCount = 0;
|
||||
downDetails.clear();
|
||||
endDetails.clear();
|
||||
startDetails.clear();
|
||||
updateDetails.clear();
|
||||
});
|
||||
|
||||
testWidgets('kTouchSlop is evaluated in the global coordinate space when scaled down', (WidgetTester tester) async {
|
||||
int dragCancelCount = 0;
|
||||
final List<DragDownDetails> downDetails = <DragDownDetails>[];
|
||||
final List<DragEndDetails> endDetails = <DragEndDetails>[];
|
||||
final List<DragStartDetails> startDetails = <DragStartDetails>[];
|
||||
final List<DragUpdateDetails> updateDetails = <DragUpdateDetails>[];
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Transform.scale(
|
||||
scale: 0.5,
|
||||
child: GestureDetector(
|
||||
onHorizontalDragCancel: () {
|
||||
dragCancelCount++;
|
||||
},
|
||||
onHorizontalDragDown: (DragDownDetails details) {
|
||||
downDetails.add(details);
|
||||
},
|
||||
onHorizontalDragEnd: (DragEndDetails details) {
|
||||
endDetails.add(details);
|
||||
},
|
||||
onHorizontalDragStart: (DragStartDetails details) {
|
||||
startDetails.add(details);
|
||||
},
|
||||
onHorizontalDragUpdate: (DragUpdateDetails details) {
|
||||
updateDetails.add(details);
|
||||
},
|
||||
onTap: () {
|
||||
// Competing gesture detector.
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Move just above kTouchSlop should recognize drag.
|
||||
await tester.drag(find.byKey(redContainer), const Offset(kTouchSlop + 1, 0));
|
||||
|
||||
expect(dragCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails, hasLength(1));
|
||||
expect(startDetails.single.localPosition, const Offset(50 + (kTouchSlop + 1) * 2, 75));
|
||||
expect(startDetails.single.globalPosition, const Offset(400 + (kTouchSlop + 1), 300));
|
||||
expect(updateDetails, isEmpty);
|
||||
|
||||
dragCancelCount = 0;
|
||||
downDetails.clear();
|
||||
endDetails.clear();
|
||||
startDetails.clear();
|
||||
updateDetails.clear();
|
||||
|
||||
// Move just below kTouchSlop does not recognize drag.
|
||||
await tester.drag(find.byKey(redContainer), const Offset(kTouchSlop - 1, 0));
|
||||
expect(dragCancelCount, 1);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails, isEmpty);
|
||||
expect(startDetails, isEmpty);
|
||||
expect(updateDetails, isEmpty);
|
||||
|
||||
dragCancelCount = 0;
|
||||
downDetails.clear();
|
||||
endDetails.clear();
|
||||
startDetails.clear();
|
||||
updateDetails.clear();
|
||||
|
||||
// Move in two separate movements
|
||||
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
|
||||
await gesture.moveBy(const Offset(kTouchSlop + 1, 30));
|
||||
await gesture.moveBy(const Offset(100, 10));
|
||||
await gesture.up();
|
||||
|
||||
expect(dragCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails, hasLength(1));
|
||||
expect(startDetails.single.localPosition, const Offset(50 + (kTouchSlop + 1) * 2, 75.0 + 30.0 * 2));
|
||||
expect(startDetails.single.globalPosition, const Offset(400 + (kTouchSlop + 1), 300 + 30.0));
|
||||
expect(updateDetails.single.localPosition, startDetails.single.localPosition + const Offset(100.0 * 2, 10.0 * 2.0));
|
||||
expect(updateDetails.single.globalPosition, startDetails.single.globalPosition + const Offset(100.0, 10.0));
|
||||
expect(updateDetails.single.delta, const Offset(100.0 * 2.0, 0.0));
|
||||
expect(updateDetails.single.primaryDelta, 100.0 * 2);
|
||||
|
||||
dragCancelCount = 0;
|
||||
downDetails.clear();
|
||||
endDetails.clear();
|
||||
startDetails.clear();
|
||||
updateDetails.clear();
|
||||
});
|
||||
|
||||
testWidgets('kTouchSlop is evaluated in the global coordinate space when roateted 45 degrees', (WidgetTester tester) async {
|
||||
int dragCancelCount = 0;
|
||||
final List<DragDownDetails> downDetails = <DragDownDetails>[];
|
||||
final List<DragEndDetails> endDetails = <DragEndDetails>[];
|
||||
final List<DragStartDetails> startDetails = <DragStartDetails>[];
|
||||
final List<DragUpdateDetails> updateDetails = <DragUpdateDetails>[];
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Transform.rotate(
|
||||
angle: math.pi / 4,
|
||||
child: GestureDetector(
|
||||
onHorizontalDragCancel: () {
|
||||
dragCancelCount++;
|
||||
},
|
||||
onHorizontalDragDown: (DragDownDetails details) {
|
||||
downDetails.add(details);
|
||||
},
|
||||
onHorizontalDragEnd: (DragEndDetails details) {
|
||||
endDetails.add(details);
|
||||
},
|
||||
onHorizontalDragStart: (DragStartDetails details) {
|
||||
startDetails.add(details);
|
||||
},
|
||||
onHorizontalDragUpdate: (DragUpdateDetails details) {
|
||||
updateDetails.add(details);
|
||||
},
|
||||
onTap: () {
|
||||
// Competing gesture detector.
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Move just below kTouchSlop should not recognize drag.
|
||||
const Offset moveBy1 = Offset(kTouchSlop/ 2, kTouchSlop / 2);
|
||||
expect(moveBy1.distance, lessThan(kTouchSlop));
|
||||
await tester.drag(find.byKey(redContainer), moveBy1);
|
||||
expect(dragCancelCount, 1);
|
||||
expect(downDetails.single.localPosition, within(distance: 0.0001, from: const Offset(50, 75)));
|
||||
expect(downDetails.single.globalPosition, within(distance: 0.0001, from: const Offset(400, 300)));
|
||||
expect(endDetails, isEmpty);
|
||||
expect(startDetails, isEmpty);
|
||||
expect(updateDetails, isEmpty);
|
||||
|
||||
dragCancelCount = 0;
|
||||
downDetails.clear();
|
||||
endDetails.clear();
|
||||
startDetails.clear();
|
||||
updateDetails.clear();
|
||||
|
||||
// Move above kTouchSlop recognizes drag.
|
||||
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
|
||||
await gesture.moveBy(const Offset(kTouchSlop, kTouchSlop));
|
||||
await gesture.moveBy(const Offset(3, 4));
|
||||
await gesture.up();
|
||||
|
||||
expect(dragCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, within(distance: 0.0001, from: const Offset(50, 75)));
|
||||
expect(downDetails.single.globalPosition, within(distance: 0.0001, from: const Offset(400, 300)));
|
||||
expect(endDetails, hasLength(1));
|
||||
expect(startDetails, hasLength(1));
|
||||
expect(updateDetails.single.globalPosition, within(distance: 0.0001, from: const Offset(400 + kTouchSlop + 3, 300 + kTouchSlop + 4)));
|
||||
expect(updateDetails.single.delta, within(distance: 0.1, from: const Offset(5, 0.0))); // sqrt(3^2 + 4^2)
|
||||
expect(updateDetails.single.primaryDelta, within(distance: 0.1, from: 5.0)); // sqrt(3^2 + 4^2)
|
||||
});
|
||||
});
|
||||
|
||||
group('Vertical', () {
|
||||
testWidgets('gets local corrdinates', (WidgetTester tester) async {
|
||||
int dragCancelCount = 0;
|
||||
final List<DragDownDetails> downDetails = <DragDownDetails>[];
|
||||
final List<DragEndDetails> endDetails = <DragEndDetails>[];
|
||||
final List<DragStartDetails> startDetails = <DragStartDetails>[];
|
||||
final List<DragUpdateDetails> updateDetails = <DragUpdateDetails>[];
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onVerticalDragCancel: () {
|
||||
dragCancelCount++;
|
||||
},
|
||||
onVerticalDragDown: (DragDownDetails details) {
|
||||
downDetails.add(details);
|
||||
},
|
||||
onVerticalDragEnd: (DragEndDetails details) {
|
||||
endDetails.add(details);
|
||||
},
|
||||
onVerticalDragStart: (DragStartDetails details) {
|
||||
startDetails.add(details);
|
||||
},
|
||||
onVerticalDragUpdate: (DragUpdateDetails details) {
|
||||
updateDetails.add(details);
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.drag(find.byKey(redContainer), const Offset(0, 100));
|
||||
expect(dragCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails, hasLength(1));
|
||||
expect(startDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(startDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(updateDetails.last.localPosition, const Offset(50, 75 + 100.0));
|
||||
expect(updateDetails.last.globalPosition, const Offset(400, 300 + 100.0));
|
||||
expect(
|
||||
updateDetails.fold(Offset.zero, (Offset offset, DragUpdateDetails details) => offset + details.delta),
|
||||
const Offset(0, 100),
|
||||
);
|
||||
expect(
|
||||
updateDetails.fold(0.0, (double offset, DragUpdateDetails details) => offset + details.primaryDelta),
|
||||
100.0,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('kTouchSlop is evaluated in the global coordinate space when scaled up', (WidgetTester tester) async {
|
||||
int dragCancelCount = 0;
|
||||
final List<DragDownDetails> downDetails = <DragDownDetails>[];
|
||||
final List<DragEndDetails> endDetails = <DragEndDetails>[];
|
||||
final List<DragStartDetails> startDetails = <DragStartDetails>[];
|
||||
final List<DragUpdateDetails> updateDetails = <DragUpdateDetails>[];
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Transform.scale(
|
||||
scale: 2.0,
|
||||
child: GestureDetector(
|
||||
onVerticalDragCancel: () {
|
||||
dragCancelCount++;
|
||||
},
|
||||
onVerticalDragDown: (DragDownDetails details) {
|
||||
downDetails.add(details);
|
||||
},
|
||||
onVerticalDragEnd: (DragEndDetails details) {
|
||||
endDetails.add(details);
|
||||
},
|
||||
onVerticalDragStart: (DragStartDetails details) {
|
||||
startDetails.add(details);
|
||||
},
|
||||
onVerticalDragUpdate: (DragUpdateDetails details) {
|
||||
updateDetails.add(details);
|
||||
},
|
||||
onTap: () {
|
||||
// Competing gesture detector.
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Move just above kTouchSlop should recognize drag.
|
||||
await tester.drag(find.byKey(redContainer), const Offset(0, kTouchSlop + 1));
|
||||
|
||||
expect(dragCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails, hasLength(1));
|
||||
expect(startDetails.single.localPosition, const Offset(50, 75 + (kTouchSlop + 1) / 2));
|
||||
expect(startDetails.single.globalPosition, const Offset(400, 300 + (kTouchSlop + 1)));
|
||||
expect(updateDetails, isEmpty);
|
||||
|
||||
dragCancelCount = 0;
|
||||
downDetails.clear();
|
||||
endDetails.clear();
|
||||
startDetails.clear();
|
||||
updateDetails.clear();
|
||||
|
||||
// Move just below kTouchSlop does not recognize drag.
|
||||
await tester.drag(find.byKey(redContainer), const Offset(0, kTouchSlop - 1));
|
||||
expect(dragCancelCount, 1);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails, isEmpty);
|
||||
expect(startDetails, isEmpty);
|
||||
expect(updateDetails, isEmpty);
|
||||
|
||||
dragCancelCount = 0;
|
||||
downDetails.clear();
|
||||
endDetails.clear();
|
||||
startDetails.clear();
|
||||
updateDetails.clear();
|
||||
|
||||
// Move in two separate movements
|
||||
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
|
||||
await gesture.moveBy(const Offset(30, kTouchSlop + 1));
|
||||
await gesture.moveBy(const Offset(10, 100));
|
||||
await gesture.up();
|
||||
|
||||
expect(dragCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails, hasLength(1));
|
||||
expect(startDetails.single.localPosition, const Offset(50 + 30.0 / 2, 75.0 + (kTouchSlop + 1) / 2));
|
||||
expect(startDetails.single.globalPosition, const Offset(400 + 30.0, 300 + (kTouchSlop + 1)));
|
||||
expect(updateDetails.single.localPosition, startDetails.single.localPosition + const Offset(10.0 / 2, 100.0 / 2));
|
||||
expect(updateDetails.single.globalPosition, startDetails.single.globalPosition + const Offset(10.0, 100.0));
|
||||
expect(updateDetails.single.delta, const Offset(0.0, 100.0 / 2));
|
||||
expect(updateDetails.single.primaryDelta, 100.0 / 2);
|
||||
|
||||
dragCancelCount = 0;
|
||||
downDetails.clear();
|
||||
endDetails.clear();
|
||||
startDetails.clear();
|
||||
updateDetails.clear();
|
||||
});
|
||||
|
||||
testWidgets('kTouchSlop is evaluated in the global coordinate space when scaled down', (WidgetTester tester) async {
|
||||
int dragCancelCount = 0;
|
||||
final List<DragDownDetails> downDetails = <DragDownDetails>[];
|
||||
final List<DragEndDetails> endDetails = <DragEndDetails>[];
|
||||
final List<DragStartDetails> startDetails = <DragStartDetails>[];
|
||||
final List<DragUpdateDetails> updateDetails = <DragUpdateDetails>[];
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Transform.scale(
|
||||
scale: 0.5,
|
||||
child: GestureDetector(
|
||||
onVerticalDragCancel: () {
|
||||
dragCancelCount++;
|
||||
},
|
||||
onVerticalDragDown: (DragDownDetails details) {
|
||||
downDetails.add(details);
|
||||
},
|
||||
onVerticalDragEnd: (DragEndDetails details) {
|
||||
endDetails.add(details);
|
||||
},
|
||||
onVerticalDragStart: (DragStartDetails details) {
|
||||
startDetails.add(details);
|
||||
},
|
||||
onVerticalDragUpdate: (DragUpdateDetails details) {
|
||||
updateDetails.add(details);
|
||||
},
|
||||
onTap: () {
|
||||
// Competing gesture detector.
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Move just above kTouchSlop should recognize drag.
|
||||
await tester.drag(find.byKey(redContainer), const Offset(0, kTouchSlop + 1));
|
||||
|
||||
expect(dragCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails, hasLength(1));
|
||||
expect(startDetails.single.localPosition, const Offset(50, 75 + (kTouchSlop + 1) * 2));
|
||||
expect(startDetails.single.globalPosition, const Offset(400, 300 + (kTouchSlop + 1)));
|
||||
expect(updateDetails, isEmpty);
|
||||
|
||||
dragCancelCount = 0;
|
||||
downDetails.clear();
|
||||
endDetails.clear();
|
||||
startDetails.clear();
|
||||
updateDetails.clear();
|
||||
|
||||
// Move just below kTouchSlop does not recognize drag.
|
||||
await tester.drag(find.byKey(redContainer), const Offset(0, kTouchSlop - 1));
|
||||
expect(dragCancelCount, 1);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails, isEmpty);
|
||||
expect(startDetails, isEmpty);
|
||||
expect(updateDetails, isEmpty);
|
||||
|
||||
dragCancelCount = 0;
|
||||
downDetails.clear();
|
||||
endDetails.clear();
|
||||
startDetails.clear();
|
||||
updateDetails.clear();
|
||||
|
||||
// Move in two separate movements
|
||||
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
|
||||
await gesture.moveBy(const Offset(30, kTouchSlop + 1));
|
||||
await gesture.moveBy(const Offset(10, 100));
|
||||
await gesture.up();
|
||||
|
||||
expect(dragCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(endDetails, hasLength(1));
|
||||
expect(startDetails.single.localPosition, const Offset(50 + 30.0 * 2, 75.0 + (kTouchSlop + 1) * 2));
|
||||
expect(startDetails.single.globalPosition, const Offset(400 + 30.0, 300 + (kTouchSlop + 1)));
|
||||
expect(updateDetails.single.localPosition, startDetails.single.localPosition + const Offset(10.0 * 2, 100.0 * 2.0));
|
||||
expect(updateDetails.single.globalPosition, startDetails.single.globalPosition + const Offset(10.0, 100.0));
|
||||
expect(updateDetails.single.delta, const Offset(0.0, 100.0 * 2.0));
|
||||
expect(updateDetails.single.primaryDelta, 100.0 * 2);
|
||||
|
||||
dragCancelCount = 0;
|
||||
downDetails.clear();
|
||||
endDetails.clear();
|
||||
startDetails.clear();
|
||||
updateDetails.clear();
|
||||
});
|
||||
|
||||
testWidgets('kTouchSlop is evaluated in the global coordinate space when roateted 45 degrees', (WidgetTester tester) async {
|
||||
int dragCancelCount = 0;
|
||||
final List<DragDownDetails> downDetails = <DragDownDetails>[];
|
||||
final List<DragEndDetails> endDetails = <DragEndDetails>[];
|
||||
final List<DragStartDetails> startDetails = <DragStartDetails>[];
|
||||
final List<DragUpdateDetails> updateDetails = <DragUpdateDetails>[];
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Transform.rotate(
|
||||
angle: math.pi / 4,
|
||||
child: GestureDetector(
|
||||
onVerticalDragCancel: () {
|
||||
dragCancelCount++;
|
||||
},
|
||||
onVerticalDragDown: (DragDownDetails details) {
|
||||
downDetails.add(details);
|
||||
},
|
||||
onVerticalDragEnd: (DragEndDetails details) {
|
||||
endDetails.add(details);
|
||||
},
|
||||
onVerticalDragStart: (DragStartDetails details) {
|
||||
startDetails.add(details);
|
||||
},
|
||||
onVerticalDragUpdate: (DragUpdateDetails details) {
|
||||
updateDetails.add(details);
|
||||
},
|
||||
onTap: () {
|
||||
// Competing gesture detector.
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Move just below kTouchSlop should not recognize drag.
|
||||
const Offset moveBy1 = Offset(kTouchSlop/ 2, kTouchSlop / 2);
|
||||
expect(moveBy1.distance, lessThan(kTouchSlop));
|
||||
await tester.drag(find.byKey(redContainer), moveBy1);
|
||||
expect(dragCancelCount, 1);
|
||||
expect(downDetails.single.localPosition, within(distance: 0.0001, from: const Offset(50, 75)));
|
||||
expect(downDetails.single.globalPosition, within(distance: 0.0001, from: const Offset(400, 300)));
|
||||
expect(endDetails, isEmpty);
|
||||
expect(startDetails, isEmpty);
|
||||
expect(updateDetails, isEmpty);
|
||||
|
||||
dragCancelCount = 0;
|
||||
downDetails.clear();
|
||||
endDetails.clear();
|
||||
startDetails.clear();
|
||||
updateDetails.clear();
|
||||
|
||||
// Move above kTouchSlop recognizes drag.
|
||||
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
|
||||
await gesture.moveBy(const Offset(kTouchSlop, kTouchSlop));
|
||||
await gesture.moveBy(const Offset(-4, 3));
|
||||
await gesture.up();
|
||||
|
||||
expect(dragCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, within(distance: 0.0001, from: const Offset(50, 75)));
|
||||
expect(downDetails.single.globalPosition, within(distance: 0.0001, from: const Offset(400, 300)));
|
||||
expect(endDetails, hasLength(1));
|
||||
expect(startDetails, hasLength(1));
|
||||
expect(updateDetails.single.globalPosition, within(distance: 0.0001, from: const Offset(400 + kTouchSlop - 4, 300 + kTouchSlop + 3)));
|
||||
expect(updateDetails.single.delta, within(distance: 0.1, from: const Offset(0.0, 5.0))); // sqrt(3^2 + 4^2)
|
||||
expect(updateDetails.single.primaryDelta, within(distance: 0.1, from: 5.0)); // sqrt(3^2 + 4^2)
|
||||
});
|
||||
});
|
||||
}
|
177
packages/flutter/test/gestures/transformed_tap_test.dart
Normal file
177
packages/flutter/test/gestures/transformed_tap_test.dart
Normal file
@ -0,0 +1,177 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('gets local corrdinates', (WidgetTester tester) async {
|
||||
int tapCount = 0;
|
||||
int tapCancelCount = 0;
|
||||
final List<TapDownDetails> downDetails = <TapDownDetails>[];
|
||||
final List<TapUpDetails> upDetails = <TapUpDetails>[];
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
tapCount++;
|
||||
},
|
||||
onTapCancel: () {
|
||||
tapCancelCount++;
|
||||
},
|
||||
onTapDown: (TapDownDetails details) {
|
||||
downDetails.add(details);
|
||||
},
|
||||
onTapUp: (TapUpDetails details) {
|
||||
upDetails.add(details);
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
await tester.tapAt(tester.getCenter(find.byKey(redContainer)));
|
||||
expect(tapCount, 1);
|
||||
expect(tapCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(upDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(upDetails.single.globalPosition, const Offset(400, 300));
|
||||
});
|
||||
|
||||
testWidgets('kTouchSlop is evaluated in the global coordinate space when scaled up', (WidgetTester tester) async {
|
||||
int tapCount = 0;
|
||||
int tapCancelCount = 0;
|
||||
final List<TapDownDetails> downDetails = <TapDownDetails>[];
|
||||
final List<TapUpDetails> upDetails = <TapUpDetails>[];
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Transform.scale(
|
||||
scale: 2.0,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
tapCount++;
|
||||
},
|
||||
onTapCancel: () {
|
||||
tapCancelCount++;
|
||||
},
|
||||
onTapDown: (TapDownDetails details) {
|
||||
downDetails.add(details);
|
||||
},
|
||||
onTapUp: (TapUpDetails details) {
|
||||
upDetails.add(details);
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Move just below kTouchSlop should recognize tap.
|
||||
TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
|
||||
await gesture.moveBy(const Offset(0, kTouchSlop - 1));
|
||||
await gesture.up();
|
||||
|
||||
expect(tapCount, 1);
|
||||
expect(tapCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(upDetails.single.localPosition, const Offset(50, 75 + (kTouchSlop - 1) / 2.0));
|
||||
expect(upDetails.single.globalPosition, const Offset(400, 300 + (kTouchSlop - 1)));
|
||||
|
||||
downDetails.clear();
|
||||
upDetails.clear();
|
||||
tapCount = 0;
|
||||
tapCancelCount = 0;
|
||||
|
||||
// Move more then kTouchSlop should cancel.
|
||||
gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
|
||||
await gesture.moveBy(const Offset(0, kTouchSlop + 1));
|
||||
await gesture.up();
|
||||
expect(tapCount, 0);
|
||||
expect(tapCancelCount, 1);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(upDetails, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('kTouchSlop is evaluated in the global coordinate space when scaled down', (WidgetTester tester) async {
|
||||
int tapCount = 0;
|
||||
int tapCancelCount = 0;
|
||||
final List<TapDownDetails> downDetails = <TapDownDetails>[];
|
||||
final List<TapUpDetails> upDetails = <TapUpDetails>[];
|
||||
|
||||
final Key redContainer = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Transform.scale(
|
||||
scale: 0.5,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
tapCount++;
|
||||
},
|
||||
onTapCancel: () {
|
||||
tapCancelCount++;
|
||||
},
|
||||
onTapDown: (TapDownDetails details) {
|
||||
downDetails.add(details);
|
||||
},
|
||||
onTapUp: (TapUpDetails details) {
|
||||
upDetails.add(details);
|
||||
},
|
||||
child: Container(
|
||||
key: redContainer,
|
||||
width: 100,
|
||||
height: 150,
|
||||
color: Colors.red,
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Move just below kTouchSlop should recognize tap.
|
||||
TestGesture gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
|
||||
await gesture.moveBy(const Offset(0, kTouchSlop - 1));
|
||||
await gesture.up();
|
||||
|
||||
expect(tapCount, 1);
|
||||
expect(tapCancelCount, 0);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(upDetails.single.localPosition, const Offset(50, 75 + (kTouchSlop - 1) * 2.0));
|
||||
expect(upDetails.single.globalPosition, const Offset(400, 300 + (kTouchSlop - 1)));
|
||||
|
||||
downDetails.clear();
|
||||
upDetails.clear();
|
||||
tapCount = 0;
|
||||
tapCancelCount = 0;
|
||||
|
||||
// Move more then kTouchSlop should cancel.
|
||||
gesture = await tester.startGesture(tester.getCenter(find.byKey(redContainer)));
|
||||
await gesture.moveBy(const Offset(0, kTouchSlop + 1));
|
||||
await gesture.up();
|
||||
expect(tapCount, 0);
|
||||
expect(tapCancelCount, 1);
|
||||
expect(downDetails.single.localPosition, const Offset(50, 75));
|
||||
expect(downDetails.single.globalPosition, const Offset(400, 300));
|
||||
expect(upDetails, isEmpty);
|
||||
});
|
||||
}
|
@ -265,10 +265,13 @@ void main() {
|
||||
final HitTestEntry entry1 = HitTestEntry(_DummyHitTestTarget());
|
||||
final HitTestEntry entry2 = HitTestEntry(_DummyHitTestTarget());
|
||||
final HitTestEntry entry3 = HitTestEntry(_DummyHitTestTarget());
|
||||
final Matrix4 transform = Matrix4.translationValues(40.0, 150.0, 0.0);
|
||||
|
||||
final HitTestResult wrapped = HitTestResult();
|
||||
final HitTestResult wrapped = MyHitTestResult()
|
||||
..publicPushTransform(transform);
|
||||
wrapped.add(entry1);
|
||||
expect(wrapped.path, equals(<HitTestEntry>[entry1]));
|
||||
expect(entry1.transform, transform);
|
||||
|
||||
final BoxHitTestResult wrapping = BoxHitTestResult.wrap(wrapped);
|
||||
expect(wrapping.path, equals(<HitTestEntry>[entry1]));
|
||||
@ -277,10 +280,12 @@ void main() {
|
||||
wrapping.add(entry2);
|
||||
expect(wrapping.path, equals(<HitTestEntry>[entry1, entry2]));
|
||||
expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2]));
|
||||
expect(entry2.transform, transform);
|
||||
|
||||
wrapped.add(entry3);
|
||||
expect(wrapping.path, equals(<HitTestEntry>[entry1, entry2, entry3]));
|
||||
expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2, entry3]));
|
||||
expect(entry3.transform, transform);
|
||||
});
|
||||
|
||||
test('addWithPaintTransform', () {
|
||||
@ -517,3 +522,7 @@ class _DummyHitTestTarget implements HitTestTarget {
|
||||
// Nothing to do.
|
||||
}
|
||||
}
|
||||
|
||||
class MyHitTestResult extends HitTestResult {
|
||||
void publicPushTransform(Matrix4 transform) => pushTransform(transform);
|
||||
}
|
||||
|
@ -834,10 +834,13 @@ void main() {
|
||||
final HitTestEntry entry1 = HitTestEntry(_DummyHitTestTarget());
|
||||
final HitTestEntry entry2 = HitTestEntry(_DummyHitTestTarget());
|
||||
final HitTestEntry entry3 = HitTestEntry(_DummyHitTestTarget());
|
||||
final Matrix4 transform = Matrix4.translationValues(40.0, 150.0, 0.0);
|
||||
|
||||
final HitTestResult wrapped = HitTestResult();
|
||||
final HitTestResult wrapped = MyHitTestResult()
|
||||
..publicPushTransform(transform);
|
||||
wrapped.add(entry1);
|
||||
expect(wrapped.path, equals(<HitTestEntry>[entry1]));
|
||||
expect(entry1.transform, transform);
|
||||
|
||||
final SliverHitTestResult wrapping = SliverHitTestResult.wrap(wrapped);
|
||||
expect(wrapping.path, equals(<HitTestEntry>[entry1]));
|
||||
@ -846,10 +849,12 @@ void main() {
|
||||
wrapping.add(entry2);
|
||||
expect(wrapping.path, equals(<HitTestEntry>[entry1, entry2]));
|
||||
expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2]));
|
||||
expect(entry2.transform, transform);
|
||||
|
||||
wrapped.add(entry3);
|
||||
expect(wrapping.path, equals(<HitTestEntry>[entry1, entry2, entry3]));
|
||||
expect(wrapped.path, equals(<HitTestEntry>[entry1, entry2, entry3]));
|
||||
expect(entry3.transform, transform);
|
||||
});
|
||||
|
||||
test('addWithAxisOffset', () {
|
||||
@ -924,3 +929,7 @@ class _DummyHitTestTarget implements HitTestTarget {
|
||||
// Nothing to do.
|
||||
}
|
||||
}
|
||||
|
||||
class MyHitTestResult extends HitTestResult {
|
||||
void publicPushTransform(Matrix4 transform) => pushTransform(transform);
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -594,6 +596,7 @@ void main() {
|
||||
|
||||
await gesture.removePointer();
|
||||
});
|
||||
|
||||
testWidgets('Exit event when unplugging mouse should have a position', (WidgetTester tester) async {
|
||||
final List<PointerEnterEvent> enter = <PointerEnterEvent>[];
|
||||
final List<PointerHoverEvent> hover = <PointerHoverEvent>[];
|
||||
@ -640,4 +643,323 @@ void main() {
|
||||
expect(exit.single.delta, const Offset(0.0, 0.0));
|
||||
});
|
||||
});
|
||||
|
||||
group('transformed events', () {
|
||||
testWidgets('simple offset for touch/signal', (WidgetTester tester) async {
|
||||
final List<PointerEvent> events = <PointerEvent>[];
|
||||
final Key key = UniqueKey();
|
||||
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Listener(
|
||||
onPointerDown: (PointerDownEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
onPointerUp: (PointerUpEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
onPointerMove: (PointerMoveEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
onPointerSignal: (PointerSignalEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
child: Container(
|
||||
key: key,
|
||||
color: Colors.red,
|
||||
height: 100,
|
||||
width: 100,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
const Offset moved = Offset(20, 30);
|
||||
final Offset center = tester.getCenter(find.byKey(key));
|
||||
final Offset topLeft = tester.getTopLeft(find.byKey(key));
|
||||
final TestGesture gesture = await tester.startGesture(center);
|
||||
await gesture.moveBy(moved);
|
||||
await gesture.up();
|
||||
|
||||
expect(events, hasLength(3));
|
||||
final PointerDownEvent down = events[0];
|
||||
final PointerMoveEvent move = events[1];
|
||||
final PointerUpEvent up = events[2];
|
||||
|
||||
final Matrix4 expectedTransform = Matrix4.translationValues(-topLeft.dx, -topLeft.dy, 0);
|
||||
|
||||
expect(center, isNot(const Offset(50, 50)));
|
||||
|
||||
expect(down.localPosition, const Offset(50, 50));
|
||||
expect(down.position, center);
|
||||
expect(down.delta, Offset.zero);
|
||||
expect(down.localDelta, Offset.zero);
|
||||
expect(down.transform, expectedTransform);
|
||||
|
||||
expect(move.localPosition, const Offset(50, 50) + moved);
|
||||
expect(move.position, center + moved);
|
||||
expect(move.delta, moved);
|
||||
expect(move.localDelta, moved);
|
||||
expect(move.transform, expectedTransform);
|
||||
|
||||
expect(up.localPosition, const Offset(50, 50) + moved);
|
||||
expect(up.position, center + moved);
|
||||
expect(up.delta, Offset.zero);
|
||||
expect(up.localDelta, Offset.zero);
|
||||
expect(up.transform, expectedTransform);
|
||||
|
||||
events.clear();
|
||||
await scrollAt(center, tester);
|
||||
expect(events.single.localPosition, const Offset(50, 50));
|
||||
expect(events.single.position, center);
|
||||
expect(events.single.delta, Offset.zero);
|
||||
expect(events.single.localDelta, Offset.zero);
|
||||
expect(events.single.transform, expectedTransform);
|
||||
});
|
||||
|
||||
testWidgets('scaled for touch/signal', (WidgetTester tester) async {
|
||||
final List<PointerEvent> events = <PointerEvent>[];
|
||||
final Key key = UniqueKey();
|
||||
|
||||
const double scaleFactor = 2;
|
||||
|
||||
await tester.pumpWidget(
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Transform(
|
||||
transform: Matrix4.identity()..scale(scaleFactor),
|
||||
child: Listener(
|
||||
onPointerDown: (PointerDownEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
onPointerUp: (PointerUpEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
onPointerMove: (PointerMoveEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
onPointerSignal: (PointerSignalEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
child: Container(
|
||||
key: key,
|
||||
color: Colors.red,
|
||||
height: 100,
|
||||
width: 100,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
const Offset moved = Offset(20, 30);
|
||||
final Offset center = tester.getCenter(find.byKey(key));
|
||||
final TestGesture gesture = await tester.startGesture(center);
|
||||
await gesture.moveBy(moved);
|
||||
await gesture.up();
|
||||
|
||||
expect(events, hasLength(3));
|
||||
final PointerDownEvent down = events[0];
|
||||
final PointerMoveEvent move = events[1];
|
||||
final PointerUpEvent up = events[2];
|
||||
|
||||
final Matrix4 expectedTransform = Matrix4.identity()
|
||||
..scale(1 / scaleFactor, 1 / scaleFactor, 1.0);
|
||||
|
||||
expect(center, isNot(const Offset(50, 50)));
|
||||
|
||||
expect(down.localPosition, const Offset(50, 50));
|
||||
expect(down.position, center);
|
||||
expect(down.delta, Offset.zero);
|
||||
expect(down.localDelta, Offset.zero);
|
||||
expect(down.transform, expectedTransform);
|
||||
|
||||
expect(move.localPosition, const Offset(50, 50) + moved / scaleFactor);
|
||||
expect(move.position, center + moved);
|
||||
expect(move.delta, moved);
|
||||
expect(move.localDelta, moved / scaleFactor);
|
||||
expect(move.transform, expectedTransform);
|
||||
|
||||
expect(up.localPosition, const Offset(50, 50) + moved / scaleFactor);
|
||||
expect(up.position, center + moved);
|
||||
expect(up.delta, Offset.zero);
|
||||
expect(up.localDelta, Offset.zero);
|
||||
expect(up.transform, expectedTransform);
|
||||
|
||||
events.clear();
|
||||
await scrollAt(center, tester);
|
||||
expect(events.single.localPosition, const Offset(50, 50));
|
||||
expect(events.single.position, center);
|
||||
expect(events.single.delta, Offset.zero);
|
||||
expect(events.single.localDelta, Offset.zero);
|
||||
expect(events.single.transform, expectedTransform);
|
||||
|
||||
await gesture.removePointer();
|
||||
});
|
||||
|
||||
testWidgets('scaled and offset for touch/signal', (WidgetTester tester) async {
|
||||
final List<PointerEvent> events = <PointerEvent>[];
|
||||
final Key key = UniqueKey();
|
||||
|
||||
const double scaleFactor = 2;
|
||||
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Transform(
|
||||
transform: Matrix4.identity()..scale(scaleFactor),
|
||||
child: Listener(
|
||||
onPointerDown: (PointerDownEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
onPointerUp: (PointerUpEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
onPointerMove: (PointerMoveEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
onPointerSignal: (PointerSignalEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
child: Container(
|
||||
key: key,
|
||||
color: Colors.red,
|
||||
height: 100,
|
||||
width: 100,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
const Offset moved = Offset(20, 30);
|
||||
final Offset center = tester.getCenter(find.byKey(key));
|
||||
final Offset topLeft = tester.getTopLeft(find.byKey(key));
|
||||
final TestGesture gesture = await tester.startGesture(center);
|
||||
await gesture.moveBy(moved);
|
||||
await gesture.up();
|
||||
|
||||
expect(events, hasLength(3));
|
||||
final PointerDownEvent down = events[0];
|
||||
final PointerMoveEvent move = events[1];
|
||||
final PointerUpEvent up = events[2];
|
||||
|
||||
final Matrix4 expectedTransform = Matrix4.identity()
|
||||
..scale(1 / scaleFactor, 1 / scaleFactor, 1.0)
|
||||
..translate(-topLeft.dx, -topLeft.dy, 0);
|
||||
|
||||
expect(center, isNot(const Offset(50, 50)));
|
||||
|
||||
expect(down.localPosition, const Offset(50, 50));
|
||||
expect(down.position, center);
|
||||
expect(down.delta, Offset.zero);
|
||||
expect(down.localDelta, Offset.zero);
|
||||
expect(down.transform, expectedTransform);
|
||||
|
||||
expect(move.localPosition, const Offset(50, 50) + moved / scaleFactor);
|
||||
expect(move.position, center + moved);
|
||||
expect(move.delta, moved);
|
||||
expect(move.localDelta, moved / scaleFactor);
|
||||
expect(move.transform, expectedTransform);
|
||||
|
||||
expect(up.localPosition, const Offset(50, 50) + moved / scaleFactor);
|
||||
expect(up.position, center + moved);
|
||||
expect(up.delta, Offset.zero);
|
||||
expect(up.localDelta, Offset.zero);
|
||||
expect(up.transform, expectedTransform);
|
||||
|
||||
events.clear();
|
||||
await scrollAt(center, tester);
|
||||
expect(events.single.localPosition, const Offset(50, 50));
|
||||
expect(events.single.position, center);
|
||||
expect(events.single.delta, Offset.zero);
|
||||
expect(events.single.localDelta, Offset.zero);
|
||||
expect(events.single.transform, expectedTransform);
|
||||
|
||||
await gesture.removePointer();
|
||||
});
|
||||
|
||||
testWidgets('rotated for touch/signal', (WidgetTester tester) async {
|
||||
final List<PointerEvent> events = <PointerEvent>[];
|
||||
final Key key = UniqueKey();
|
||||
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: Transform(
|
||||
transform: Matrix4.identity()
|
||||
..rotateZ(math.pi / 2), // 90 degrees clockwise around Container origin
|
||||
child: Listener(
|
||||
onPointerDown: (PointerDownEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
onPointerUp: (PointerUpEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
onPointerMove: (PointerMoveEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
onPointerSignal: (PointerSignalEvent event) {
|
||||
events.add(event);
|
||||
},
|
||||
child: Container(
|
||||
key: key,
|
||||
color: Colors.red,
|
||||
height: 100,
|
||||
width: 100,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
const Offset moved = Offset(20, 30);
|
||||
final Offset downPosition = tester.getCenter(find.byKey(key)) + const Offset(10, 5);
|
||||
final TestGesture gesture = await tester.startGesture(downPosition);
|
||||
await gesture.moveBy(moved);
|
||||
await gesture.up();
|
||||
|
||||
expect(events, hasLength(3));
|
||||
final PointerDownEvent down = events[0];
|
||||
final PointerMoveEvent move = events[1];
|
||||
final PointerUpEvent up = events[2];
|
||||
|
||||
const Offset offset = Offset((800 - 100) / 2, (600 - 100) / 2);
|
||||
final Matrix4 expectedTransform = Matrix4.identity()
|
||||
..rotateZ(-math.pi / 2)
|
||||
..translate(-offset.dx, -offset.dy, 0.0);
|
||||
|
||||
final Offset localDownPosition = const Offset(50, 50) + const Offset(5, -10);
|
||||
expect(down.localPosition, within(distance: 0.001, from: localDownPosition));
|
||||
expect(down.position, downPosition);
|
||||
expect(down.delta, Offset.zero);
|
||||
expect(down.localDelta, Offset.zero);
|
||||
expect(down.transform, expectedTransform);
|
||||
|
||||
const Offset localDelta = Offset(30, -20);
|
||||
expect(move.localPosition, within(distance: 0.001, from: localDownPosition + localDelta));
|
||||
expect(move.position, downPosition + moved);
|
||||
expect(move.delta, moved);
|
||||
expect(move.localDelta, localDelta);
|
||||
expect(move.transform, expectedTransform);
|
||||
|
||||
expect(up.localPosition, within(distance: 0.001, from: localDownPosition + localDelta));
|
||||
expect(up.position, downPosition + moved);
|
||||
expect(up.delta, Offset.zero);
|
||||
expect(up.localDelta, Offset.zero);
|
||||
expect(up.transform, expectedTransform);
|
||||
|
||||
events.clear();
|
||||
await scrollAt(downPosition, tester);
|
||||
expect(events.single.localPosition, within(distance: 0.001, from: localDownPosition));
|
||||
expect(events.single.position, downPosition);
|
||||
expect(events.single.delta, Offset.zero);
|
||||
expect(events.single.localDelta, Offset.zero);
|
||||
expect(events.single.transform, expectedTransform);
|
||||
|
||||
await gesture.removePointer();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> scrollAt(Offset position, WidgetTester tester) {
|
||||
final TestPointer testPointer = TestPointer(1, PointerDeviceKind.mouse);
|
||||
// Create a hover event so that |testPointer| has a location when generating the scroll.
|
||||
testPointer.hover(position);
|
||||
final HitTestResult result = tester.hitTestOnBinding(position);
|
||||
return tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 20.0)), result);
|
||||
}
|
||||
|
218
packages/flutter/test/widgets/transformed_scrollable_test.dart
Normal file
218
packages/flutter/test/widgets/transformed_scrollable_test.dart
Normal file
@ -0,0 +1,218 @@
|
||||
// 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:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Scrollable scaled up', (WidgetTester tester) async {
|
||||
final ScrollController controller = ScrollController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Transform.scale(
|
||||
scale: 2.0,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 200,
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
cacheExtent: 0.0,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Container(
|
||||
height: 100.0,
|
||||
color: index % 2 == 0 ? Colors.blue : Colors.red,
|
||||
child: Text('Tile $index'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(controller.offset, 0.0);
|
||||
|
||||
await tester.drag(find.byType(ListView), const Offset(0.0, -100.0));
|
||||
await tester.pump();
|
||||
expect(controller.offset, 50.0); // 100.0 / 2.0
|
||||
|
||||
await tester.drag(find.byType(ListView), const Offset(80.0, -70.0));
|
||||
await tester.pump();
|
||||
expect(controller.offset, 85.0); // 50.0 + (70.0 / 2)
|
||||
|
||||
await tester.drag(find.byType(ListView), const Offset(100.0, 0.0));
|
||||
await tester.pump();
|
||||
expect(controller.offset, 85.0);
|
||||
|
||||
await tester.drag(find.byType(ListView), const Offset(0.0, 85.0));
|
||||
await tester.pump();
|
||||
expect(controller.offset, 42.5); // 85.0 - (85.0 / 2)
|
||||
});
|
||||
|
||||
testWidgets('Scrollable scaled down', (WidgetTester tester) async {
|
||||
final ScrollController controller = ScrollController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Transform.scale(
|
||||
scale: 0.5,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 200,
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
cacheExtent: 0.0,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Container(
|
||||
height: 100.0,
|
||||
color: index % 2 == 0 ? Colors.blue : Colors.red,
|
||||
child: Text('Tile $index'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(controller.offset, 0.0);
|
||||
|
||||
await tester.drag(find.byType(ListView), const Offset(0.0, -100.0));
|
||||
await tester.pump();
|
||||
expect(controller.offset, 200.0); // 100.0 * 2.0
|
||||
|
||||
await tester.drag(find.byType(ListView), const Offset(80.0, -70.0));
|
||||
await tester.pump();
|
||||
expect(controller.offset, 340.0); // 200.0 + (70.0 * 2)
|
||||
|
||||
await tester.drag(find.byType(ListView), const Offset(100.0, 0.0));
|
||||
await tester.pump();
|
||||
expect(controller.offset, 340.0);
|
||||
|
||||
await tester.drag(find.byType(ListView), const Offset(0.0, 170.0));
|
||||
await tester.pump();
|
||||
expect(controller.offset, 0.0); // 340.0 - (170.0 * 2)
|
||||
});
|
||||
|
||||
testWidgets('Scrollable rotated 90 degrees', (WidgetTester tester) async {
|
||||
final ScrollController controller = ScrollController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Transform.rotate(
|
||||
angle: math.pi / 2,
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 200,
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
cacheExtent: 0.0,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Container(
|
||||
height: 100.0,
|
||||
color: index % 2 == 0 ? Colors.blue : Colors.red,
|
||||
child: Text('Tile $index'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(controller.offset, 0.0);
|
||||
|
||||
await tester.drag(find.byType(ListView), const Offset(100.0, 0.0));
|
||||
await tester.pump();
|
||||
expect(controller.offset, 100.0);
|
||||
|
||||
await tester.drag(find.byType(ListView), const Offset(0.0, -100.0));
|
||||
await tester.pump();
|
||||
expect(controller.offset, 100.0);
|
||||
|
||||
await tester.drag(find.byType(ListView), const Offset(-70.0, -50.0));
|
||||
await tester.pump();
|
||||
expect(controller.offset, 30.0); // 100.0 - 70.0
|
||||
});
|
||||
|
||||
testWidgets('Perspective transform on scrollable', (WidgetTester tester) async {
|
||||
final ScrollController controller = ScrollController();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Transform(
|
||||
transform: Matrix4.identity()
|
||||
..setEntry(3, 2, 0.001)
|
||||
..rotateX(math.pi / 4),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 200,
|
||||
child: ListView.builder(
|
||||
controller: controller,
|
||||
cacheExtent: 0.0,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Container(
|
||||
height: 100.0,
|
||||
color: index % 2 == 0 ? Colors.blue : Colors.red,
|
||||
child: Text('Tile $index'),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(controller.offset, 0.0);
|
||||
|
||||
// We want to test that the point in the ListView that the finger touches
|
||||
// on the screen stays under the finger as the finger scrolls the ListView
|
||||
// in vertical direction. For this, we pick a point in the ListView (here
|
||||
// the center of Tile 5) and calculate its position in the coordinate space
|
||||
// of the screen. We then place our finger on that point and drag that
|
||||
// point up in vertical direction. After the scroll activity is done,
|
||||
// we verify that - in the coordinate space of the screen (!) - the point
|
||||
// has moved the same distance as the finger. Due to the perspective
|
||||
// transform the point will have moved more distance in the *local*
|
||||
// coordinate system of the ListView.
|
||||
|
||||
// Calculate where the center of Tile 5 is located in the coordinate
|
||||
// space of the screen. We cannot use `tester.getCenter` because it
|
||||
// does not properly remove the perspective component from the transform
|
||||
// to give us the place on the screen at which we need to touch the screen
|
||||
// to have the center of Tile 5 directly under our finger.
|
||||
final RenderBox tile5 = tester.renderObject(find.text('Tile 5'));
|
||||
final Offset pointOnScreenStart = MatrixUtils.transformPoint(
|
||||
PointerEvent.removePerspectiveTransform(tile5.getTransformTo(null)),
|
||||
tile5.size.center(Offset.zero),
|
||||
);
|
||||
|
||||
// Place the finger on the tracked point and move the finger upwards for
|
||||
// 50 pixels to scroll the ListView (the ListView's scroll offset will
|
||||
// move more then 50 pixels due to the perspective transform).
|
||||
await tester.dragFrom(pointOnScreenStart, const Offset(0.0, -50.0));
|
||||
await tester.pump();
|
||||
|
||||
// Get the new position of the tracked point in the screen's coordinate
|
||||
// system.
|
||||
final Offset pointOnScreenEnd = MatrixUtils.transformPoint(
|
||||
PointerEvent.removePerspectiveTransform(tile5.getTransformTo(null)),
|
||||
tile5.size.center(Offset.zero),
|
||||
);
|
||||
|
||||
// The tracked point (in the coordinate space of the screen) and the finger
|
||||
// should have moved the same vertical distance over the screen.
|
||||
expect(
|
||||
pointOnScreenStart.dy - pointOnScreenEnd.dy,
|
||||
within(distance: 0.00001, from: 50.0),
|
||||
);
|
||||
|
||||
// While the point traveled the same distance as the finger in the
|
||||
// coordinate space of the screen, the scroll view actually moved far more
|
||||
// pixels in its local coordinate system due to the perspective transform.
|
||||
expect(controller.offset, greaterThan(100));
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user