diff --git a/packages/flutter/lib/src/widgets/interactive_viewer.dart b/packages/flutter/lib/src/widgets/interactive_viewer.dart index 6f7eb58497..8d7b008fc8 100644 --- a/packages/flutter/lib/src/widgets/interactive_viewer.dart +++ b/packages/flutter/lib/src/widgets/interactive_viewer.dart @@ -930,12 +930,15 @@ class _InteractiveViewerState extends State with TickerProvid widget.onInteractionEnd?.call(ScaleEndDetails()); return; } - final RenderBox childRenderBox = _childKey.currentContext!.findRenderObject()! as RenderBox; - final Size childSize = childRenderBox.size; - final double scaleChange = 1.0 - event.scrollDelta.dy / childSize.height; - if (scaleChange == 0.0) { + + // Ignore left and right scroll. + if (event.scrollDelta.dy == 0.0) { return; } + + // In the Flutter engine, the mousewheel scrollDelta is hardcoded to 20 per scroll, while a trackpad scroll can be any amount. + // The calculation for scaleChange here was arbitrarily chosen to feel natural for both trackpads and mousewheels on all platforms. + final double scaleChange = math.exp(-event.scrollDelta.dy / 200); final Offset focalPointScene = _transformationController!.toScene( event.localPosition, ); diff --git a/packages/flutter/test/widgets/interactive_viewer_test.dart b/packages/flutter/test/widgets/interactive_viewer_test.dart index 2fa5f699b4..7fa937d1de 100644 --- a/packages/flutter/test/widgets/interactive_viewer_test.dart +++ b/packages/flutter/test/widgets/interactive_viewer_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:math' as math; + import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -673,6 +675,37 @@ void main() { expect(scenePoint, const Offset(100, 100)); }); + testWidgets('Scaling amount is equal forth and back with a mouse scroll', (WidgetTester tester) async { + final TransformationController transformationController = TransformationController(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: InteractiveViewer( + constrained: false, + maxScale: 100000, + minScale: 0.01, + transformationController: transformationController, + child: Container(width: 1000.0, height: 1000.0), + ), + )), + ), + ); + + final Offset center = tester.getCenter(find.byType(InteractiveViewer)); + await scrollAt(center, tester, const Offset(0.0, -200.0)); + await tester.pumpAndSettle(); + expect(transformationController.value.getMaxScaleOnAxis(), math.exp(200 / 200)); + await scrollAt(center, tester, const Offset(0.0, -200.0)); + await tester.pumpAndSettle(); + // math.exp round the number too short compared to the one in transformationController. + expect(transformationController.value.getMaxScaleOnAxis(), closeTo(math.exp(400 / 200), 0.000000000000001)); + await scrollAt(center, tester, const Offset(0.0, 200.0)); + await scrollAt(center, tester, const Offset(0.0, 200.0)); + await tester.pumpAndSettle(); + expect(transformationController.value.getMaxScaleOnAxis(), 1.0); + }); + testWidgets('onInteraction can be used to get scene point', (WidgetTester tester) async{ final TransformationController transformationController = TransformationController(); late Offset focalPoint;