Unverified Commit 850f3b37 authored by Callum Moffat's avatar Callum Moffat Committed by GitHub

Add PointerScaleEvent and use in InteractiveViewer (#112172)

Enables pinch-to-zoom in InteractiveViewer on web via PointerScaleEvent.
parent b82cf76f
......@@ -267,9 +267,16 @@ class PointerEventConverter {
position: position,
embedderId: datum.embedderId,
);
case ui.PointerSignalKind.scale:
return PointerScaleEvent(
timeStamp: timeStamp,
kind: kind,
device: datum.device,
position: position,
embedderId: datum.embedderId,
scale: datum.scale,
);
case ui.PointerSignalKind.unknown:
default: // ignore: no_default_cases, to allow adding new [PointerSignalKind]
// TODO(moffatman): Remove after landing https://github.com/flutter/engine/pull/36342
// This branch should already have 'unknown' filtered out, but
// we don't want to return anything or miss if someone adds a new
// enumeration to PointerSignalKind.
......
......@@ -1917,6 +1917,105 @@ class _TransformedPointerScrollInertiaCancelEvent extends _TransformedPointerEve
PointerScrollInertiaCancelEvent transformed(Matrix4? transform) => original.transformed(transform);
}
mixin _CopyPointerScaleEvent on PointerEvent {
/// The scale (zoom factor) of the event.
double get scale;
@override
PointerScaleEvent 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,
double? scale,
}) {
return PointerScaleEvent(
timeStamp: timeStamp ?? this.timeStamp,
kind: kind ?? this.kind,
device: device ?? this.device,
position: position ?? this.position,
embedderId: embedderId ?? this.embedderId,
scale: scale ?? this.scale,
).transformed(transform);
}
}
/// The pointer issued a scale event.
///
/// Pinching-to-zoom in the browser is an example of an event
/// that would create a [PointerScaleEvent].
///
/// See also:
///
/// * [Listener.onPointerSignal], which allows callers to be notified of these
/// events in a widget tree.
/// * [PointerSignalResolver], which provides an opt-in mechanism whereby
/// participating agents may disambiguate an event's target.
class PointerScaleEvent extends PointerSignalEvent with _PointerEventDescription, _CopyPointerScaleEvent {
/// Creates a pointer scale event.
///
/// All of the arguments must be non-null.
const PointerScaleEvent({
super.timeStamp,
super.kind,
super.device,
super.position,
super.embedderId,
this.scale = 1.0,
}) : assert(timeStamp != null),
assert(kind != null),
assert(device != null),
assert(position != null),
assert(embedderId != null),
assert(scale != null);
@override
final double scale;
@override
PointerScaleEvent transformed(Matrix4? transform) {
if (transform == null || transform == this.transform) {
return this;
}
return _TransformedPointerScaleEvent(original as PointerScaleEvent? ?? this, transform);
}
}
class _TransformedPointerScaleEvent extends _TransformedPointerEvent with _CopyPointerScaleEvent implements PointerScaleEvent {
_TransformedPointerScaleEvent(this.original, this.transform)
: assert(original != null), assert(transform != null);
@override
final PointerScaleEvent original;
@override
final Matrix4 transform;
@override
double get scale => original.scale;
@override
PointerScaleEvent transformed(Matrix4? transform) => original.transformed(transform);
}
mixin _CopyPointerPanZoomStartEvent on PointerEvent {
@override
PointerPanZoomStartEvent copyWith({
......
......@@ -951,55 +951,62 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
// Handle mousewheel scroll events.
void _receivedPointerSignal(PointerSignalEvent event) {
final double scaleChange;
if (event is PointerScrollEvent) {
// Ignore left and right scroll.
if (event.scrollDelta.dy == 0.0) {
return;
}
widget.onInteractionStart?.call(
ScaleStartDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
),
);
final double scaleChange = math.exp(-event.scrollDelta.dy / widget.scaleFactor);
if (!_gestureIsSupported(_GestureType.scale)) {
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
scale: scaleChange,
));
widget.onInteractionEnd?.call(ScaleEndDetails());
return;
}
final Offset focalPointScene = _transformationController!.toScene(
event.localPosition,
);
_transformationController!.value = _matrixScale(
_transformationController!.value,
scaleChange,
);
// After scaling, translate such that the event's position is at the
// same scene point before and after the scale.
final Offset focalPointSceneScaled = _transformationController!.toScene(
event.localPosition,
);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
focalPointSceneScaled - focalPointScene,
);
scaleChange = math.exp(-event.scrollDelta.dy / widget.scaleFactor);
}
else if (event is PointerScaleEvent) {
scaleChange = event.scale;
}
else {
return;
}
widget.onInteractionStart?.call(
ScaleStartDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
),
);
if (!_gestureIsSupported(_GestureType.scale)) {
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
scale: scaleChange,
));
widget.onInteractionEnd?.call(ScaleEndDetails());
return;
}
final Offset focalPointScene = _transformationController!.toScene(
event.localPosition,
);
_transformationController!.value = _matrixScale(
_transformationController!.value,
scaleChange,
);
// After scaling, translate such that the event's position is at the
// same scene point before and after the scale.
final Offset focalPointSceneScaled = _transformationController!.toScene(
event.localPosition,
);
_transformationController!.value = _matrixTranslate(
_transformationController!.value,
focalPointSceneScaled - focalPointScene,
);
widget.onInteractionUpdate?.call(ScaleUpdateDetails(
focalPoint: event.position,
localFocalPoint: event.localPosition,
scale: scaleChange,
));
widget.onInteractionEnd?.call(ScaleEndDetails());
}
// Handle inertia drag animation.
......
......@@ -1705,6 +1705,43 @@ void main() {
// so the translation comes to a stop more quickly.
expect(translation2.y, lessThan(translation1.y));
});
testWidgets('discrete scale pointer event', (WidgetTester tester) async {
final TransformationController transformationController = TransformationController();
const double boundaryMargin = 50.0;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: InteractiveViewer(
boundaryMargin: const EdgeInsets.all(boundaryMargin),
transformationController: transformationController,
child: const SizedBox(width: 200.0, height: 200.0),
),
),
),
),
);
expect(transformationController.value.getMaxScaleOnAxis(), 1.0);
// Send a scale event.
final TestPointer pointer = TestPointer(1, PointerDeviceKind.trackpad);
await tester.sendEventToBinding(pointer.hover(tester.getCenter(find.byType(SizedBox))));
await tester.sendEventToBinding(pointer.scale(1.5));
await tester.pump();
expect(transformationController.value.getMaxScaleOnAxis(), 1.5);
// Send another scale event.
await tester.sendEventToBinding(pointer.scale(1.5));
await tester.pump();
expect(transformationController.value.getMaxScaleOnAxis(), 2.25);
// Send another scale event.
await tester.sendEventToBinding(pointer.scale(1.5));
await tester.pump();
expect(transformationController.value.getMaxScaleOnAxis(), 2.5); // capped at maxScale (2.5)
});
});
group('getNearestPointOnLine', () {
......
......@@ -321,6 +321,25 @@ class TestPointer {
);
}
/// Create a [PointerScaleEvent] (e.g., legacy pinch-to-zoom).
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
PointerScaleEvent scale(
double scale, {
Duration timeStamp = Duration.zero,
}) {
assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate pointer signal events");
assert(location != null);
return PointerScaleEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
position: location!,
scale: scale,
);
}
/// Create a [PointerPanZoomStartEvent] (e.g., trackpad scroll; not scroll wheel
/// or finger-drag scroll) with the given delta.
///
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment