diff --git a/packages/flutter/lib/src/gestures/events.dart b/packages/flutter/lib/src/gestures/events.dart index 373a94094472..1db53193269e 100644 --- a/packages/flutter/lib/src/gestures/events.dart +++ b/packages/flutter/lib/src/gestures/events.dart @@ -1788,8 +1788,7 @@ class PointerScrollEvent extends PointerSignalEvent with _PointerEventDescriptio assert(kind != null), assert(device != null), assert(position != null), - assert(scrollDelta != null), - assert(!identical(kind, PointerDeviceKind.trackpad)); + assert(scrollDelta != null); @override final Offset scrollDelta; diff --git a/packages/flutter/lib/src/widgets/interactive_viewer.dart b/packages/flutter/lib/src/widgets/interactive_viewer.dart index 005bb991609e..f4bf64efd430 100644 --- a/packages/flutter/lib/src/widgets/interactive_viewer.dart +++ b/packages/flutter/lib/src/widgets/interactive_viewer.dart @@ -954,11 +954,57 @@ class _InteractiveViewerState extends State with TickerProvid _controller.forward(); } - // Handle mousewheel scroll events. + // Handle mousewheel and web trackpad scroll events. void _receivedPointerSignal(PointerSignalEvent event) { final double scaleChange; if (event is PointerScrollEvent) { - // Ignore left and right scroll. + if (event.kind == PointerDeviceKind.trackpad) { + // Trackpad scroll, so treat it as a pan. + widget.onInteractionStart?.call( + ScaleStartDetails( + focalPoint: event.position, + localFocalPoint: event.localPosition, + ), + ); + + final Offset localDelta = PointerEvent.transformDeltaViaPositions( + untransformedEndPosition: event.position + event.scrollDelta, + untransformedDelta: event.scrollDelta, + transform: event.transform, + ); + + if (!_gestureIsSupported(_GestureType.pan)) { + widget.onInteractionUpdate?.call(ScaleUpdateDetails( + focalPoint: event.position - event.scrollDelta, + localFocalPoint: event.localPosition - event.scrollDelta, + focalPointDelta: -localDelta, + )); + widget.onInteractionEnd?.call(ScaleEndDetails()); + return; + } + + final Offset focalPointScene = _transformationController!.toScene( + event.localPosition, + ); + + final Offset newFocalPointScene = _transformationController!.toScene( + event.localPosition - localDelta, + ); + + _transformationController!.value = _matrixTranslate( + _transformationController!.value, + newFocalPointScene - focalPointScene + ); + + widget.onInteractionUpdate?.call(ScaleUpdateDetails( + focalPoint: event.position - event.scrollDelta, + localFocalPoint: event.localPosition - localDelta, + focalPointDelta: -localDelta + )); + widget.onInteractionEnd?.call(ScaleEndDetails()); + return; + } + // Ignore left and right mouse wheel scroll. if (event.scrollDelta.dy == 0.0) { return; } diff --git a/packages/flutter/test/gestures/events_test.dart b/packages/flutter/test/gestures/events_test.dart index 6cbd72e717f0..21f8bbc3150d 100644 --- a/packages/flutter/test/gestures/events_test.dart +++ b/packages/flutter/test/gestures/events_test.dart @@ -850,12 +850,15 @@ void main() { expect(const PointerHoverEvent(kind: PointerDeviceKind.trackpad), isNotNull); // Regression test for https://github.com/flutter/flutter/issues/108176 expect(const PointerScrollInertiaCancelEvent(kind: PointerDeviceKind.trackpad), isNotNull); + + expect(const PointerScrollEvent(kind: PointerDeviceKind.trackpad), isNotNull); // The test passes if it compiles. }); test('Ensure certain event types are not allowed', () { expect(() => PointerDownEvent(kind: PointerDeviceKind.trackpad), throwsAssertionError); - expect(() => PointerScrollEvent(kind: PointerDeviceKind.trackpad), throwsAssertionError); + expect(() => PointerMoveEvent(kind: PointerDeviceKind.trackpad), throwsAssertionError); + expect(() => PointerUpEvent(kind: PointerDeviceKind.trackpad), throwsAssertionError); }); } diff --git a/packages/flutter/test/widgets/interactive_viewer_test.dart b/packages/flutter/test/widgets/interactive_viewer_test.dart index db8a2e70fc72..61bad52f2bc3 100644 --- a/packages/flutter/test/widgets/interactive_viewer_test.dart +++ b/packages/flutter/test/widgets/interactive_viewer_test.dart @@ -1722,6 +1722,50 @@ void main() { expect(translation2.y, lessThan(translation1.y)); }); + testWidgets('discrete scroll pointer events', (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); + Vector3 translation = transformationController.value.getTranslation(); + expect(translation.x, 0); + expect(translation.y, 0); + + // Send a mouse scroll event, it should cause a scale. + final TestPointer mouse = TestPointer(1, PointerDeviceKind.mouse); + await tester.sendEventToBinding(mouse.hover(tester.getCenter(find.byType(SizedBox)))); + await tester.sendEventToBinding(mouse.scroll(const Offset(300, -200))); + await tester.pump(); + expect(transformationController.value.getMaxScaleOnAxis(), 2.5); + translation = transformationController.value.getTranslation(); + // Will be translated to maintain centering. + expect(translation.x, -150); + expect(translation.y, -150); + + // Send a trackpad scroll event, it should cause a pan and no scale. + final TestPointer trackpad = TestPointer(1, PointerDeviceKind.trackpad); + await tester.sendEventToBinding(trackpad.hover(tester.getCenter(find.byType(SizedBox)))); + await tester.sendEventToBinding(trackpad.scroll(const Offset(100, -25))); + await tester.pump(); + expect(transformationController.value.getMaxScaleOnAxis(), 2.5); + translation = transformationController.value.getTranslation(); + expect(translation.x, -250); + expect(translation.y, -125); + }); + testWidgets('discrete scale pointer event', (WidgetTester tester) async { final TransformationController transformationController = TransformationController(); const double boundaryMargin = 50.0;