Support trackpad gestures in framework (#89944)
* Implement trackpad gestures in framework * Touch and Pan/Zoom pointers have separate IDs now * Handle trackpad pointer device type * Respect supportedDevices for pan/zoom events * Update after rebase * Fix check failures * Avoid error with very short drags * Address feedback * Refactor drag event handler * Address more feedback * Add some missing punctuation
This commit is contained in:
parent
38f360f91a
commit
30a501801a
@ -346,11 +346,11 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
|
|||||||
|
|
||||||
void _handlePointerEventImmediately(PointerEvent event) {
|
void _handlePointerEventImmediately(PointerEvent event) {
|
||||||
HitTestResult? hitTestResult;
|
HitTestResult? hitTestResult;
|
||||||
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
|
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent || event is PointerPanZoomStartEvent) {
|
||||||
assert(!_hitTests.containsKey(event.pointer));
|
assert(!_hitTests.containsKey(event.pointer));
|
||||||
hitTestResult = HitTestResult();
|
hitTestResult = HitTestResult();
|
||||||
hitTest(hitTestResult, event.position);
|
hitTest(hitTestResult, event.position);
|
||||||
if (event is PointerDownEvent) {
|
if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
|
||||||
_hitTests[event.pointer] = hitTestResult;
|
_hitTests[event.pointer] = hitTestResult;
|
||||||
}
|
}
|
||||||
assert(() {
|
assert(() {
|
||||||
@ -358,9 +358,9 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
|
|||||||
debugPrint('$event: $hitTestResult');
|
debugPrint('$event: $hitTestResult');
|
||||||
return true;
|
return true;
|
||||||
}());
|
}());
|
||||||
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
|
} else if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
|
||||||
hitTestResult = _hitTests.remove(event.pointer);
|
hitTestResult = _hitTests.remove(event.pointer);
|
||||||
} else if (event.down) {
|
} else if (event.down || event is PointerPanZoomUpdateEvent) {
|
||||||
// Because events that occur with the pointer down (like
|
// Because events that occur with the pointer down (like
|
||||||
// [PointerMoveEvent]s) should be dispatched to the same place that their
|
// [PointerMoveEvent]s) should be dispatched to the same place that their
|
||||||
// initial PointerDownEvent was, we want to re-use the path we found when
|
// initial PointerDownEvent was, we want to re-use the path we found when
|
||||||
@ -443,9 +443,9 @@ mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, H
|
|||||||
@override // from HitTestTarget
|
@override // from HitTestTarget
|
||||||
void handleEvent(PointerEvent event, HitTestEntry entry) {
|
void handleEvent(PointerEvent event, HitTestEntry entry) {
|
||||||
pointerRouter.route(event);
|
pointerRouter.route(event);
|
||||||
if (event is PointerDownEvent) {
|
if (event is PointerDownEvent || event is PointerPanZoomStartEvent) {
|
||||||
gestureArena.close(event.pointer);
|
gestureArena.close(event.pointer);
|
||||||
} else if (event is PointerUpEvent) {
|
} else if (event is PointerUpEvent || event is PointerPanZoomEndEvent) {
|
||||||
gestureArena.sweep(event.pointer);
|
gestureArena.sweep(event.pointer);
|
||||||
} else if (event is PointerSignalEvent) {
|
} else if (event is PointerSignalEvent) {
|
||||||
pointerSignalResolver.resolve(event);
|
pointerSignalResolver.resolve(event);
|
||||||
|
@ -15,14 +15,13 @@ import 'events.dart';
|
|||||||
int _synthesiseDownButtons(int buttons, PointerDeviceKind kind) {
|
int _synthesiseDownButtons(int buttons, PointerDeviceKind kind) {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case PointerDeviceKind.mouse:
|
case PointerDeviceKind.mouse:
|
||||||
|
case PointerDeviceKind.trackpad:
|
||||||
return buttons;
|
return buttons;
|
||||||
case PointerDeviceKind.touch:
|
case PointerDeviceKind.touch:
|
||||||
case PointerDeviceKind.stylus:
|
case PointerDeviceKind.stylus:
|
||||||
case PointerDeviceKind.invertedStylus:
|
case PointerDeviceKind.invertedStylus:
|
||||||
return buttons == 0 ? kPrimaryButton : buttons;
|
return buttons == 0 ? kPrimaryButton : buttons;
|
||||||
case PointerDeviceKind.unknown:
|
case PointerDeviceKind.unknown:
|
||||||
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
|
|
||||||
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
|
|
||||||
// We have no information about the device but we know we never want
|
// We have no information about the device but we know we never want
|
||||||
// buttons to be 0 when the pointer is down.
|
// buttons to be 0 when the pointer is down.
|
||||||
return buttons == 0 ? kPrimaryButton : buttons;
|
return buttons == 0 ? kPrimaryButton : buttons;
|
||||||
@ -209,9 +208,44 @@ class PointerEventConverter {
|
|||||||
radiusMax: radiusMax,
|
radiusMax: radiusMax,
|
||||||
embedderId: datum.embedderId,
|
embedderId: datum.embedderId,
|
||||||
);
|
);
|
||||||
default: // ignore: no_default_cases, to allow adding new pointer events to [ui.PointerChange]
|
case ui.PointerChange.panZoomStart:
|
||||||
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
|
return PointerPanZoomStartEvent(
|
||||||
throw StateError('Unreachable');
|
timeStamp: timeStamp,
|
||||||
|
pointer: datum.pointerIdentifier,
|
||||||
|
kind: kind,
|
||||||
|
device: datum.device,
|
||||||
|
position: position,
|
||||||
|
embedderId: datum.embedderId,
|
||||||
|
synthesized: datum.synthesized,
|
||||||
|
);
|
||||||
|
case ui.PointerChange.panZoomUpdate:
|
||||||
|
final Offset pan =
|
||||||
|
Offset(datum.panX, datum.panY) / devicePixelRatio;
|
||||||
|
final Offset panDelta =
|
||||||
|
Offset(datum.panDeltaX, datum.panDeltaY) / devicePixelRatio;
|
||||||
|
return PointerPanZoomUpdateEvent(
|
||||||
|
timeStamp: timeStamp,
|
||||||
|
pointer: datum.pointerIdentifier,
|
||||||
|
kind: kind,
|
||||||
|
device: datum.device,
|
||||||
|
position: position,
|
||||||
|
pan: pan,
|
||||||
|
panDelta: panDelta,
|
||||||
|
scale: datum.scale,
|
||||||
|
rotation: datum.rotation,
|
||||||
|
embedderId: datum.embedderId,
|
||||||
|
synthesized: datum.synthesized,
|
||||||
|
);
|
||||||
|
case ui.PointerChange.panZoomEnd:
|
||||||
|
return PointerPanZoomEndEvent(
|
||||||
|
timeStamp: timeStamp,
|
||||||
|
pointer: datum.pointerIdentifier,
|
||||||
|
kind: kind,
|
||||||
|
device: datum.device,
|
||||||
|
position: position,
|
||||||
|
embedderId: datum.embedderId,
|
||||||
|
synthesized: datum.synthesized,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
case ui.PointerSignalKind.scroll:
|
case ui.PointerSignalKind.scroll:
|
||||||
final Offset scrollDelta =
|
final Offset scrollDelta =
|
||||||
|
@ -1986,6 +1986,354 @@ class _TransformedPointerScrollEvent extends _TransformedPointerEvent with _Copy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mixin _CopyPointerPanZoomStartEvent on PointerEvent {
|
||||||
|
@override
|
||||||
|
PointerPanZoomStartEvent copyWith({
|
||||||
|
Duration? timeStamp,
|
||||||
|
int? pointer,
|
||||||
|
PointerDeviceKind? kind,
|
||||||
|
int? device,
|
||||||
|
Offset? position,
|
||||||
|
Offset? delta,
|
||||||
|
int? buttons,
|
||||||
|
bool? obscured,
|
||||||
|
double? pressure,
|
||||||
|
double? pressureMin,
|
||||||
|
double? pressureMax,
|
||||||
|
double? distance,
|
||||||
|
double? distanceMax,
|
||||||
|
double? size,
|
||||||
|
double? radiusMajor,
|
||||||
|
double? radiusMinor,
|
||||||
|
double? radiusMin,
|
||||||
|
double? radiusMax,
|
||||||
|
double? orientation,
|
||||||
|
double? tilt,
|
||||||
|
bool? synthesized,
|
||||||
|
int? embedderId,
|
||||||
|
}) {
|
||||||
|
return PointerPanZoomStartEvent(
|
||||||
|
timeStamp: timeStamp ?? this.timeStamp,
|
||||||
|
kind: kind ?? this.kind,
|
||||||
|
device: device ?? this.device,
|
||||||
|
position: position ?? this.position,
|
||||||
|
embedderId: embedderId ?? this.embedderId,
|
||||||
|
).transformed(transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A pan/zoom has begun on this pointer.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Listener.onPointerPanZoomStart], which allows callers to be notified of these
|
||||||
|
/// events in a widget tree.
|
||||||
|
class PointerPanZoomStartEvent extends PointerEvent with _PointerEventDescription, _CopyPointerPanZoomStartEvent {
|
||||||
|
/// Creates a pointer pan/zoom start event.
|
||||||
|
///
|
||||||
|
/// All of the arguments must be non-null.
|
||||||
|
const PointerPanZoomStartEvent({
|
||||||
|
Duration timeStamp = Duration.zero,
|
||||||
|
PointerDeviceKind kind = PointerDeviceKind.mouse,
|
||||||
|
int device = 0,
|
||||||
|
int pointer = 0,
|
||||||
|
Offset position = Offset.zero,
|
||||||
|
int embedderId = 0,
|
||||||
|
bool synthesized = false,
|
||||||
|
}) : assert(timeStamp != null),
|
||||||
|
assert(kind != null),
|
||||||
|
assert(device != null),
|
||||||
|
assert(pointer != null),
|
||||||
|
assert(position != null),
|
||||||
|
assert(embedderId != null),
|
||||||
|
assert(synthesized != null),
|
||||||
|
super(
|
||||||
|
timeStamp: timeStamp,
|
||||||
|
kind: kind,
|
||||||
|
device: device,
|
||||||
|
pointer: pointer,
|
||||||
|
position: position,
|
||||||
|
embedderId: embedderId,
|
||||||
|
synthesized: synthesized,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
PointerPanZoomStartEvent transformed(Matrix4? transform) {
|
||||||
|
if (transform == null || transform == this.transform) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return _TransformedPointerPanZoomStartEvent(original as PointerPanZoomStartEvent? ?? this, transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TransformedPointerPanZoomStartEvent extends _TransformedPointerEvent with _CopyPointerPanZoomStartEvent implements PointerPanZoomStartEvent {
|
||||||
|
_TransformedPointerPanZoomStartEvent(this.original, this.transform)
|
||||||
|
: assert(original != null), assert(transform != null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final PointerPanZoomStartEvent original;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Matrix4 transform;
|
||||||
|
|
||||||
|
@override
|
||||||
|
PointerPanZoomStartEvent transformed(Matrix4? transform) => original.transformed(transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin _CopyPointerPanZoomUpdateEvent on PointerEvent {
|
||||||
|
/// The total pan offset of the pan/zoom.
|
||||||
|
Offset get pan;
|
||||||
|
/// The total pan offset of the pan/zoom, transformed into local coordinates.
|
||||||
|
Offset get localPan;
|
||||||
|
/// The amount the pan offset changed since the last event.
|
||||||
|
Offset get panDelta;
|
||||||
|
/// The amount the pan offset changed since the last event, transformed into local coordinates.
|
||||||
|
Offset get localPanDelta;
|
||||||
|
/// The scale (zoom factor) of the pan/zoom.
|
||||||
|
double get scale;
|
||||||
|
/// The amount the pan/zoom has rotated in radians so far.
|
||||||
|
double get rotation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
PointerPanZoomUpdateEvent copyWith({
|
||||||
|
Duration? timeStamp,
|
||||||
|
int? pointer,
|
||||||
|
PointerDeviceKind? kind,
|
||||||
|
int? device,
|
||||||
|
Offset? position,
|
||||||
|
Offset? delta,
|
||||||
|
int? buttons,
|
||||||
|
bool? obscured,
|
||||||
|
double? pressure,
|
||||||
|
double? pressureMin,
|
||||||
|
double? pressureMax,
|
||||||
|
double? distance,
|
||||||
|
double? distanceMax,
|
||||||
|
double? size,
|
||||||
|
double? radiusMajor,
|
||||||
|
double? radiusMinor,
|
||||||
|
double? radiusMin,
|
||||||
|
double? radiusMax,
|
||||||
|
double? orientation,
|
||||||
|
double? tilt,
|
||||||
|
bool? synthesized,
|
||||||
|
int? embedderId,
|
||||||
|
Offset? pan,
|
||||||
|
Offset? localPan,
|
||||||
|
Offset? panDelta,
|
||||||
|
Offset? localPanDelta,
|
||||||
|
double? scale,
|
||||||
|
double? rotation,
|
||||||
|
}) {
|
||||||
|
return PointerPanZoomUpdateEvent(
|
||||||
|
timeStamp: timeStamp ?? this.timeStamp,
|
||||||
|
kind: kind ?? this.kind,
|
||||||
|
device: device ?? this.device,
|
||||||
|
position: position ?? this.position,
|
||||||
|
embedderId: embedderId ?? this.embedderId,
|
||||||
|
pan: pan ?? this.pan,
|
||||||
|
panDelta: panDelta ?? this.panDelta,
|
||||||
|
scale: scale ?? this.scale,
|
||||||
|
rotation: rotation ?? this.rotation,
|
||||||
|
).transformed(transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The active pan/zoom on this pointer has updated.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Listener.onPointerPanZoomUpdate], which allows callers to be notified of these
|
||||||
|
/// events in a widget tree.
|
||||||
|
class PointerPanZoomUpdateEvent extends PointerEvent with _PointerEventDescription, _CopyPointerPanZoomUpdateEvent {
|
||||||
|
/// Creates a pointer pan/zoom update event.
|
||||||
|
///
|
||||||
|
/// All of the arguments must be non-null.
|
||||||
|
const PointerPanZoomUpdateEvent({
|
||||||
|
Duration timeStamp = Duration.zero,
|
||||||
|
PointerDeviceKind kind = PointerDeviceKind.mouse,
|
||||||
|
int device = 0,
|
||||||
|
int pointer = 0,
|
||||||
|
Offset position = Offset.zero,
|
||||||
|
int embedderId = 0,
|
||||||
|
this.pan = Offset.zero,
|
||||||
|
this.panDelta = Offset.zero,
|
||||||
|
this.scale = 1.0,
|
||||||
|
this.rotation = 0.0,
|
||||||
|
bool synthesized = false,
|
||||||
|
}) : assert(timeStamp != null),
|
||||||
|
assert(kind != null),
|
||||||
|
assert(device != null),
|
||||||
|
assert(pointer != null),
|
||||||
|
assert(position != null),
|
||||||
|
assert(embedderId != null),
|
||||||
|
assert(pan != null),
|
||||||
|
assert(panDelta != null),
|
||||||
|
assert(scale != null),
|
||||||
|
assert(rotation != null),
|
||||||
|
assert(synthesized != null),
|
||||||
|
super(
|
||||||
|
timeStamp: timeStamp,
|
||||||
|
kind: kind,
|
||||||
|
device: device,
|
||||||
|
pointer: pointer,
|
||||||
|
position: position,
|
||||||
|
embedderId: embedderId,
|
||||||
|
synthesized: synthesized,
|
||||||
|
);
|
||||||
|
@override
|
||||||
|
final Offset pan;
|
||||||
|
@override
|
||||||
|
Offset get localPan => pan;
|
||||||
|
@override
|
||||||
|
final Offset panDelta;
|
||||||
|
@override
|
||||||
|
Offset get localPanDelta => panDelta;
|
||||||
|
@override
|
||||||
|
final double scale;
|
||||||
|
@override
|
||||||
|
final double rotation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
PointerPanZoomUpdateEvent transformed(Matrix4? transform) {
|
||||||
|
if (transform == null || transform == this.transform) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return _TransformedPointerPanZoomUpdateEvent(original as PointerPanZoomUpdateEvent? ?? this, transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TransformedPointerPanZoomUpdateEvent extends _TransformedPointerEvent with _CopyPointerPanZoomUpdateEvent implements PointerPanZoomUpdateEvent {
|
||||||
|
_TransformedPointerPanZoomUpdateEvent(this.original, this.transform)
|
||||||
|
: assert(original != null), assert(transform != null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Offset get pan => original.pan;
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final Offset localPan = PointerEvent.transformPosition(transform, pan);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Offset get panDelta => original.panDelta;
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final Offset localPanDelta = PointerEvent.transformDeltaViaPositions(
|
||||||
|
transform: transform,
|
||||||
|
untransformedDelta: panDelta,
|
||||||
|
untransformedEndPosition: pan,
|
||||||
|
transformedEndPosition: localPan,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get scale => original.scale;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get rotation => original.rotation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final PointerPanZoomUpdateEvent original;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Matrix4 transform;
|
||||||
|
|
||||||
|
@override
|
||||||
|
PointerPanZoomUpdateEvent transformed(Matrix4? transform) => original.transformed(transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin _CopyPointerPanZoomEndEvent on PointerEvent {
|
||||||
|
@override
|
||||||
|
PointerPanZoomEndEvent copyWith({
|
||||||
|
Duration? timeStamp,
|
||||||
|
int? pointer,
|
||||||
|
PointerDeviceKind? kind,
|
||||||
|
int? device,
|
||||||
|
Offset? position,
|
||||||
|
Offset? delta,
|
||||||
|
int? buttons,
|
||||||
|
bool? obscured,
|
||||||
|
double? pressure,
|
||||||
|
double? pressureMin,
|
||||||
|
double? pressureMax,
|
||||||
|
double? distance,
|
||||||
|
double? distanceMax,
|
||||||
|
double? size,
|
||||||
|
double? radiusMajor,
|
||||||
|
double? radiusMinor,
|
||||||
|
double? radiusMin,
|
||||||
|
double? radiusMax,
|
||||||
|
double? orientation,
|
||||||
|
double? tilt,
|
||||||
|
bool? synthesized,
|
||||||
|
int? embedderId,
|
||||||
|
}) {
|
||||||
|
return PointerPanZoomEndEvent(
|
||||||
|
timeStamp: timeStamp ?? this.timeStamp,
|
||||||
|
kind: kind ?? this.kind,
|
||||||
|
device: device ?? this.device,
|
||||||
|
position: position ?? this.position,
|
||||||
|
embedderId: embedderId ?? this.embedderId,
|
||||||
|
).transformed(transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The pan/zoom on this pointer has ended.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Listener.onPointerPanZoomEnd], which allows callers to be notified of these
|
||||||
|
/// events in a widget tree.
|
||||||
|
class PointerPanZoomEndEvent extends PointerEvent with _PointerEventDescription, _CopyPointerPanZoomEndEvent {
|
||||||
|
/// Creates a pointer pan/zoom end event.
|
||||||
|
///
|
||||||
|
/// All of the arguments must be non-null.
|
||||||
|
const PointerPanZoomEndEvent({
|
||||||
|
Duration timeStamp = Duration.zero,
|
||||||
|
PointerDeviceKind kind = PointerDeviceKind.mouse,
|
||||||
|
int device = 0,
|
||||||
|
int pointer = 0,
|
||||||
|
Offset position = Offset.zero,
|
||||||
|
int embedderId = 0,
|
||||||
|
bool synthesized = false,
|
||||||
|
}) : assert(timeStamp != null),
|
||||||
|
assert(kind != null),
|
||||||
|
assert(device != null),
|
||||||
|
assert(pointer != null),
|
||||||
|
assert(position != null),
|
||||||
|
assert(embedderId != null),
|
||||||
|
assert(synthesized != null),
|
||||||
|
super(
|
||||||
|
timeStamp: timeStamp,
|
||||||
|
kind: kind,
|
||||||
|
device: device,
|
||||||
|
pointer: pointer,
|
||||||
|
position: position,
|
||||||
|
embedderId: embedderId,
|
||||||
|
synthesized: synthesized,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
PointerPanZoomEndEvent transformed(Matrix4? transform) {
|
||||||
|
if (transform == null || transform == this.transform) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return _TransformedPointerPanZoomEndEvent(original as PointerPanZoomEndEvent? ?? this, transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TransformedPointerPanZoomEndEvent extends _TransformedPointerEvent with _CopyPointerPanZoomEndEvent implements PointerPanZoomEndEvent {
|
||||||
|
_TransformedPointerPanZoomEndEvent(this.original, this.transform)
|
||||||
|
: assert(original != null), assert(transform != null);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final PointerPanZoomEndEvent original;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Matrix4 transform;
|
||||||
|
|
||||||
|
@override
|
||||||
|
PointerPanZoomEndEvent transformed(Matrix4? transform) => original.transformed(transform);
|
||||||
|
}
|
||||||
|
|
||||||
mixin _CopyPointerCancelEvent on PointerEvent {
|
mixin _CopyPointerCancelEvent on PointerEvent {
|
||||||
@override
|
@override
|
||||||
PointerCancelEvent copyWith({
|
PointerCancelEvent copyWith({
|
||||||
@ -2108,8 +2456,7 @@ double computeHitSlop(PointerDeviceKind kind, DeviceGestureSettings? settings) {
|
|||||||
case PointerDeviceKind.invertedStylus:
|
case PointerDeviceKind.invertedStylus:
|
||||||
case PointerDeviceKind.unknown:
|
case PointerDeviceKind.unknown:
|
||||||
case PointerDeviceKind.touch:
|
case PointerDeviceKind.touch:
|
||||||
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
|
case PointerDeviceKind.trackpad:
|
||||||
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
|
|
||||||
return settings?.touchSlop ?? kTouchSlop;
|
return settings?.touchSlop ?? kTouchSlop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2123,8 +2470,7 @@ double computePanSlop(PointerDeviceKind kind, DeviceGestureSettings? settings) {
|
|||||||
case PointerDeviceKind.invertedStylus:
|
case PointerDeviceKind.invertedStylus:
|
||||||
case PointerDeviceKind.unknown:
|
case PointerDeviceKind.unknown:
|
||||||
case PointerDeviceKind.touch:
|
case PointerDeviceKind.touch:
|
||||||
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
|
case PointerDeviceKind.trackpad:
|
||||||
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
|
|
||||||
return settings?.panSlop ?? kPanSlop;
|
return settings?.panSlop ?? kPanSlop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2138,8 +2484,7 @@ double computeScaleSlop(PointerDeviceKind kind) {
|
|||||||
case PointerDeviceKind.invertedStylus:
|
case PointerDeviceKind.invertedStylus:
|
||||||
case PointerDeviceKind.unknown:
|
case PointerDeviceKind.unknown:
|
||||||
case PointerDeviceKind.touch:
|
case PointerDeviceKind.touch:
|
||||||
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
|
case PointerDeviceKind.trackpad:
|
||||||
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
|
|
||||||
return kScaleSlop;
|
return kScaleSlop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -261,14 +261,11 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
return super.isPointerAllowed(event as PointerDownEvent);
|
return super.isPointerAllowed(event as PointerDownEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
void _addPointer(PointerEvent event) {
|
||||||
void addAllowedPointer(PointerDownEvent event) {
|
|
||||||
super.addAllowedPointer(event);
|
|
||||||
_velocityTrackers[event.pointer] = velocityTrackerBuilder(event);
|
_velocityTrackers[event.pointer] = velocityTrackerBuilder(event);
|
||||||
if (_state == _DragState.ready) {
|
if (_state == _DragState.ready) {
|
||||||
_state = _DragState.possible;
|
_state = _DragState.possible;
|
||||||
_initialPosition = OffsetPair(global: event.position, local: event.localPosition);
|
_initialPosition = OffsetPair(global: event.position, local: event.localPosition);
|
||||||
_initialButtons = event.buttons;
|
|
||||||
_pendingDragOffset = OffsetPair.zero;
|
_pendingDragOffset = OffsetPair.zero;
|
||||||
_globalDistanceMoved = 0.0;
|
_globalDistanceMoved = 0.0;
|
||||||
_lastPendingEventTimestamp = event.timeStamp;
|
_lastPendingEventTimestamp = event.timeStamp;
|
||||||
@ -280,44 +277,75 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void handleEvent(PointerEvent event) {
|
void addAllowedPointer(PointerDownEvent event) {
|
||||||
assert(_state != _DragState.ready);
|
super.addAllowedPointer(event);
|
||||||
if (!event.synthesized
|
if (_state == _DragState.ready) {
|
||||||
&& (event is PointerDownEvent || event is PointerMoveEvent)) {
|
_initialButtons = event.buttons;
|
||||||
final VelocityTracker tracker = _velocityTrackers[event.pointer]!;
|
}
|
||||||
assert(tracker != null);
|
_addPointer(event);
|
||||||
tracker.addPosition(event.timeStamp, event.localPosition);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event is PointerMoveEvent) {
|
@override
|
||||||
if (event.buttons != _initialButtons) {
|
void addAllowedPointerPanZoom(PointerPanZoomStartEvent event) {
|
||||||
|
super.addAllowedPointerPanZoom(event);
|
||||||
|
startTrackingPointer(event.pointer, event.transform);
|
||||||
|
if (_state == _DragState.ready) {
|
||||||
|
_initialButtons = kPrimaryButton;
|
||||||
|
}
|
||||||
|
_addPointer(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void handleEvent(PointerEvent event) {
|
||||||
|
assert(_state != _DragState.ready);
|
||||||
|
if (!event.synthesized &&
|
||||||
|
(event is PointerDownEvent ||
|
||||||
|
event is PointerMoveEvent ||
|
||||||
|
event is PointerPanZoomStartEvent ||
|
||||||
|
event is PointerPanZoomUpdateEvent)) {
|
||||||
|
final VelocityTracker tracker = _velocityTrackers[event.pointer]!;
|
||||||
|
assert(tracker != null);
|
||||||
|
if (event is PointerPanZoomStartEvent) {
|
||||||
|
tracker.addPosition(event.timeStamp, Offset.zero);
|
||||||
|
} else if (event is PointerPanZoomUpdateEvent) {
|
||||||
|
tracker.addPosition(event.timeStamp, event.pan);
|
||||||
|
} else {
|
||||||
|
tracker.addPosition(event.timeStamp, event.localPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event is PointerMoveEvent && event.buttons != _initialButtons) {
|
||||||
_giveUpPointer(event.pointer);
|
_giveUpPointer(event.pointer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (event is PointerMoveEvent || event is PointerPanZoomUpdateEvent) {
|
||||||
|
final Offset delta = (event is PointerMoveEvent) ? event.delta : (event as PointerPanZoomUpdateEvent).panDelta;
|
||||||
|
final Offset localDelta = (event is PointerMoveEvent) ? event.localDelta : (event as PointerPanZoomUpdateEvent).localPanDelta;
|
||||||
|
final Offset position = (event is PointerMoveEvent) ? event.position : (event.position + (event as PointerPanZoomUpdateEvent).pan);
|
||||||
|
final Offset localPosition = (event is PointerMoveEvent) ? event.localPosition : (event.localPosition + (event as PointerPanZoomUpdateEvent).localPan);
|
||||||
if (_state == _DragState.accepted) {
|
if (_state == _DragState.accepted) {
|
||||||
_checkUpdate(
|
_checkUpdate(
|
||||||
sourceTimeStamp: event.timeStamp,
|
sourceTimeStamp: event.timeStamp,
|
||||||
delta: _getDeltaForDetails(event.localDelta),
|
delta: _getDeltaForDetails(localDelta),
|
||||||
primaryDelta: _getPrimaryValueFromOffset(event.localDelta),
|
primaryDelta: _getPrimaryValueFromOffset(localDelta),
|
||||||
globalPosition: event.position,
|
globalPosition: position,
|
||||||
localPosition: event.localPosition,
|
localPosition: localPosition,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_pendingDragOffset += OffsetPair(local: event.localDelta, global: event.delta);
|
_pendingDragOffset += OffsetPair(local: localDelta, global: delta);
|
||||||
_lastPendingEventTimestamp = event.timeStamp;
|
_lastPendingEventTimestamp = event.timeStamp;
|
||||||
_lastTransform = event.transform;
|
_lastTransform = event.transform;
|
||||||
final Offset movedLocally = _getDeltaForDetails(event.localDelta);
|
final Offset movedLocally = _getDeltaForDetails(localDelta);
|
||||||
final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!);
|
final Matrix4? localToGlobalTransform = event.transform == null ? null : Matrix4.tryInvert(event.transform!);
|
||||||
_globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
|
_globalDistanceMoved += PointerEvent.transformDeltaViaPositions(
|
||||||
transform: localToGlobalTransform,
|
transform: localToGlobalTransform,
|
||||||
untransformedDelta: movedLocally,
|
untransformedDelta: movedLocally,
|
||||||
untransformedEndPosition: event.localPosition,
|
untransformedEndPosition: localPosition
|
||||||
).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
|
).distance * (_getPrimaryValueFromOffset(movedLocally) ?? 1).sign;
|
||||||
if (_hasSufficientGlobalDistanceToAccept(event.kind, gestureSettings?.touchSlop))
|
if (_hasSufficientGlobalDistanceToAccept(event.kind, gestureSettings?.touchSlop))
|
||||||
resolve(GestureDisposition.accepted);
|
resolve(GestureDisposition.accepted);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (event is PointerUpEvent || event is PointerCancelEvent) {
|
if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent) {
|
||||||
_giveUpPointer(event.pointer);
|
_giveUpPointer(event.pointer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,6 +96,43 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
|
|||||||
/// coming from.
|
/// coming from.
|
||||||
final Map<int, PointerDeviceKind> _pointerToKind = <int, PointerDeviceKind>{};
|
final Map<int, PointerDeviceKind> _pointerToKind = <int, PointerDeviceKind>{};
|
||||||
|
|
||||||
|
/// Registers a new pointer pan/zoom that might be relevant to this gesture
|
||||||
|
/// detector.
|
||||||
|
///
|
||||||
|
/// A pointer pan/zoom is a stream of events that conveys data covering
|
||||||
|
/// pan, zoom, and rotate data from a multi-finger trackpad gesture.
|
||||||
|
///
|
||||||
|
/// The owner of this gesture recognizer calls addPointerPanZoom() with the
|
||||||
|
/// PointerPanZoomStartEvent of each pointer that should be considered for
|
||||||
|
/// this gesture.
|
||||||
|
///
|
||||||
|
/// It's the GestureRecognizer's responsibility to then add itself
|
||||||
|
/// to the global pointer router (see [PointerRouter]) to receive
|
||||||
|
/// subsequent events for this pointer, and to add the pointer to
|
||||||
|
/// the global gesture arena manager (see [GestureArenaManager]) to track
|
||||||
|
/// that pointer.
|
||||||
|
///
|
||||||
|
/// This method is called for each and all pointers being added. In
|
||||||
|
/// most cases, you want to override [addAllowedPointerPanZoom] instead.
|
||||||
|
void addPointerPanZoom(PointerPanZoomStartEvent event) {
|
||||||
|
_pointerToKind[event.pointer] = event.kind;
|
||||||
|
if (isPointerPanZoomAllowed(event)) {
|
||||||
|
addAllowedPointerPanZoom(event);
|
||||||
|
} else {
|
||||||
|
handleNonAllowedPointerPanZoom(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers a new pointer pan/zoom that's been checked to be allowed by this
|
||||||
|
/// gesture recognizer.
|
||||||
|
///
|
||||||
|
/// Subclasses of [GestureRecognizer] are supposed to override this method
|
||||||
|
/// instead of [addPointerPanZoom] because [addPointerPanZoom] will be called for each
|
||||||
|
/// pointer being added while [addAllowedPointerPanZoom] is only called for pointers
|
||||||
|
/// that are allowed by this recognizer.
|
||||||
|
@protected
|
||||||
|
void addAllowedPointerPanZoom(PointerPanZoomStartEvent event) { }
|
||||||
|
|
||||||
/// Registers a new pointer that might be relevant to this gesture
|
/// Registers a new pointer that might be relevant to this gesture
|
||||||
/// detector.
|
/// detector.
|
||||||
///
|
///
|
||||||
@ -147,6 +184,18 @@ abstract class GestureRecognizer extends GestureArenaMember with DiagnosticableT
|
|||||||
return _supportedDevices == null || _supportedDevices!.contains(event.kind);
|
return _supportedDevices == null || _supportedDevices!.contains(event.kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles a pointer pan/zoom being added that's not allowed by this recognizer.
|
||||||
|
///
|
||||||
|
/// Subclasses can override this method and reject the gesture.
|
||||||
|
@protected
|
||||||
|
void handleNonAllowedPointerPanZoom(PointerPanZoomStartEvent event) { }
|
||||||
|
|
||||||
|
/// Checks whether or not a pointer pan/zoom is allowed to be tracked by this recognizer.
|
||||||
|
@protected
|
||||||
|
bool isPointerPanZoomAllowed(PointerPanZoomStartEvent event) {
|
||||||
|
return _supportedDevices == null || _supportedDevices!.contains(event.kind);
|
||||||
|
}
|
||||||
|
|
||||||
/// For a given pointer ID, returns the device kind associated with it.
|
/// For a given pointer ID, returns the device kind associated with it.
|
||||||
///
|
///
|
||||||
/// The pointer ID is expected to be a valid one i.e. an event was received
|
/// The pointer ID is expected to be a valid one i.e. an event was received
|
||||||
@ -397,7 +446,7 @@ abstract class OneSequenceGestureRecognizer extends GestureRecognizer {
|
|||||||
/// a [PointerUpEvent] or a [PointerCancelEvent] event.
|
/// a [PointerUpEvent] or a [PointerCancelEvent] event.
|
||||||
@protected
|
@protected
|
||||||
void stopTrackingIfPointerNoLongerDown(PointerEvent event) {
|
void stopTrackingIfPointerNoLongerDown(PointerEvent event) {
|
||||||
if (event is PointerUpEvent || event is PointerCancelEvent)
|
if (event is PointerUpEvent || event is PointerCancelEvent || event is PointerPanZoomEndEvent)
|
||||||
stopTrackingPointer(event.pointer);
|
stopTrackingPointer(event.pointer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,20 @@ enum _ScaleState {
|
|||||||
started,
|
started,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _PointerPanZoomData {
|
||||||
|
_PointerPanZoomData({
|
||||||
|
required this.focalPoint,
|
||||||
|
required this.scale,
|
||||||
|
required this.rotation
|
||||||
|
});
|
||||||
|
Offset focalPoint;
|
||||||
|
double scale;
|
||||||
|
double rotation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '_PointerPanZoomData(focalPoint: $focalPoint, scale: $scale, angle: $rotation)';
|
||||||
|
}
|
||||||
|
|
||||||
/// Details for [GestureScaleStartCallback].
|
/// Details for [GestureScaleStartCallback].
|
||||||
class ScaleStartDetails {
|
class ScaleStartDetails {
|
||||||
/// Creates details for [GestureScaleStartCallback].
|
/// Creates details for [GestureScaleStartCallback].
|
||||||
@ -175,7 +189,7 @@ class ScaleUpdateDetails {
|
|||||||
' verticalScale: $verticalScale,'
|
' verticalScale: $verticalScale,'
|
||||||
' rotation: $rotation,'
|
' rotation: $rotation,'
|
||||||
' pointerCount: $pointerCount,'
|
' pointerCount: $pointerCount,'
|
||||||
' focalPointDelta: $localFocalPoint)';
|
' focalPointDelta: $focalPointDelta)';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Details for [GestureScaleEndCallback].
|
/// Details for [GestureScaleEndCallback].
|
||||||
@ -329,21 +343,51 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
late Offset _localFocalPoint;
|
late Offset _localFocalPoint;
|
||||||
_LineBetweenPointers? _initialLine;
|
_LineBetweenPointers? _initialLine;
|
||||||
_LineBetweenPointers? _currentLine;
|
_LineBetweenPointers? _currentLine;
|
||||||
late Map<int, Offset> _pointerLocations;
|
final Map<int, Offset> _pointerLocations = <int, Offset>{};
|
||||||
late List<int> _pointerQueue; // A queue to sort pointers in order of entrance
|
final List<int> _pointerQueue = <int>[]; // A queue to sort pointers in order of entrance
|
||||||
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
|
final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
|
||||||
late Offset _delta;
|
late Offset _delta;
|
||||||
|
final Map<int, _PointerPanZoomData> _pointerPanZooms = <int, _PointerPanZoomData>{};
|
||||||
|
double _initialPanZoomScaleFactor = 1;
|
||||||
|
double _initialPanZoomRotationFactor = 0;
|
||||||
|
|
||||||
double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
|
double get _pointerScaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
|
||||||
|
|
||||||
double get _horizontalScaleFactor => _initialHorizontalSpan > 0.0 ? _currentHorizontalSpan / _initialHorizontalSpan : 1.0;
|
double get _pointerHorizontalScaleFactor => _initialHorizontalSpan > 0.0 ? _currentHorizontalSpan / _initialHorizontalSpan : 1.0;
|
||||||
|
|
||||||
double get _verticalScaleFactor => _initialVerticalSpan > 0.0 ? _currentVerticalSpan / _initialVerticalSpan : 1.0;
|
double get _pointerVerticalScaleFactor => _initialVerticalSpan > 0.0 ? _currentVerticalSpan / _initialVerticalSpan : 1.0;
|
||||||
|
|
||||||
|
double get _scaleFactor {
|
||||||
|
double scale = _pointerScaleFactor;
|
||||||
|
for (final _PointerPanZoomData p in _pointerPanZooms.values) {
|
||||||
|
scale *= p.scale / _initialPanZoomScaleFactor;
|
||||||
|
}
|
||||||
|
return scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get _horizontalScaleFactor {
|
||||||
|
double scale = _pointerHorizontalScaleFactor;
|
||||||
|
for (final _PointerPanZoomData p in _pointerPanZooms.values) {
|
||||||
|
scale *= p.scale / _initialPanZoomScaleFactor;
|
||||||
|
}
|
||||||
|
return scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
double get _verticalScaleFactor {
|
||||||
|
double scale = _pointerVerticalScaleFactor;
|
||||||
|
for (final _PointerPanZoomData p in _pointerPanZooms.values) {
|
||||||
|
scale *= p.scale / _initialPanZoomScaleFactor;
|
||||||
|
}
|
||||||
|
return scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get _pointerCount {
|
||||||
|
return _pointerPanZooms.length + _pointerQueue.length;
|
||||||
|
}
|
||||||
|
|
||||||
double _computeRotationFactor() {
|
double _computeRotationFactor() {
|
||||||
if (_initialLine == null || _currentLine == null) {
|
double factor = 0.0;
|
||||||
return 0.0;
|
if (_initialLine != null && _currentLine != null) {
|
||||||
}
|
|
||||||
final double fx = _initialLine!.pointerStartLocation.dx;
|
final double fx = _initialLine!.pointerStartLocation.dx;
|
||||||
final double fy = _initialLine!.pointerStartLocation.dy;
|
final double fy = _initialLine!.pointerStartLocation.dy;
|
||||||
final double sx = _initialLine!.pointerEndLocation.dx;
|
final double sx = _initialLine!.pointerEndLocation.dx;
|
||||||
@ -357,7 +401,13 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
final double angle1 = math.atan2(fy - sy, fx - sx);
|
final double angle1 = math.atan2(fy - sy, fx - sx);
|
||||||
final double angle2 = math.atan2(nfy - nsy, nfx - nsx);
|
final double angle2 = math.atan2(nfy - nsy, nfx - nsx);
|
||||||
|
|
||||||
return angle2 - angle1;
|
factor = angle2 - angle1;
|
||||||
|
}
|
||||||
|
for (final _PointerPanZoomData p in _pointerPanZooms.values) {
|
||||||
|
factor += p.rotation;
|
||||||
|
}
|
||||||
|
factor -= _initialPanZoomRotationFactor;
|
||||||
|
return factor;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -372,8 +422,21 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
_currentHorizontalSpan = 0.0;
|
_currentHorizontalSpan = 0.0;
|
||||||
_initialVerticalSpan = 0.0;
|
_initialVerticalSpan = 0.0;
|
||||||
_currentVerticalSpan = 0.0;
|
_currentVerticalSpan = 0.0;
|
||||||
_pointerLocations = <int, Offset>{};
|
}
|
||||||
_pointerQueue = <int>[];
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool isPointerPanZoomAllowed(PointerPanZoomStartEvent event) => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addAllowedPointerPanZoom(PointerPanZoomStartEvent event) {
|
||||||
|
super.addAllowedPointerPanZoom(event);
|
||||||
|
startTrackingPointer(event.pointer, event.transform);
|
||||||
|
_velocityTrackers[event.pointer] = VelocityTracker.withKind(event.kind);
|
||||||
|
if (_state == _ScaleState.ready) {
|
||||||
|
_state = _ScaleState.possible;
|
||||||
|
_initialPanZoomScaleFactor = 1.0;
|
||||||
|
_initialPanZoomRotationFactor = 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -400,6 +463,30 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
_pointerQueue.remove(event.pointer);
|
_pointerQueue.remove(event.pointer);
|
||||||
didChangeConfiguration = true;
|
didChangeConfiguration = true;
|
||||||
_lastTransform = event.transform;
|
_lastTransform = event.transform;
|
||||||
|
} else if (event is PointerPanZoomStartEvent) {
|
||||||
|
assert(_pointerPanZooms[event.pointer] == null);
|
||||||
|
_pointerPanZooms[event.pointer] = _PointerPanZoomData(
|
||||||
|
focalPoint: event.position,
|
||||||
|
scale: 1,
|
||||||
|
rotation: 0
|
||||||
|
);
|
||||||
|
didChangeConfiguration = true;
|
||||||
|
shouldStartIfAccepted = true;
|
||||||
|
} else if (event is PointerPanZoomUpdateEvent) {
|
||||||
|
assert(_pointerPanZooms[event.pointer] != null);
|
||||||
|
if (!event.synthesized)
|
||||||
|
_velocityTrackers[event.pointer]!.addPosition(event.timeStamp, event.pan);
|
||||||
|
_pointerPanZooms[event.pointer] = _PointerPanZoomData(
|
||||||
|
focalPoint: event.position + event.pan,
|
||||||
|
scale: event.scale,
|
||||||
|
rotation: event.rotation
|
||||||
|
);
|
||||||
|
_lastTransform = event.transform;
|
||||||
|
shouldStartIfAccepted = true;
|
||||||
|
} else if (event is PointerPanZoomEndEvent) {
|
||||||
|
assert(_pointerPanZooms[event.pointer] != null);
|
||||||
|
_pointerPanZooms.remove(event.pointer);
|
||||||
|
didChangeConfiguration = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateLines();
|
_updateLines();
|
||||||
@ -411,15 +498,15 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _update() {
|
void _update() {
|
||||||
final int count = _pointerLocations.keys.length;
|
|
||||||
|
|
||||||
final Offset? previousFocalPoint = _currentFocalPoint;
|
final Offset? previousFocalPoint = _currentFocalPoint;
|
||||||
|
|
||||||
// Compute the focal point
|
// Compute the focal point
|
||||||
Offset focalPoint = Offset.zero;
|
Offset focalPoint = Offset.zero;
|
||||||
for (final int pointer in _pointerLocations.keys)
|
for (final int pointer in _pointerLocations.keys)
|
||||||
focalPoint += _pointerLocations[pointer]!;
|
focalPoint += _pointerLocations[pointer]!;
|
||||||
_currentFocalPoint = count > 0 ? focalPoint / count.toDouble() : Offset.zero;
|
for (final _PointerPanZoomData p in _pointerPanZooms.values)
|
||||||
|
focalPoint += p.focalPoint;
|
||||||
|
_currentFocalPoint = _pointerCount > 0 ? focalPoint / _pointerCount.toDouble() : Offset.zero;
|
||||||
|
|
||||||
if (previousFocalPoint == null) {
|
if (previousFocalPoint == null) {
|
||||||
_localFocalPoint = PointerEvent.transformPosition(
|
_localFocalPoint = PointerEvent.transformPosition(
|
||||||
@ -436,6 +523,14 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
_delta = _localFocalPoint - localPreviousFocalPoint;
|
_delta = _localFocalPoint - localPreviousFocalPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final int count = _pointerLocations.keys.length;
|
||||||
|
|
||||||
|
Offset pointerFocalPoint = Offset.zero;
|
||||||
|
for (final int pointer in _pointerLocations.keys)
|
||||||
|
pointerFocalPoint += _pointerLocations[pointer]!;
|
||||||
|
if (count > 0)
|
||||||
|
pointerFocalPoint = pointerFocalPoint / count.toDouble();
|
||||||
|
|
||||||
// Span is the average deviation from focal point. Horizontal and vertical
|
// Span is the average deviation from focal point. Horizontal and vertical
|
||||||
// spans are the average deviations from the focal point's horizontal and
|
// spans are the average deviations from the focal point's horizontal and
|
||||||
// vertical coordinates, respectively.
|
// vertical coordinates, respectively.
|
||||||
@ -443,9 +538,9 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
double totalHorizontalDeviation = 0.0;
|
double totalHorizontalDeviation = 0.0;
|
||||||
double totalVerticalDeviation = 0.0;
|
double totalVerticalDeviation = 0.0;
|
||||||
for (final int pointer in _pointerLocations.keys) {
|
for (final int pointer in _pointerLocations.keys) {
|
||||||
totalDeviation += (_currentFocalPoint! - _pointerLocations[pointer]!).distance;
|
totalDeviation += (pointerFocalPoint - _pointerLocations[pointer]!).distance;
|
||||||
totalHorizontalDeviation += (_currentFocalPoint!.dx - _pointerLocations[pointer]!.dx).abs();
|
totalHorizontalDeviation += (pointerFocalPoint.dx - _pointerLocations[pointer]!.dx).abs();
|
||||||
totalVerticalDeviation += (_currentFocalPoint!.dy - _pointerLocations[pointer]!.dy).abs();
|
totalVerticalDeviation += (pointerFocalPoint.dy - _pointerLocations[pointer]!.dy).abs();
|
||||||
}
|
}
|
||||||
_currentSpan = count > 0 ? totalDeviation / count : 0.0;
|
_currentSpan = count > 0 ? totalDeviation / count : 0.0;
|
||||||
_currentHorizontalSpan = count > 0 ? totalHorizontalDeviation / count : 0.0;
|
_currentHorizontalSpan = count > 0 ? totalHorizontalDeviation / count : 0.0;
|
||||||
@ -488,6 +583,13 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
_initialLine = _currentLine;
|
_initialLine = _currentLine;
|
||||||
_initialHorizontalSpan = _currentHorizontalSpan;
|
_initialHorizontalSpan = _currentHorizontalSpan;
|
||||||
_initialVerticalSpan = _currentVerticalSpan;
|
_initialVerticalSpan = _currentVerticalSpan;
|
||||||
|
if (_pointerPanZooms.isEmpty) {
|
||||||
|
_initialPanZoomScaleFactor = 1.0;
|
||||||
|
_initialPanZoomRotationFactor = 0.0;
|
||||||
|
} else {
|
||||||
|
_initialPanZoomScaleFactor = _scaleFactor / _pointerScaleFactor;
|
||||||
|
_initialPanZoomRotationFactor = _pointerPanZooms.values.map((_PointerPanZoomData x) => x.rotation).reduce((double a, double b) => a + b);
|
||||||
|
}
|
||||||
if (_state == _ScaleState.started) {
|
if (_state == _ScaleState.started) {
|
||||||
if (onEnd != null) {
|
if (onEnd != null) {
|
||||||
final VelocityTracker tracker = _velocityTrackers[pointer]!;
|
final VelocityTracker tracker = _velocityTrackers[pointer]!;
|
||||||
@ -497,9 +599,9 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
|
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
|
||||||
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
|
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
|
||||||
velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
|
velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
|
||||||
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerQueue.length)));
|
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerCount)));
|
||||||
} else {
|
} else {
|
||||||
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(pointerCount: _pointerQueue.length)));
|
invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(pointerCount: _pointerCount)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_state = _ScaleState.accepted;
|
_state = _ScaleState.accepted;
|
||||||
@ -515,7 +617,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
if (_state == _ScaleState.possible) {
|
if (_state == _ScaleState.possible) {
|
||||||
final double spanDelta = (_currentSpan - _initialSpan).abs();
|
final double spanDelta = (_currentSpan - _initialSpan).abs();
|
||||||
final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance;
|
final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance;
|
||||||
if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind, gestureSettings))
|
if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind, gestureSettings) || math.max(_scaleFactor / _pointerScaleFactor, _pointerScaleFactor / _scaleFactor) > 1.05)
|
||||||
resolve(GestureDisposition.accepted);
|
resolve(GestureDisposition.accepted);
|
||||||
} else if (_state.index >= _ScaleState.accepted.index) {
|
} else if (_state.index >= _ScaleState.accepted.index) {
|
||||||
resolve(GestureDisposition.accepted);
|
resolve(GestureDisposition.accepted);
|
||||||
@ -535,7 +637,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
focalPoint: _currentFocalPoint!,
|
focalPoint: _currentFocalPoint!,
|
||||||
localFocalPoint: _localFocalPoint,
|
localFocalPoint: _localFocalPoint,
|
||||||
rotation: _computeRotationFactor(),
|
rotation: _computeRotationFactor(),
|
||||||
pointerCount: _pointerQueue.length,
|
pointerCount: _pointerCount,
|
||||||
focalPointDelta: _delta,
|
focalPointDelta: _delta,
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
@ -548,7 +650,7 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
onStart!(ScaleStartDetails(
|
onStart!(ScaleStartDetails(
|
||||||
focalPoint: _currentFocalPoint!,
|
focalPoint: _currentFocalPoint!,
|
||||||
localFocalPoint: _localFocalPoint,
|
localFocalPoint: _localFocalPoint,
|
||||||
pointerCount: _pointerQueue.length,
|
pointerCount: _pointerCount,
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -564,12 +666,22 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
|
|||||||
_initialLine = _currentLine;
|
_initialLine = _currentLine;
|
||||||
_initialHorizontalSpan = _currentHorizontalSpan;
|
_initialHorizontalSpan = _currentHorizontalSpan;
|
||||||
_initialVerticalSpan = _currentVerticalSpan;
|
_initialVerticalSpan = _currentVerticalSpan;
|
||||||
|
if (_pointerPanZooms.isEmpty) {
|
||||||
|
_initialPanZoomScaleFactor = 1.0;
|
||||||
|
_initialPanZoomRotationFactor = 0.0;
|
||||||
|
} else {
|
||||||
|
_initialPanZoomScaleFactor = _scaleFactor / _pointerScaleFactor;
|
||||||
|
_initialPanZoomRotationFactor = _pointerPanZooms.values.map((_PointerPanZoomData x) => x.rotation).reduce((double a, double b) => a + b);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void rejectGesture(int pointer) {
|
void rejectGesture(int pointer) {
|
||||||
|
_pointerPanZooms.remove(pointer);
|
||||||
|
_pointerLocations.remove(pointer);
|
||||||
|
_pointerQueue.remove(pointer);
|
||||||
stopTrackingPointer(pointer);
|
stopTrackingPointer(pointer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2858,6 +2858,21 @@ typedef PointerUpEventListener = void Function(PointerUpEvent event);
|
|||||||
/// Used by [Listener] and [RenderPointerListener].
|
/// Used by [Listener] and [RenderPointerListener].
|
||||||
typedef PointerCancelEventListener = void Function(PointerCancelEvent event);
|
typedef PointerCancelEventListener = void Function(PointerCancelEvent event);
|
||||||
|
|
||||||
|
/// Signature for listening to [PointerPanZoomStartEvent] events.
|
||||||
|
///
|
||||||
|
/// Used by [Listener] and [RenderPointerListener].
|
||||||
|
typedef PointerPanZoomStartEventListener = void Function(PointerPanZoomStartEvent event);
|
||||||
|
|
||||||
|
/// Signature for listening to [PointerPanZoomUpdateEvent] events.
|
||||||
|
///
|
||||||
|
/// Used by [Listener] and [RenderPointerListener].
|
||||||
|
typedef PointerPanZoomUpdateEventListener = void Function(PointerPanZoomUpdateEvent event);
|
||||||
|
|
||||||
|
/// Signature for listening to [PointerPanZoomEndEvent] events.
|
||||||
|
///
|
||||||
|
/// Used by [Listener] and [RenderPointerListener].
|
||||||
|
typedef PointerPanZoomEndEventListener = void Function(PointerPanZoomEndEvent event);
|
||||||
|
|
||||||
/// Signature for listening to [PointerSignalEvent] events.
|
/// Signature for listening to [PointerSignalEvent] events.
|
||||||
///
|
///
|
||||||
/// Used by [Listener] and [RenderPointerListener].
|
/// Used by [Listener] and [RenderPointerListener].
|
||||||
@ -2885,6 +2900,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
|
|||||||
this.onPointerUp,
|
this.onPointerUp,
|
||||||
this.onPointerHover,
|
this.onPointerHover,
|
||||||
this.onPointerCancel,
|
this.onPointerCancel,
|
||||||
|
this.onPointerPanZoomStart,
|
||||||
|
this.onPointerPanZoomUpdate,
|
||||||
|
this.onPointerPanZoomEnd,
|
||||||
this.onPointerSignal,
|
this.onPointerSignal,
|
||||||
HitTestBehavior behavior = HitTestBehavior.deferToChild,
|
HitTestBehavior behavior = HitTestBehavior.deferToChild,
|
||||||
RenderBox? child,
|
RenderBox? child,
|
||||||
@ -2909,6 +2927,15 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
|
|||||||
/// no longer directed towards this receiver.
|
/// no longer directed towards this receiver.
|
||||||
PointerCancelEventListener? onPointerCancel;
|
PointerCancelEventListener? onPointerCancel;
|
||||||
|
|
||||||
|
/// Called when a pan/zoom begins such as from a trackpad gesture.
|
||||||
|
PointerPanZoomStartEventListener? onPointerPanZoomStart;
|
||||||
|
|
||||||
|
/// Called when a pan/zoom is updated.
|
||||||
|
PointerPanZoomUpdateEventListener? onPointerPanZoomUpdate;
|
||||||
|
|
||||||
|
/// Called when a pan/zoom finishes.
|
||||||
|
PointerPanZoomEndEventListener? onPointerPanZoomEnd;
|
||||||
|
|
||||||
/// Called when a pointer signal occurs over this object.
|
/// Called when a pointer signal occurs over this object.
|
||||||
PointerSignalEventListener? onPointerSignal;
|
PointerSignalEventListener? onPointerSignal;
|
||||||
|
|
||||||
@ -2930,6 +2957,12 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
|
|||||||
return onPointerHover?.call(event);
|
return onPointerHover?.call(event);
|
||||||
if (event is PointerCancelEvent)
|
if (event is PointerCancelEvent)
|
||||||
return onPointerCancel?.call(event);
|
return onPointerCancel?.call(event);
|
||||||
|
if (event is PointerPanZoomStartEvent)
|
||||||
|
return onPointerPanZoomStart?.call(event);
|
||||||
|
if (event is PointerPanZoomUpdateEvent)
|
||||||
|
return onPointerPanZoomUpdate?.call(event);
|
||||||
|
if (event is PointerPanZoomEndEvent)
|
||||||
|
return onPointerPanZoomEnd?.call(event);
|
||||||
if (event is PointerSignalEvent)
|
if (event is PointerSignalEvent)
|
||||||
return onPointerSignal?.call(event);
|
return onPointerSignal?.call(event);
|
||||||
}
|
}
|
||||||
@ -2945,6 +2978,9 @@ class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
|
|||||||
'up': onPointerUp,
|
'up': onPointerUp,
|
||||||
'hover': onPointerHover,
|
'hover': onPointerHover,
|
||||||
'cancel': onPointerCancel,
|
'cancel': onPointerCancel,
|
||||||
|
'panZoomStart': onPointerPanZoomStart,
|
||||||
|
'panZoomUpdate': onPointerPanZoomUpdate,
|
||||||
|
'panZoomEnd': onPointerPanZoomEnd,
|
||||||
'signal': onPointerSignal,
|
'signal': onPointerSignal,
|
||||||
},
|
},
|
||||||
ifEmpty: '<none>',
|
ifEmpty: '<none>',
|
||||||
|
@ -634,6 +634,7 @@ class _AndroidMotionEventConverter {
|
|||||||
int toolType = AndroidPointerProperties.kToolTypeUnknown;
|
int toolType = AndroidPointerProperties.kToolTypeUnknown;
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case PointerDeviceKind.touch:
|
case PointerDeviceKind.touch:
|
||||||
|
case PointerDeviceKind.trackpad:
|
||||||
toolType = AndroidPointerProperties.kToolTypeFinger;
|
toolType = AndroidPointerProperties.kToolTypeFinger;
|
||||||
break;
|
break;
|
||||||
case PointerDeviceKind.mouse:
|
case PointerDeviceKind.mouse:
|
||||||
@ -646,8 +647,6 @@ class _AndroidMotionEventConverter {
|
|||||||
toolType = AndroidPointerProperties.kToolTypeEraser;
|
toolType = AndroidPointerProperties.kToolTypeEraser;
|
||||||
break;
|
break;
|
||||||
case PointerDeviceKind.unknown:
|
case PointerDeviceKind.unknown:
|
||||||
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
|
|
||||||
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
|
|
||||||
toolType = AndroidPointerProperties.kToolTypeUnknown;
|
toolType = AndroidPointerProperties.kToolTypeUnknown;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -6090,6 +6090,9 @@ class Listener extends SingleChildRenderObjectWidget {
|
|||||||
this.onPointerUp,
|
this.onPointerUp,
|
||||||
this.onPointerHover,
|
this.onPointerHover,
|
||||||
this.onPointerCancel,
|
this.onPointerCancel,
|
||||||
|
this.onPointerPanZoomStart,
|
||||||
|
this.onPointerPanZoomUpdate,
|
||||||
|
this.onPointerPanZoomEnd,
|
||||||
this.onPointerSignal,
|
this.onPointerSignal,
|
||||||
this.behavior = HitTestBehavior.deferToChild,
|
this.behavior = HitTestBehavior.deferToChild,
|
||||||
Widget? child,
|
Widget? child,
|
||||||
@ -6119,6 +6122,15 @@ class Listener extends SingleChildRenderObjectWidget {
|
|||||||
/// no longer directed towards this receiver.
|
/// no longer directed towards this receiver.
|
||||||
final PointerCancelEventListener? onPointerCancel;
|
final PointerCancelEventListener? onPointerCancel;
|
||||||
|
|
||||||
|
/// Called when a pan/zoom begins such as from a trackpad gesture.
|
||||||
|
final PointerPanZoomStartEventListener? onPointerPanZoomStart;
|
||||||
|
|
||||||
|
/// Called when a pan/zoom is updated.
|
||||||
|
final PointerPanZoomUpdateEventListener? onPointerPanZoomUpdate;
|
||||||
|
|
||||||
|
/// Called when a pan/zoom finishes.
|
||||||
|
final PointerPanZoomEndEventListener? onPointerPanZoomEnd;
|
||||||
|
|
||||||
/// Called when a pointer signal occurs over this object.
|
/// Called when a pointer signal occurs over this object.
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
@ -6138,6 +6150,9 @@ class Listener extends SingleChildRenderObjectWidget {
|
|||||||
onPointerUp: onPointerUp,
|
onPointerUp: onPointerUp,
|
||||||
onPointerHover: onPointerHover,
|
onPointerHover: onPointerHover,
|
||||||
onPointerCancel: onPointerCancel,
|
onPointerCancel: onPointerCancel,
|
||||||
|
onPointerPanZoomStart: onPointerPanZoomStart,
|
||||||
|
onPointerPanZoomUpdate: onPointerPanZoomUpdate,
|
||||||
|
onPointerPanZoomEnd: onPointerPanZoomEnd,
|
||||||
onPointerSignal: onPointerSignal,
|
onPointerSignal: onPointerSignal,
|
||||||
behavior: behavior,
|
behavior: behavior,
|
||||||
);
|
);
|
||||||
@ -6151,6 +6166,9 @@ class Listener extends SingleChildRenderObjectWidget {
|
|||||||
..onPointerUp = onPointerUp
|
..onPointerUp = onPointerUp
|
||||||
..onPointerHover = onPointerHover
|
..onPointerHover = onPointerHover
|
||||||
..onPointerCancel = onPointerCancel
|
..onPointerCancel = onPointerCancel
|
||||||
|
..onPointerPanZoomStart = onPointerPanZoomStart
|
||||||
|
..onPointerPanZoomUpdate = onPointerPanZoomUpdate
|
||||||
|
..onPointerPanZoomEnd = onPointerPanZoomEnd
|
||||||
..onPointerSignal = onPointerSignal
|
..onPointerSignal = onPointerSignal
|
||||||
..behavior = behavior;
|
..behavior = behavior;
|
||||||
}
|
}
|
||||||
@ -6164,6 +6182,9 @@ class Listener extends SingleChildRenderObjectWidget {
|
|||||||
if (onPointerUp != null) 'up',
|
if (onPointerUp != null) 'up',
|
||||||
if (onPointerHover != null) 'hover',
|
if (onPointerHover != null) 'hover',
|
||||||
if (onPointerCancel != null) 'cancel',
|
if (onPointerCancel != null) 'cancel',
|
||||||
|
if (onPointerPanZoomStart != null) 'panZoomStart',
|
||||||
|
if (onPointerPanZoomUpdate != null) 'panZoomUpdate',
|
||||||
|
if (onPointerPanZoomEnd != null) 'panZoomEnd',
|
||||||
if (onPointerSignal != null) 'signal',
|
if (onPointerSignal != null) 'signal',
|
||||||
];
|
];
|
||||||
properties.add(IterableProperty<String>('listeners', listeners, ifEmpty: '<none>'));
|
properties.add(IterableProperty<String>('listeners', listeners, ifEmpty: '<none>'));
|
||||||
|
@ -1648,9 +1648,8 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
|
|||||||
expectedMode = FocusHighlightMode.touch;
|
expectedMode = FocusHighlightMode.touch;
|
||||||
break;
|
break;
|
||||||
case PointerDeviceKind.mouse:
|
case PointerDeviceKind.mouse:
|
||||||
|
case PointerDeviceKind.trackpad:
|
||||||
case PointerDeviceKind.unknown:
|
case PointerDeviceKind.unknown:
|
||||||
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
|
|
||||||
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
|
|
||||||
_lastInteractionWasTouch = false;
|
_lastInteractionWasTouch = false;
|
||||||
expectedMode = FocusHighlightMode.traditional;
|
expectedMode = FocusHighlightMode.traditional;
|
||||||
break;
|
break;
|
||||||
|
@ -1435,6 +1435,13 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
|
|||||||
recognizer.addPointer(event);
|
recognizer.addPointer(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handlePointerPanZoomStart(PointerPanZoomStartEvent event) {
|
||||||
|
assert(_recognizers != null);
|
||||||
|
for (final GestureRecognizer recognizer in _recognizers!.values) {
|
||||||
|
recognizer.addPointerPanZoom(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HitTestBehavior get _defaultBehavior {
|
HitTestBehavior get _defaultBehavior {
|
||||||
return widget.child == null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild;
|
return widget.child == null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild;
|
||||||
}
|
}
|
||||||
@ -1449,6 +1456,7 @@ class RawGestureDetectorState extends State<RawGestureDetector> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget result = Listener(
|
Widget result = Listener(
|
||||||
onPointerDown: _handlePointerDown,
|
onPointerDown: _handlePointerDown,
|
||||||
|
onPointerPanZoomStart: _handlePointerPanZoomStart,
|
||||||
behavior: widget.behavior ?? _defaultBehavior,
|
behavior: widget.behavior ?? _defaultBehavior,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
);
|
);
|
||||||
|
@ -19,6 +19,7 @@ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
|
|||||||
PointerDeviceKind.touch,
|
PointerDeviceKind.touch,
|
||||||
PointerDeviceKind.stylus,
|
PointerDeviceKind.stylus,
|
||||||
PointerDeviceKind.invertedStylus,
|
PointerDeviceKind.invertedStylus,
|
||||||
|
PointerDeviceKind.trackpad,
|
||||||
// The VoiceAccess sends pointer events with unknown type when scrolling
|
// The VoiceAccess sends pointer events with unknown type when scrolling
|
||||||
// scrollables.
|
// scrollables.
|
||||||
PointerDeviceKind.unknown,
|
PointerDeviceKind.unknown,
|
||||||
|
@ -682,13 +682,12 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
|
|||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case PointerDeviceKind.touch:
|
case PointerDeviceKind.touch:
|
||||||
|
case PointerDeviceKind.trackpad:
|
||||||
return paddedRect.contains(position);
|
return paddedRect.contains(position);
|
||||||
case PointerDeviceKind.mouse:
|
case PointerDeviceKind.mouse:
|
||||||
case PointerDeviceKind.stylus:
|
case PointerDeviceKind.stylus:
|
||||||
case PointerDeviceKind.invertedStylus:
|
case PointerDeviceKind.invertedStylus:
|
||||||
case PointerDeviceKind.unknown:
|
case PointerDeviceKind.unknown:
|
||||||
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
|
|
||||||
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
|
|
||||||
return interactiveRect.contains(position);
|
return interactiveRect.contains(position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -713,6 +712,7 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
|
|||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case PointerDeviceKind.touch:
|
case PointerDeviceKind.touch:
|
||||||
|
case PointerDeviceKind.trackpad:
|
||||||
final Rect touchThumbRect = _thumbRect!.expandToInclude(
|
final Rect touchThumbRect = _thumbRect!.expandToInclude(
|
||||||
Rect.fromCircle(center: _thumbRect!.center, radius: _kMinInteractiveSize / 2),
|
Rect.fromCircle(center: _thumbRect!.center, radius: _kMinInteractiveSize / 2),
|
||||||
);
|
);
|
||||||
@ -721,8 +721,6 @@ class ScrollbarPainter extends ChangeNotifier implements CustomPainter {
|
|||||||
case PointerDeviceKind.stylus:
|
case PointerDeviceKind.stylus:
|
||||||
case PointerDeviceKind.invertedStylus:
|
case PointerDeviceKind.invertedStylus:
|
||||||
case PointerDeviceKind.unknown:
|
case PointerDeviceKind.unknown:
|
||||||
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
|
|
||||||
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
|
|
||||||
return _thumbRect!.contains(position);
|
return _thumbRect!.contains(position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1963,6 +1961,7 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
|
|||||||
onExit: (PointerExitEvent event) {
|
onExit: (PointerExitEvent event) {
|
||||||
switch(event.kind) {
|
switch(event.kind) {
|
||||||
case PointerDeviceKind.mouse:
|
case PointerDeviceKind.mouse:
|
||||||
|
case PointerDeviceKind.trackpad:
|
||||||
if (enableGestures)
|
if (enableGestures)
|
||||||
handleHoverExit(event);
|
handleHoverExit(event);
|
||||||
break;
|
break;
|
||||||
@ -1970,14 +1969,13 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
|
|||||||
case PointerDeviceKind.invertedStylus:
|
case PointerDeviceKind.invertedStylus:
|
||||||
case PointerDeviceKind.unknown:
|
case PointerDeviceKind.unknown:
|
||||||
case PointerDeviceKind.touch:
|
case PointerDeviceKind.touch:
|
||||||
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
|
|
||||||
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onHover: (PointerHoverEvent event) {
|
onHover: (PointerHoverEvent event) {
|
||||||
switch(event.kind) {
|
switch(event.kind) {
|
||||||
case PointerDeviceKind.mouse:
|
case PointerDeviceKind.mouse:
|
||||||
|
case PointerDeviceKind.trackpad:
|
||||||
if (enableGestures)
|
if (enableGestures)
|
||||||
handleHover(event);
|
handleHover(event);
|
||||||
break;
|
break;
|
||||||
@ -1985,8 +1983,6 @@ class RawScrollbarState<T extends RawScrollbar> extends State<T> with TickerProv
|
|||||||
case PointerDeviceKind.invertedStylus:
|
case PointerDeviceKind.invertedStylus:
|
||||||
case PointerDeviceKind.unknown:
|
case PointerDeviceKind.unknown:
|
||||||
case PointerDeviceKind.touch:
|
case PointerDeviceKind.touch:
|
||||||
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
|
|
||||||
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1525,6 +1525,7 @@ class TextSelectionGestureDetectorBuilder {
|
|||||||
case TargetPlatform.macOS:
|
case TargetPlatform.macOS:
|
||||||
switch (details.kind) {
|
switch (details.kind) {
|
||||||
case PointerDeviceKind.mouse:
|
case PointerDeviceKind.mouse:
|
||||||
|
case PointerDeviceKind.trackpad:
|
||||||
case PointerDeviceKind.stylus:
|
case PointerDeviceKind.stylus:
|
||||||
case PointerDeviceKind.invertedStylus:
|
case PointerDeviceKind.invertedStylus:
|
||||||
// Precise devices should place the cursor at a precise position.
|
// Precise devices should place the cursor at a precise position.
|
||||||
@ -1532,8 +1533,6 @@ class TextSelectionGestureDetectorBuilder {
|
|||||||
break;
|
break;
|
||||||
case PointerDeviceKind.touch:
|
case PointerDeviceKind.touch:
|
||||||
case PointerDeviceKind.unknown:
|
case PointerDeviceKind.unknown:
|
||||||
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
|
|
||||||
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
|
|
||||||
// On macOS/iOS/iPadOS a touch tap places the cursor at the edge
|
// On macOS/iOS/iPadOS a touch tap places the cursor at the edge
|
||||||
// of the word.
|
// of the word.
|
||||||
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
|
renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
|
||||||
|
@ -1484,4 +1484,219 @@ void main() {
|
|||||||
|
|
||||||
tap2.dispose();
|
tap2.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testGesture('Should recognize pan gestures from platform', (GestureTester tester) {
|
||||||
|
final PanGestureRecognizer pan = PanGestureRecognizer();
|
||||||
|
// We need a competing gesture recognizer so that the gesture is not immediately claimed.
|
||||||
|
final PanGestureRecognizer competingPan = PanGestureRecognizer();
|
||||||
|
addTearDown(pan.dispose);
|
||||||
|
addTearDown(competingPan.dispose);
|
||||||
|
|
||||||
|
bool didStartPan = false;
|
||||||
|
pan.onStart = (_) {
|
||||||
|
didStartPan = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
Offset? updatedScrollDelta;
|
||||||
|
pan.onUpdate = (DragUpdateDetails details) {
|
||||||
|
updatedScrollDelta = details.delta;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool didEndPan = false;
|
||||||
|
pan.onEnd = (DragEndDetails details) {
|
||||||
|
didEndPan = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
final TestPointer pointer = TestPointer(2);
|
||||||
|
final PointerPanZoomStartEvent start = pointer.panZoomStart(const Offset(10.0, 10.0));
|
||||||
|
pan.addPointerPanZoom(start);
|
||||||
|
competingPan.addPointerPanZoom(start);
|
||||||
|
tester.closeArena(2);
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, isNull);
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
tester.route(start);
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, isNull);
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
// Gesture will be claimed when distance reaches kPanSlop, which was 36.0 when this test was last updated.
|
||||||
|
tester.route(pointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(20.0, 20.0))); // moved 20 horizontally and 20 vertically which is 28 total
|
||||||
|
expect(didStartPan, isFalse); // 28 < 36
|
||||||
|
tester.route(pointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(30.0, 30.0))); // moved 30 horizontally and 30 vertically which is 42 total
|
||||||
|
expect(didStartPan, isTrue); // 42 > 36
|
||||||
|
didStartPan = false;
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
tester.route(pointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(30.0, 25.0)));
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, const Offset(0.0, -5.0));
|
||||||
|
updatedScrollDelta = null;
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
tester.route(pointer.panZoomEnd());
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, isNull);
|
||||||
|
expect(didEndPan, isTrue);
|
||||||
|
didEndPan = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
testGesture('Pointer pan/zooms drags should allow touches to join them', (GestureTester tester) {
|
||||||
|
final PanGestureRecognizer pan = PanGestureRecognizer();
|
||||||
|
// We need a competing gesture recognizer so that the gesture is not immediately claimed.
|
||||||
|
final PanGestureRecognizer competingPan = PanGestureRecognizer();
|
||||||
|
addTearDown(pan.dispose);
|
||||||
|
addTearDown(competingPan.dispose);
|
||||||
|
|
||||||
|
bool didStartPan = false;
|
||||||
|
pan.onStart = (_) {
|
||||||
|
didStartPan = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
Offset? updatedScrollDelta;
|
||||||
|
pan.onUpdate = (DragUpdateDetails details) {
|
||||||
|
updatedScrollDelta = details.delta;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool didEndPan = false;
|
||||||
|
pan.onEnd = (DragEndDetails details) {
|
||||||
|
didEndPan = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
final TestPointer panZoomPointer = TestPointer(2);
|
||||||
|
final TestPointer touchPointer = TestPointer(3);
|
||||||
|
final PointerPanZoomStartEvent start = panZoomPointer.panZoomStart(const Offset(10.0, 10.0));
|
||||||
|
pan.addPointerPanZoom(start);
|
||||||
|
competingPan.addPointerPanZoom(start);
|
||||||
|
tester.closeArena(2);
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, isNull);
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
tester.route(start);
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, isNull);
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
// Gesture will be claimed when distance reaches kPanSlop, which was 36.0 when this test was last updated.
|
||||||
|
tester.route(panZoomPointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(20.0, 20.0))); // moved 20 horizontally and 20 vertically which is 28 total
|
||||||
|
expect(didStartPan, isFalse); // 28 < 36
|
||||||
|
tester.route(panZoomPointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(30.0, 30.0))); // moved 30 horizontally and 30 vertically which is 42 total
|
||||||
|
expect(didStartPan, isTrue); // 42 > 36
|
||||||
|
didStartPan = false;
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
tester.route(panZoomPointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(30.0, 25.0)));
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, const Offset(0.0, -5.0));
|
||||||
|
updatedScrollDelta = null;
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
final PointerDownEvent touchDown = touchPointer.down(const Offset(20.0, 20.0));
|
||||||
|
pan.addPointer(touchDown);
|
||||||
|
competingPan.addPointer(touchDown);
|
||||||
|
tester.closeArena(3);
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, isNull);
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
tester.route(touchPointer.move(const Offset(25.0, 25.0)));
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, const Offset(5.0, 5.0));
|
||||||
|
updatedScrollDelta = null;
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
tester.route(touchPointer.up());
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, isNull);
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
tester.route(panZoomPointer.panZoomEnd());
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, isNull);
|
||||||
|
expect(didEndPan, isTrue);
|
||||||
|
didEndPan = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
testGesture('Touch drags should allow pointer pan/zooms to join them', (GestureTester tester) {
|
||||||
|
final PanGestureRecognizer pan = PanGestureRecognizer();
|
||||||
|
// We need a competing gesture recognizer so that the gesture is not immediately claimed.
|
||||||
|
final PanGestureRecognizer competingPan = PanGestureRecognizer();
|
||||||
|
addTearDown(pan.dispose);
|
||||||
|
addTearDown(competingPan.dispose);
|
||||||
|
|
||||||
|
bool didStartPan = false;
|
||||||
|
pan.onStart = (_) {
|
||||||
|
didStartPan = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
Offset? updatedScrollDelta;
|
||||||
|
pan.onUpdate = (DragUpdateDetails details) {
|
||||||
|
updatedScrollDelta = details.delta;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool didEndPan = false;
|
||||||
|
pan.onEnd = (DragEndDetails details) {
|
||||||
|
didEndPan = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
final TestPointer panZoomPointer = TestPointer(2);
|
||||||
|
final TestPointer touchPointer = TestPointer(3);
|
||||||
|
final PointerDownEvent touchDown = touchPointer.down(const Offset(20.0, 20.0));
|
||||||
|
pan.addPointer(touchDown);
|
||||||
|
competingPan.addPointer(touchDown);
|
||||||
|
tester.closeArena(3);
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, isNull);
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
tester.route(touchPointer.move(const Offset(60.0, 60.0)));
|
||||||
|
expect(didStartPan, isTrue);
|
||||||
|
didStartPan = false;
|
||||||
|
expect(updatedScrollDelta, isNull);
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
tester.route(touchPointer.move(const Offset(70.0, 70.0)));
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, const Offset(10.0, 10.0));
|
||||||
|
updatedScrollDelta = null;
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
final PointerPanZoomStartEvent start = panZoomPointer.panZoomStart(const Offset(10.0, 10.0));
|
||||||
|
pan.addPointerPanZoom(start);
|
||||||
|
competingPan.addPointerPanZoom(start);
|
||||||
|
tester.closeArena(2);
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, isNull);
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
tester.route(start);
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, isNull);
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
// Gesture will be claimed when distance reaches kPanSlop, which was 36.0 when this test was last updated.
|
||||||
|
tester.route(panZoomPointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(20.0, 20.0))); // moved 20 horizontally and 20 vertically which is 28 total
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, const Offset(20.0, 20.0));
|
||||||
|
updatedScrollDelta = null;
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
tester.route(panZoomPointer.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(30.0, 30.0))); // moved 30 horizontally and 30 vertically which is 42 total
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, const Offset(10.0, 10.0));
|
||||||
|
updatedScrollDelta = null;
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
tester.route(panZoomPointer.panZoomEnd());
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, isNull);
|
||||||
|
expect(didEndPan, isFalse);
|
||||||
|
|
||||||
|
tester.route(touchPointer.up());
|
||||||
|
expect(didStartPan, isFalse);
|
||||||
|
expect(updatedScrollDelta, isNull);
|
||||||
|
expect(didEndPan, isTrue);
|
||||||
|
didEndPan = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -505,6 +505,39 @@ void main() {
|
|||||||
localPosition: localPosition,
|
localPosition: localPosition,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const PointerPanZoomStartEvent panZoomStart = PointerPanZoomStartEvent(
|
||||||
|
timeStamp: Duration(seconds: 2),
|
||||||
|
device: 1,
|
||||||
|
position: Offset(20, 30),
|
||||||
|
);
|
||||||
|
_expectTransformedEvent(
|
||||||
|
original: panZoomStart,
|
||||||
|
transform: transform,
|
||||||
|
localPosition: localPosition,
|
||||||
|
);
|
||||||
|
|
||||||
|
const PointerPanZoomUpdateEvent panZoomUpdate = PointerPanZoomUpdateEvent(
|
||||||
|
timeStamp: Duration(seconds: 2),
|
||||||
|
device: 1,
|
||||||
|
position: Offset(20, 30),
|
||||||
|
);
|
||||||
|
_expectTransformedEvent(
|
||||||
|
original: panZoomUpdate,
|
||||||
|
transform: transform,
|
||||||
|
localPosition: localPosition,
|
||||||
|
);
|
||||||
|
|
||||||
|
const PointerPanZoomEndEvent panZoomEnd = PointerPanZoomEndEvent(
|
||||||
|
timeStamp: Duration(seconds: 2),
|
||||||
|
device: 1,
|
||||||
|
position: Offset(20, 30),
|
||||||
|
);
|
||||||
|
_expectTransformedEvent(
|
||||||
|
original: panZoomEnd,
|
||||||
|
transform: transform,
|
||||||
|
localPosition: localPosition,
|
||||||
|
);
|
||||||
|
|
||||||
const PointerUpEvent up = PointerUpEvent(
|
const PointerUpEvent up = PointerUpEvent(
|
||||||
timeStamp: Duration(seconds: 2),
|
timeStamp: Duration(seconds: 2),
|
||||||
pointer: 45,
|
pointer: 45,
|
||||||
|
@ -335,4 +335,23 @@ void main() {
|
|||||||
expect(events[4].buttons, equals(0));
|
expect(events[4].buttons, equals(0));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Pointer pan/zoom events', () {
|
||||||
|
const ui.PointerDataPacket packet = ui.PointerDataPacket(
|
||||||
|
data: <ui.PointerData>[
|
||||||
|
ui.PointerData(change: ui.PointerChange.panZoomStart),
|
||||||
|
ui.PointerData(change: ui.PointerChange.panZoomUpdate),
|
||||||
|
ui.PointerData(change: ui.PointerChange.panZoomEnd),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<PointerEvent> events = <PointerEvent>[];
|
||||||
|
binding.callback = events.add;
|
||||||
|
|
||||||
|
ui.window.onPointerDataPacket?.call(packet);
|
||||||
|
expect(events.length, 3);
|
||||||
|
expect(events[0], isA<PointerPanZoomStartEvent>());
|
||||||
|
expect(events[1], isA<PointerPanZoomUpdateEvent>());
|
||||||
|
expect(events[2], isA<PointerPanZoomEndEvent>());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -370,9 +370,9 @@ void main() {
|
|||||||
tester.route(down);
|
tester.route(down);
|
||||||
expect(log, isEmpty);
|
expect(log, isEmpty);
|
||||||
|
|
||||||
// scale will win if focal point delta exceeds 18.0*2
|
// Scale will win if focal point delta exceeds 18.0*2.
|
||||||
|
|
||||||
tester.route(pointer1.move(const Offset(10.0, 50.0))); // delta of 40.0 exceeds 18.0*2
|
tester.route(pointer1.move(const Offset(10.0, 50.0))); // Delta of 40.0 exceeds 18.0*2.
|
||||||
expect(log, equals(<String>['scale-start', 'scale-update']));
|
expect(log, equals(<String>['scale-start', 'scale-update']));
|
||||||
log.clear();
|
log.clear();
|
||||||
|
|
||||||
@ -704,6 +704,461 @@ void main() {
|
|||||||
scale.dispose();
|
scale.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testGesture('Should recognize scale gestures from pointer pan/zoom events', (GestureTester tester) {
|
||||||
|
final ScaleGestureRecognizer scale = ScaleGestureRecognizer();
|
||||||
|
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
|
||||||
|
|
||||||
|
bool didStartScale = false;
|
||||||
|
Offset? updatedFocalPoint;
|
||||||
|
scale.onStart = (ScaleStartDetails details) {
|
||||||
|
didStartScale = true;
|
||||||
|
updatedFocalPoint = details.focalPoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
double? updatedScale;
|
||||||
|
double? updatedHorizontalScale;
|
||||||
|
double? updatedVerticalScale;
|
||||||
|
Offset? updatedDelta;
|
||||||
|
scale.onUpdate = (ScaleUpdateDetails details) {
|
||||||
|
updatedScale = details.scale;
|
||||||
|
updatedHorizontalScale = details.horizontalScale;
|
||||||
|
updatedVerticalScale = details.verticalScale;
|
||||||
|
updatedFocalPoint = details.focalPoint;
|
||||||
|
updatedDelta = details.focalPointDelta;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool didEndScale = false;
|
||||||
|
scale.onEnd = (ScaleEndDetails details) {
|
||||||
|
didEndScale = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
final TestPointer pointer1 = TestPointer(2);
|
||||||
|
|
||||||
|
final PointerPanZoomStartEvent start = pointer1.panZoomStart(Offset.zero);
|
||||||
|
scale.addPointerPanZoom(start);
|
||||||
|
drag.addPointerPanZoom(start);
|
||||||
|
|
||||||
|
tester.closeArena(2);
|
||||||
|
expect(didStartScale, isFalse);
|
||||||
|
expect(updatedScale, isNull);
|
||||||
|
expect(updatedFocalPoint, isNull);
|
||||||
|
expect(updatedDelta, isNull);
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
// Panning.
|
||||||
|
tester.route(start);
|
||||||
|
expect(didStartScale, isFalse);
|
||||||
|
expect(updatedScale, isNull);
|
||||||
|
expect(updatedFocalPoint, isNull);
|
||||||
|
expect(updatedDelta, isNull);
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(20.0, 30.0)));
|
||||||
|
expect(didStartScale, isTrue);
|
||||||
|
didStartScale = false;
|
||||||
|
expect(updatedFocalPoint, const Offset(20.0, 30.0));
|
||||||
|
updatedFocalPoint = null;
|
||||||
|
expect(updatedScale, 1.0);
|
||||||
|
updatedScale = null;
|
||||||
|
expect(updatedDelta, const Offset(20.0, 30.0));
|
||||||
|
updatedDelta = null;
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
// Zoom in.
|
||||||
|
tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(20.0, 30.0), scale: 2.0));
|
||||||
|
expect(updatedFocalPoint, const Offset(20.0, 30.0));
|
||||||
|
updatedFocalPoint = null;
|
||||||
|
expect(updatedScale, 2.0);
|
||||||
|
expect(updatedHorizontalScale, 2.0);
|
||||||
|
expect(updatedVerticalScale, 2.0);
|
||||||
|
expect(updatedDelta, Offset.zero);
|
||||||
|
updatedScale = null;
|
||||||
|
updatedHorizontalScale = null;
|
||||||
|
updatedVerticalScale = null;
|
||||||
|
updatedDelta = null;
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
// Zoom out.
|
||||||
|
tester.route(pointer1.panZoomUpdate(Offset.zero, pan: const Offset(20.0, 30.0)));
|
||||||
|
expect(updatedFocalPoint, const Offset(20.0, 30.0));
|
||||||
|
updatedFocalPoint = null;
|
||||||
|
expect(updatedScale, 1.0);
|
||||||
|
expect(updatedHorizontalScale, 1.0);
|
||||||
|
expect(updatedVerticalScale, 1.0);
|
||||||
|
expect(updatedDelta, Offset.zero);
|
||||||
|
updatedScale = null;
|
||||||
|
updatedHorizontalScale = null;
|
||||||
|
updatedVerticalScale = null;
|
||||||
|
updatedDelta = null;
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
// We are done.
|
||||||
|
tester.route(pointer1.panZoomEnd());
|
||||||
|
expect(didStartScale, isFalse);
|
||||||
|
expect(updatedFocalPoint, isNull);
|
||||||
|
expect(updatedScale, isNull);
|
||||||
|
expect(updatedDelta, isNull);
|
||||||
|
expect(didEndScale, isTrue);
|
||||||
|
didEndScale = false;
|
||||||
|
|
||||||
|
scale.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testGesture('Pointer pan/zooms should work alongside touches', (GestureTester tester) {
|
||||||
|
final ScaleGestureRecognizer scale = ScaleGestureRecognizer();
|
||||||
|
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
|
||||||
|
|
||||||
|
bool didStartScale = false;
|
||||||
|
Offset? updatedFocalPoint;
|
||||||
|
scale.onStart = (ScaleStartDetails details) {
|
||||||
|
didStartScale = true;
|
||||||
|
updatedFocalPoint = details.focalPoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
double? updatedScale;
|
||||||
|
double? updatedHorizontalScale;
|
||||||
|
double? updatedVerticalScale;
|
||||||
|
Offset? updatedDelta;
|
||||||
|
double? updatedRotation;
|
||||||
|
scale.onUpdate = (ScaleUpdateDetails details) {
|
||||||
|
updatedScale = details.scale;
|
||||||
|
updatedHorizontalScale = details.horizontalScale;
|
||||||
|
updatedVerticalScale = details.verticalScale;
|
||||||
|
updatedFocalPoint = details.focalPoint;
|
||||||
|
updatedDelta = details.focalPointDelta;
|
||||||
|
updatedRotation = details.rotation;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool didEndScale = false;
|
||||||
|
scale.onEnd = (ScaleEndDetails details) {
|
||||||
|
didEndScale = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
final TestPointer touchPointer1 = TestPointer(2);
|
||||||
|
final TestPointer touchPointer2 = TestPointer(3);
|
||||||
|
final TestPointer panZoomPointer = TestPointer(4);
|
||||||
|
|
||||||
|
final PointerPanZoomStartEvent panZoomStart = panZoomPointer.panZoomStart(Offset.zero);
|
||||||
|
scale.addPointerPanZoom(panZoomStart);
|
||||||
|
drag.addPointerPanZoom(panZoomStart);
|
||||||
|
|
||||||
|
tester.closeArena(4);
|
||||||
|
expect(didStartScale, isFalse);
|
||||||
|
expect(updatedScale, isNull);
|
||||||
|
expect(updatedFocalPoint, isNull);
|
||||||
|
expect(updatedDelta, isNull);
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
// Panning starting with trackpad.
|
||||||
|
tester.route(panZoomStart);
|
||||||
|
expect(didStartScale, isFalse);
|
||||||
|
expect(updatedScale, isNull);
|
||||||
|
expect(updatedFocalPoint, isNull);
|
||||||
|
expect(updatedDelta, isNull);
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
tester.route(panZoomPointer.panZoomUpdate(Offset.zero, pan: const Offset(40, 40)));
|
||||||
|
expect(didStartScale, isTrue);
|
||||||
|
didStartScale = false;
|
||||||
|
expect(updatedFocalPoint, const Offset(40.0, 40.0));
|
||||||
|
updatedFocalPoint = null;
|
||||||
|
expect(updatedScale, 1.0);
|
||||||
|
updatedScale = null;
|
||||||
|
expect(updatedDelta, const Offset(40.0, 40.0));
|
||||||
|
updatedDelta = null;
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
// Add a touch pointer.
|
||||||
|
final PointerDownEvent touchStart1 = touchPointer1.down(const Offset(40, 40));
|
||||||
|
scale.addPointer(touchStart1);
|
||||||
|
drag.addPointer(touchStart1);
|
||||||
|
tester.closeArena(2);
|
||||||
|
tester.route(touchStart1);
|
||||||
|
expect(didEndScale, isTrue);
|
||||||
|
didEndScale = false;
|
||||||
|
|
||||||
|
tester.route(touchPointer1.move(const Offset(10, 10)));
|
||||||
|
expect(didStartScale, isTrue);
|
||||||
|
didStartScale = false;
|
||||||
|
expect(updatedFocalPoint, const Offset(25, 25));
|
||||||
|
updatedFocalPoint = null;
|
||||||
|
// 1 down pointer + pointer pan/zoom should not scale, only pan.
|
||||||
|
expect(updatedScale, 1.0);
|
||||||
|
updatedScale = null;
|
||||||
|
expect(updatedDelta, const Offset(-15, -15));
|
||||||
|
updatedDelta = null;
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
// Add a second touch pointer.
|
||||||
|
final PointerDownEvent touchStart2 = touchPointer2.down(const Offset(10, 40));
|
||||||
|
scale.addPointer(touchStart2);
|
||||||
|
drag.addPointer(touchStart2);
|
||||||
|
tester.closeArena(3);
|
||||||
|
tester.route(touchStart2);
|
||||||
|
expect(didEndScale, isTrue);
|
||||||
|
didEndScale = false;
|
||||||
|
|
||||||
|
// Move the second pointer to cause pan, zoom, and rotation.
|
||||||
|
tester.route(touchPointer2.move(const Offset(40, 40)));
|
||||||
|
expect(didStartScale, isTrue);
|
||||||
|
didStartScale = false;
|
||||||
|
expect(updatedFocalPoint, const Offset(30, 30));
|
||||||
|
updatedFocalPoint = null;
|
||||||
|
expect(updatedScale, math.sqrt(2));
|
||||||
|
updatedScale = null;
|
||||||
|
expect(updatedHorizontalScale, 1.0);
|
||||||
|
updatedHorizontalScale = null;
|
||||||
|
expect(updatedVerticalScale, 1.0);
|
||||||
|
updatedVerticalScale = null;
|
||||||
|
expect(updatedDelta, const Offset(10, 0));
|
||||||
|
updatedDelta = null;
|
||||||
|
expect(updatedRotation, -math.pi / 4);
|
||||||
|
updatedRotation = null;
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
// Change the scale and angle of the pan/zoom to test combining.
|
||||||
|
// Scale should be multiplied together.
|
||||||
|
// Rotation angle should be added together.
|
||||||
|
tester.route(panZoomPointer.panZoomUpdate(Offset.zero, pan: const Offset(40, 40), scale: math.sqrt(2), rotation: math.pi / 3));
|
||||||
|
expect(didStartScale, isFalse);
|
||||||
|
expect(updatedFocalPoint, const Offset(30, 30));
|
||||||
|
updatedFocalPoint = null;
|
||||||
|
expect(updatedScale, closeTo(2, 0.0001));
|
||||||
|
updatedScale = null;
|
||||||
|
expect(updatedHorizontalScale, math.sqrt(2));
|
||||||
|
updatedHorizontalScale = null;
|
||||||
|
expect(updatedVerticalScale, math.sqrt(2));
|
||||||
|
updatedVerticalScale = null;
|
||||||
|
expect(updatedDelta, Offset.zero);
|
||||||
|
updatedDelta = null;
|
||||||
|
expect(updatedRotation, closeTo(math.pi / 12, 0.0001));
|
||||||
|
updatedRotation = null;
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
// Move the pan/zoom origin to test combining.
|
||||||
|
tester.route(panZoomPointer.panZoomUpdate(const Offset(15, 15), pan: const Offset(55, 55), scale: math.sqrt(2), rotation: math.pi / 3));
|
||||||
|
expect(didStartScale, isFalse);
|
||||||
|
expect(updatedFocalPoint, const Offset(40, 40));
|
||||||
|
updatedFocalPoint = null;
|
||||||
|
expect(updatedScale, closeTo(2, 0.0001));
|
||||||
|
updatedScale = null;
|
||||||
|
expect(updatedDelta, const Offset(10, 10));
|
||||||
|
updatedDelta = null;
|
||||||
|
expect(updatedRotation, closeTo(math.pi / 12, 0.0001));
|
||||||
|
updatedRotation = null;
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
// We are done.
|
||||||
|
tester.route(panZoomPointer.panZoomEnd());
|
||||||
|
expect(updatedFocalPoint, isNull);
|
||||||
|
expect(didEndScale, isTrue);
|
||||||
|
didEndScale = false;
|
||||||
|
expect(updatedScale, isNull);
|
||||||
|
expect(updatedDelta, isNull);
|
||||||
|
expect(didStartScale, isFalse);
|
||||||
|
tester.route(touchPointer1.up());
|
||||||
|
expect(updatedFocalPoint, isNull);
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
expect(updatedScale, isNull);
|
||||||
|
expect(updatedDelta, isNull);
|
||||||
|
expect(didStartScale, isFalse);
|
||||||
|
tester.route(touchPointer2.up());
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
expect(updatedFocalPoint, isNull);
|
||||||
|
expect(updatedScale, isNull);
|
||||||
|
expect(updatedDelta, isNull);
|
||||||
|
expect(didStartScale, isFalse);
|
||||||
|
|
||||||
|
scale.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testGesture('Scale gesture competes with drag for trackpad gesture', (GestureTester tester) {
|
||||||
|
final ScaleGestureRecognizer scale = ScaleGestureRecognizer();
|
||||||
|
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
|
||||||
|
|
||||||
|
final List<String> log = <String>[];
|
||||||
|
|
||||||
|
scale.onStart = (ScaleStartDetails details) { log.add('scale-start'); };
|
||||||
|
scale.onUpdate = (ScaleUpdateDetails details) { log.add('scale-update'); };
|
||||||
|
scale.onEnd = (ScaleEndDetails details) { log.add('scale-end'); };
|
||||||
|
|
||||||
|
drag.onStart = (DragStartDetails details) { log.add('drag-start'); };
|
||||||
|
drag.onEnd = (DragEndDetails details) { log.add('drag-end'); };
|
||||||
|
|
||||||
|
final TestPointer pointer1 = TestPointer(2);
|
||||||
|
|
||||||
|
final PointerPanZoomStartEvent down = pointer1.panZoomStart(const Offset(10.0, 10.0));
|
||||||
|
scale.addPointerPanZoom(down);
|
||||||
|
drag.addPointerPanZoom(down);
|
||||||
|
|
||||||
|
tester.closeArena(2);
|
||||||
|
expect(log, isEmpty);
|
||||||
|
|
||||||
|
// Vertical moves are scales.
|
||||||
|
tester.route(down);
|
||||||
|
expect(log, isEmpty);
|
||||||
|
|
||||||
|
// Scale will win if focal point delta exceeds 18.0*2.
|
||||||
|
|
||||||
|
tester.route(pointer1.panZoomUpdate(const Offset(10.0, 10.0), pan: const Offset(10.0, 40.0))); // delta of 40.0 exceeds 18.0*2.
|
||||||
|
expect(log, equals(<String>['scale-start', 'scale-update']));
|
||||||
|
log.clear();
|
||||||
|
|
||||||
|
final TestPointer pointer2 = TestPointer(3);
|
||||||
|
final PointerPanZoomStartEvent down2 = pointer2.panZoomStart(const Offset(10.0, 20.0));
|
||||||
|
scale.addPointerPanZoom(down2);
|
||||||
|
drag.addPointerPanZoom(down2);
|
||||||
|
|
||||||
|
tester.closeArena(3);
|
||||||
|
expect(log, isEmpty);
|
||||||
|
|
||||||
|
// Second pointer joins scale even though it moves horizontally.
|
||||||
|
tester.route(down2);
|
||||||
|
expect(log, <String>['scale-end']);
|
||||||
|
log.clear();
|
||||||
|
|
||||||
|
tester.route(pointer2.panZoomUpdate(const Offset(10.0, 20.0), pan: const Offset(20.0, 0.0)));
|
||||||
|
expect(log, equals(<String>['scale-start', 'scale-update']));
|
||||||
|
log.clear();
|
||||||
|
|
||||||
|
tester.route(pointer1.panZoomEnd());
|
||||||
|
expect(log, equals(<String>['scale-end']));
|
||||||
|
log.clear();
|
||||||
|
|
||||||
|
tester.route(pointer2.panZoomEnd());
|
||||||
|
expect(log, isEmpty);
|
||||||
|
log.clear();
|
||||||
|
|
||||||
|
// Horizontal moves are either drags or scales, depending on which wins first.
|
||||||
|
// TODO(ianh): https://github.com/flutter/flutter/issues/11384
|
||||||
|
// In this case, we move fast, so that the scale wins. If we moved slowly,
|
||||||
|
// the horizontal drag would win, since it was added first.
|
||||||
|
final TestPointer pointer3 = TestPointer(4);
|
||||||
|
final PointerPanZoomStartEvent down3 = pointer3.panZoomStart(const Offset(30.0, 30.0));
|
||||||
|
scale.addPointerPanZoom(down3);
|
||||||
|
drag.addPointerPanZoom(down3);
|
||||||
|
tester.closeArena(4);
|
||||||
|
tester.route(down3);
|
||||||
|
|
||||||
|
expect(log, isEmpty);
|
||||||
|
|
||||||
|
tester.route(pointer3.panZoomUpdate(const Offset(30.0, 30.0), pan: const Offset(70.0, 0.0)));
|
||||||
|
expect(log, equals(<String>['scale-start', 'scale-update']));
|
||||||
|
log.clear();
|
||||||
|
|
||||||
|
tester.route(pointer3.panZoomEnd());
|
||||||
|
expect(log, equals(<String>['scale-end']));
|
||||||
|
log.clear();
|
||||||
|
|
||||||
|
scale.dispose();
|
||||||
|
drag.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testGesture('Scale gesture from pan/zoom events properly handles DragStartBehavior.start', (GestureTester tester) {
|
||||||
|
final ScaleGestureRecognizer scale = ScaleGestureRecognizer(dragStartBehavior: DragStartBehavior.start);
|
||||||
|
final HorizontalDragGestureRecognizer drag = HorizontalDragGestureRecognizer();
|
||||||
|
|
||||||
|
bool didStartScale = false;
|
||||||
|
Offset? updatedFocalPoint;
|
||||||
|
scale.onStart = (ScaleStartDetails details) {
|
||||||
|
didStartScale = true;
|
||||||
|
updatedFocalPoint = details.focalPoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
double? updatedScale;
|
||||||
|
double? updatedHorizontalScale;
|
||||||
|
double? updatedVerticalScale;
|
||||||
|
double? updatedRotation;
|
||||||
|
Offset? updatedDelta;
|
||||||
|
scale.onUpdate = (ScaleUpdateDetails details) {
|
||||||
|
updatedScale = details.scale;
|
||||||
|
updatedHorizontalScale = details.horizontalScale;
|
||||||
|
updatedVerticalScale = details.verticalScale;
|
||||||
|
updatedFocalPoint = details.focalPoint;
|
||||||
|
updatedRotation = details.rotation;
|
||||||
|
updatedDelta = details.focalPointDelta;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool didEndScale = false;
|
||||||
|
scale.onEnd = (ScaleEndDetails details) {
|
||||||
|
didEndScale = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
final TestPointer pointer1 = TestPointer(2);
|
||||||
|
|
||||||
|
final PointerPanZoomStartEvent start = pointer1.panZoomStart(Offset.zero);
|
||||||
|
scale.addPointerPanZoom(start);
|
||||||
|
drag.addPointerPanZoom(start);
|
||||||
|
|
||||||
|
tester.closeArena(2);
|
||||||
|
expect(didStartScale, isFalse);
|
||||||
|
expect(updatedScale, isNull);
|
||||||
|
expect(updatedFocalPoint, isNull);
|
||||||
|
expect(updatedDelta, isNull);
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
tester.route(start);
|
||||||
|
expect(didStartScale, isFalse);
|
||||||
|
expect(updatedScale, isNull);
|
||||||
|
expect(updatedFocalPoint, isNull);
|
||||||
|
expect(updatedDelta, isNull);
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
// Zoom enough to win the gesture.
|
||||||
|
tester.route(pointer1.panZoomUpdate(Offset.zero, scale: 1.1, rotation: 1));
|
||||||
|
expect(didStartScale, isTrue);
|
||||||
|
didStartScale = false;
|
||||||
|
expect(updatedFocalPoint, Offset.zero);
|
||||||
|
updatedFocalPoint = null;
|
||||||
|
expect(updatedScale, 1.0);
|
||||||
|
updatedScale = null;
|
||||||
|
expect(updatedDelta, Offset.zero);
|
||||||
|
updatedDelta = null;
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
// Zoom in - should be relative to 1.1.
|
||||||
|
tester.route(pointer1.panZoomUpdate(Offset.zero, scale: 1.21, rotation: 1.5));
|
||||||
|
expect(updatedFocalPoint, Offset.zero);
|
||||||
|
updatedFocalPoint = null;
|
||||||
|
expect(updatedScale, closeTo(1.1, 0.0001));
|
||||||
|
expect(updatedHorizontalScale, closeTo(1.1, 0.0001));
|
||||||
|
expect(updatedVerticalScale, closeTo(1.1, 0.0001));
|
||||||
|
expect(updatedRotation, 0.5);
|
||||||
|
expect(updatedDelta, Offset.zero);
|
||||||
|
updatedScale = null;
|
||||||
|
updatedHorizontalScale = null;
|
||||||
|
updatedVerticalScale = null;
|
||||||
|
updatedRotation = null;
|
||||||
|
updatedDelta = null;
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
// Zoom out - should be relative to 1.1.
|
||||||
|
tester.route(pointer1.panZoomUpdate(Offset.zero, scale: 0.99, rotation: 1.0));
|
||||||
|
expect(updatedFocalPoint, Offset.zero);
|
||||||
|
updatedFocalPoint = null;
|
||||||
|
expect(updatedScale, closeTo(0.9, 0.0001));
|
||||||
|
expect(updatedHorizontalScale, closeTo(0.9, 0.0001));
|
||||||
|
expect(updatedVerticalScale, closeTo(0.9, 0.0001));
|
||||||
|
expect(updatedRotation, 0.0);
|
||||||
|
expect(updatedDelta, Offset.zero);
|
||||||
|
updatedScale = null;
|
||||||
|
updatedHorizontalScale = null;
|
||||||
|
updatedVerticalScale = null;
|
||||||
|
updatedDelta = null;
|
||||||
|
expect(didEndScale, isFalse);
|
||||||
|
|
||||||
|
// We are done.
|
||||||
|
tester.route(pointer1.panZoomEnd());
|
||||||
|
expect(didStartScale, isFalse);
|
||||||
|
expect(updatedFocalPoint, isNull);
|
||||||
|
expect(updatedScale, isNull);
|
||||||
|
expect(updatedDelta, isNull);
|
||||||
|
expect(didEndScale, isTrue);
|
||||||
|
didEndScale = false;
|
||||||
|
|
||||||
|
scale.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('ScaleGestureRecognizer asserts when kind and supportedDevices are both set', (WidgetTester tester) async {
|
testWidgets('ScaleGestureRecognizer asserts when kind and supportedDevices are both set', (WidgetTester tester) async {
|
||||||
expect(
|
expect(
|
||||||
() {
|
() {
|
||||||
|
@ -272,7 +272,7 @@ void main() {
|
|||||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||||
' │ size: Size(800.0, 600.0)\n'
|
' │ size: Size(800.0, 600.0)\n'
|
||||||
' │ behavior: opaque\n'
|
' │ behavior: opaque\n'
|
||||||
' │ listeners: down\n'
|
' │ listeners: down, panZoomStart\n'
|
||||||
' │\n'
|
' │\n'
|
||||||
' └─child: RenderSemanticsAnnotations#00000\n'
|
' └─child: RenderSemanticsAnnotations#00000\n'
|
||||||
' │ needs compositing\n'
|
' │ needs compositing\n'
|
||||||
@ -432,7 +432,7 @@ void main() {
|
|||||||
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
|
||||||
' │ size: Size(800.0, 600.0)\n'
|
' │ size: Size(800.0, 600.0)\n'
|
||||||
' │ behavior: opaque\n'
|
' │ behavior: opaque\n'
|
||||||
' │ listeners: down\n'
|
' │ listeners: down, panZoomStart\n'
|
||||||
' │\n'
|
' │\n'
|
||||||
' └─child: RenderSemanticsAnnotations#00000\n'
|
' └─child: RenderSemanticsAnnotations#00000\n'
|
||||||
' │ needs compositing\n'
|
' │ needs compositing\n'
|
||||||
|
@ -134,6 +134,7 @@ void main() {
|
|||||||
PointerDeviceKind.touch,
|
PointerDeviceKind.touch,
|
||||||
PointerDeviceKind.stylus,
|
PointerDeviceKind.stylus,
|
||||||
PointerDeviceKind.invertedStylus,
|
PointerDeviceKind.invertedStylus,
|
||||||
|
PointerDeviceKind.trackpad,
|
||||||
PointerDeviceKind.unknown,
|
PointerDeviceKind.unknown,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -33,9 +33,8 @@ class TestPointer {
|
|||||||
case PointerDeviceKind.stylus:
|
case PointerDeviceKind.stylus:
|
||||||
case PointerDeviceKind.invertedStylus:
|
case PointerDeviceKind.invertedStylus:
|
||||||
case PointerDeviceKind.touch:
|
case PointerDeviceKind.touch:
|
||||||
|
case PointerDeviceKind.trackpad:
|
||||||
case PointerDeviceKind.unknown:
|
case PointerDeviceKind.unknown:
|
||||||
default: // ignore: no_default_cases, to allow adding new device types to [PointerDeviceKind]
|
|
||||||
// TODO(moffatman): Remove after landing https://github.com/flutter/flutter/issues/23604
|
|
||||||
_device = device ?? 0;
|
_device = device ?? 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -70,12 +69,27 @@ class TestPointer {
|
|||||||
bool get isDown => _isDown;
|
bool get isDown => _isDown;
|
||||||
bool _isDown = false;
|
bool _isDown = false;
|
||||||
|
|
||||||
|
/// Whether the pointer simulated by this object currently has
|
||||||
|
/// an active pan/zoom gesture.
|
||||||
|
///
|
||||||
|
/// A pan/zoom gesture begins when [panZoomStart] is called, and
|
||||||
|
/// ends when [panZoomEnd] is called.
|
||||||
|
bool get isPanZoomActive => _isPanZoomActive;
|
||||||
|
bool _isPanZoomActive = false;
|
||||||
|
|
||||||
/// The position of the last event sent by this object.
|
/// The position of the last event sent by this object.
|
||||||
///
|
///
|
||||||
/// If no event has ever been sent by this object, returns null.
|
/// If no event has ever been sent by this object, returns null.
|
||||||
Offset? get location => _location;
|
Offset? get location => _location;
|
||||||
Offset? _location;
|
Offset? _location;
|
||||||
|
|
||||||
|
|
||||||
|
/// The pan offset of the last pointer pan/zoom event sent by this object.
|
||||||
|
///
|
||||||
|
/// If no pan/zoom event has ever been sent by this object, returns null.
|
||||||
|
Offset? get pan => _pan;
|
||||||
|
Offset? _pan;
|
||||||
|
|
||||||
/// If a custom event is created outside of this class, this function is used
|
/// If a custom event is created outside of this class, this function is used
|
||||||
/// to set the [isDown].
|
/// to set the [isDown].
|
||||||
bool setDownInfo(
|
bool setDownInfo(
|
||||||
@ -115,6 +129,7 @@ class TestPointer {
|
|||||||
int? buttons,
|
int? buttons,
|
||||||
}) {
|
}) {
|
||||||
assert(!isDown);
|
assert(!isDown);
|
||||||
|
assert(!isPanZoomActive);
|
||||||
_isDown = true;
|
_isDown = true;
|
||||||
_location = newLocation;
|
_location = newLocation;
|
||||||
if (buttons != null)
|
if (buttons != null)
|
||||||
@ -149,6 +164,7 @@ class TestPointer {
|
|||||||
'Move events can only be generated when the pointer is down. To '
|
'Move events can only be generated when the pointer is down. To '
|
||||||
'create a movement event simulating a pointer move when the pointer is '
|
'create a movement event simulating a pointer move when the pointer is '
|
||||||
'up, use hover() instead.');
|
'up, use hover() instead.');
|
||||||
|
assert(!isPanZoomActive);
|
||||||
final Offset delta = newLocation - location!;
|
final Offset delta = newLocation - location!;
|
||||||
_location = newLocation;
|
_location = newLocation;
|
||||||
if (buttons != null)
|
if (buttons != null)
|
||||||
@ -171,6 +187,7 @@ class TestPointer {
|
|||||||
///
|
///
|
||||||
/// The object is no longer usable after this method has been called.
|
/// The object is no longer usable after this method has been called.
|
||||||
PointerUpEvent up({ Duration timeStamp = Duration.zero }) {
|
PointerUpEvent up({ Duration timeStamp = Duration.zero }) {
|
||||||
|
assert(!isPanZoomActive);
|
||||||
assert(isDown);
|
assert(isDown);
|
||||||
_isDown = false;
|
_isDown = false;
|
||||||
return PointerUpEvent(
|
return PointerUpEvent(
|
||||||
@ -283,6 +300,79 @@ class TestPointer {
|
|||||||
scrollDelta: scrollDelta,
|
scrollDelta: scrollDelta,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a [PointerPanZoomStartEvent] (e.g., trackpad scroll; not scroll wheel
|
||||||
|
/// or finger-drag scroll) with the given delta.
|
||||||
|
///
|
||||||
|
/// By default, the time stamp on the event is [Duration.zero]. You can give a
|
||||||
|
/// specific time stamp by passing the `timeStamp` argument.
|
||||||
|
PointerPanZoomStartEvent panZoomStart(
|
||||||
|
Offset location, {
|
||||||
|
Duration timeStamp = Duration.zero
|
||||||
|
}) {
|
||||||
|
assert(!isPanZoomActive);
|
||||||
|
_location = location;
|
||||||
|
_pan = Offset.zero;
|
||||||
|
_isPanZoomActive = true;
|
||||||
|
return PointerPanZoomStartEvent(
|
||||||
|
timeStamp: timeStamp,
|
||||||
|
kind: kind,
|
||||||
|
device: _device,
|
||||||
|
pointer: pointer,
|
||||||
|
position: location,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a [PointerPanZoomUpdateEvent] to update the active pan/zoom sequence
|
||||||
|
/// on this pointer with updated pan, scale, and/or rotation values.
|
||||||
|
///
|
||||||
|
/// [rotation] is in units of radians.
|
||||||
|
///
|
||||||
|
/// By default, the time stamp on the event is [Duration.zero]. You can give a
|
||||||
|
/// specific time stamp by passing the `timeStamp` argument.
|
||||||
|
PointerPanZoomUpdateEvent panZoomUpdate(
|
||||||
|
Offset location, {
|
||||||
|
Offset pan = Offset.zero,
|
||||||
|
double scale = 1,
|
||||||
|
double rotation = 0,
|
||||||
|
Duration timeStamp = Duration.zero,
|
||||||
|
}) {
|
||||||
|
assert(isPanZoomActive);
|
||||||
|
_location = location;
|
||||||
|
final Offset panDelta = pan - _pan!;
|
||||||
|
_pan = pan;
|
||||||
|
return PointerPanZoomUpdateEvent(
|
||||||
|
timeStamp: timeStamp,
|
||||||
|
kind: kind,
|
||||||
|
device: _device,
|
||||||
|
pointer: pointer,
|
||||||
|
position: location,
|
||||||
|
pan: pan,
|
||||||
|
panDelta: panDelta,
|
||||||
|
scale: scale,
|
||||||
|
rotation: rotation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a [PointerPanZoomEndEvent] to end the active pan/zoom sequence
|
||||||
|
/// on this pointer.
|
||||||
|
///
|
||||||
|
/// By default, the time stamp on the event is [Duration.zero]. You can give a
|
||||||
|
/// specific time stamp by passing the `timeStamp` argument.
|
||||||
|
PointerPanZoomEndEvent panZoomEnd({
|
||||||
|
Duration timeStamp = Duration.zero
|
||||||
|
}) {
|
||||||
|
assert(isPanZoomActive);
|
||||||
|
_isPanZoomActive = false;
|
||||||
|
_pan = null;
|
||||||
|
return PointerPanZoomEndEvent(
|
||||||
|
timeStamp: timeStamp,
|
||||||
|
kind: kind,
|
||||||
|
device: _device,
|
||||||
|
pointer: pointer,
|
||||||
|
position: location!,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signature for a callback that can dispatch events and returns a future that
|
/// Signature for a callback that can dispatch events and returns a future that
|
||||||
|
@ -45,5 +45,7 @@
|
|||||||
<false/>
|
<false/>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -45,5 +45,7 @@
|
|||||||
<false/>
|
<false/>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
@ -1383,6 +1383,8 @@ void main() {
|
|||||||
expect(plistFile, exists);
|
expect(plistFile, exists);
|
||||||
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
|
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
|
||||||
expect(disabled, isTrue);
|
expect(disabled, isTrue);
|
||||||
|
final bool indirectInput = _getBooleanValueFromPlist(plistFile: plistFile, key: 'UIApplicationSupportsIndirectInputEvents');
|
||||||
|
expect(indirectInput, isTrue);
|
||||||
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
|
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
|
||||||
expect(displayName, 'My Project');
|
expect(displayName, 'My Project');
|
||||||
});
|
});
|
||||||
@ -1400,6 +1402,8 @@ void main() {
|
|||||||
expect(plistFile, exists);
|
expect(plistFile, exists);
|
||||||
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
|
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
|
||||||
expect(disabled, isTrue);
|
expect(disabled, isTrue);
|
||||||
|
final bool indirectInput = _getBooleanValueFromPlist(plistFile: plistFile, key: 'UIApplicationSupportsIndirectInputEvents');
|
||||||
|
expect(indirectInput, isTrue);
|
||||||
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
|
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
|
||||||
expect(displayName, 'My Project');
|
expect(displayName, 'My Project');
|
||||||
});
|
});
|
||||||
@ -1417,6 +1421,8 @@ void main() {
|
|||||||
expect(plistFile, exists);
|
expect(plistFile, exists);
|
||||||
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
|
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
|
||||||
expect(disabled, isTrue);
|
expect(disabled, isTrue);
|
||||||
|
final bool indirectInput = _getBooleanValueFromPlist(plistFile: plistFile, key: 'UIApplicationSupportsIndirectInputEvents');
|
||||||
|
expect(indirectInput, isTrue);
|
||||||
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
|
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
|
||||||
expect(displayName, 'My Project');
|
expect(displayName, 'My Project');
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
@ -1443,6 +1449,8 @@ void main() {
|
|||||||
expect(plistFile, exists);
|
expect(plistFile, exists);
|
||||||
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
|
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
|
||||||
expect(disabled, isTrue);
|
expect(disabled, isTrue);
|
||||||
|
final bool indirectInput = _getBooleanValueFromPlist(plistFile: plistFile, key: 'UIApplicationSupportsIndirectInputEvents');
|
||||||
|
expect(indirectInput, isTrue);
|
||||||
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
|
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
|
||||||
expect(displayName, 'My Project');
|
expect(displayName, 'My Project');
|
||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
@ -1469,6 +1477,8 @@ void main() {
|
|||||||
expect(plistFile, exists);
|
expect(plistFile, exists);
|
||||||
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
|
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
|
||||||
expect(disabled, isTrue);
|
expect(disabled, isTrue);
|
||||||
|
final bool indirectInput = _getBooleanValueFromPlist(plistFile: plistFile, key: 'UIApplicationSupportsIndirectInputEvents');
|
||||||
|
expect(indirectInput, isTrue);
|
||||||
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
|
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
|
||||||
expect(displayName, 'My Project');
|
expect(displayName, 'My Project');
|
||||||
});
|
});
|
||||||
@ -1486,6 +1496,8 @@ void main() {
|
|||||||
expect(plistFile, exists);
|
expect(plistFile, exists);
|
||||||
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
|
final bool disabled = _getBooleanValueFromPlist(plistFile: plistFile, key: 'CADisableMinimumFrameDurationOnPhone');
|
||||||
expect(disabled, isTrue);
|
expect(disabled, isTrue);
|
||||||
|
final bool indirectInput = _getBooleanValueFromPlist(plistFile: plistFile, key: 'UIApplicationSupportsIndirectInputEvents');
|
||||||
|
expect(indirectInput, isTrue);
|
||||||
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
|
final String displayName = _getStringValueFromPlist(plistFile: plistFile, key: 'CFBundleDisplayName');
|
||||||
expect(displayName, 'My Project');
|
expect(displayName, 'My Project');
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user