diff --git a/packages/flutter/lib/src/widgets/interactive_viewer.dart b/packages/flutter/lib/src/widgets/interactive_viewer.dart index cb16db1566..31c0346365 100644 --- a/packages/flutter/lib/src/widgets/interactive_viewer.dart +++ b/packages/flutter/lib/src/widgets/interactive_viewer.dart @@ -220,6 +220,9 @@ class InteractiveViewer extends StatefulWidget { /// Called when the user ends a pan or scale gesture on the widget. /// + /// At the time this is called, the [TransformationController] will have + /// already been updated to reflect the change caused by the interaction. + /// /// {@template flutter.widgets.interactiveViewer.onInteraction} /// Will be called even if the interaction is disabled with /// [panEnabled] or [scaleEnabled]. @@ -229,10 +232,6 @@ class InteractiveViewer extends StatefulWidget { /// [GestureDetector.onScaleEnd]. Use [onInteractionStart], /// [onInteractionUpdate], and [onInteractionEnd] to respond to those /// gestures. - /// - /// The coordinates returned in the details are viewport coordinates relative - /// to the parent. See [TransformationController.toScene] for how to - /// convert the coordinates to scene coordinates relative to the child. /// {@endtemplate} /// /// See also: @@ -243,8 +242,17 @@ class InteractiveViewer extends StatefulWidget { /// Called when the user begins a pan or scale gesture on the widget. /// + /// At the time this is called, the [TransformationController] will not have + /// changed due to this interaction. + /// /// {@macro flutter.widgets.interactiveViewer.onInteraction} /// + /// The coordinates provided in the details' `focalPoint` and + /// `localFocalPoint` are normal Flutter event coordinates, not + /// InteractiveViewer scene coordinates. See + /// [TransformationController.toScene] for how to convert these coordinates to + /// scene coordinates relative to the child. + /// /// See also: /// /// * [onInteractionUpdate], which handles an update to the same interaction. @@ -253,8 +261,17 @@ class InteractiveViewer extends StatefulWidget { /// Called when the user updates a pan or scale gesture on the widget. /// + /// At the time this is called, the [TransformationController] will have + /// already been updated to reflect the change caused by the interaction. + /// /// {@macro flutter.widgets.interactiveViewer.onInteraction} /// + /// The coordinates provided in the details' `focalPoint` and + /// `localFocalPoint` are normal Flutter event coordinates, not + /// InteractiveViewer scene coordinates. See + /// [TransformationController.toScene] for how to convert these coordinates to + /// scene coordinates relative to the child. + /// /// See also: /// /// * [onInteractionStart], which handles the start of the same interaction. @@ -711,9 +728,7 @@ class _InteractiveViewerState extends State with TickerProvid // Handle the start of a gesture. All of pan, scale, and rotate are handled // with GestureDetector's scale gesture. void _onScaleStart(ScaleStartDetails details) { - if (widget.onInteractionStart != null) { - widget.onInteractionStart!(details); - } + widget.onInteractionStart?.call(details); if (_controller.isAnimating) { _controller.stop(); @@ -735,16 +750,6 @@ class _InteractiveViewerState extends State with TickerProvid // handled with GestureDetector's scale gesture. void _onScaleUpdate(ScaleUpdateDetails details) { final double scale = _transformationController!.value.getMaxScaleOnAxis(); - if (widget.onInteractionUpdate != null) { - widget.onInteractionUpdate!(ScaleUpdateDetails( - focalPoint: _transformationController!.toScene( - details.localFocalPoint, - ), - scale: details.scale, - rotation: details.rotation, - )); - } - final Offset focalPointScene = _transformationController!.toScene( details.localFocalPoint, ); @@ -798,7 +803,7 @@ class _InteractiveViewerState extends State with TickerProvid if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) { _referenceFocalPoint = focalPointSceneCheck; } - return; + break; case _GestureType.rotate: if (details.rotation == 0.0) { @@ -811,7 +816,7 @@ class _InteractiveViewerState extends State with TickerProvid details.localFocalPoint, ); _currentRotation = desiredRotation; - return; + break; case _GestureType.pan: assert(_referenceFocalPoint != null); @@ -832,16 +837,20 @@ class _InteractiveViewerState extends State with TickerProvid _referenceFocalPoint = _transformationController!.toScene( details.localFocalPoint, ); - return; + break; } + widget.onInteractionUpdate?.call(ScaleUpdateDetails( + focalPoint: details.focalPoint, + localFocalPoint: details.localFocalPoint, + scale: details.scale, + rotation: details.rotation, + )); } // Handle the end of a gesture of _GestureType. All of pan, scale, and rotate // are handled with GestureDetector's scale gesture. void _onScaleEnd(ScaleEndDetails details) { - if (widget.onInteractionEnd != null) { - widget.onInteractionEnd!(details); - } + widget.onInteractionEnd?.call(details); _scaleStart = null; _rotationStart = null; _referenceFocalPoint = null; @@ -890,10 +899,17 @@ class _InteractiveViewerState extends State with TickerProvid // Handle mousewheel scroll events. void _receivedPointerSignal(PointerSignalEvent event) { - if (!_gestureIsSupported(_GestureType.scale)) { - return; - } if (event is PointerScrollEvent) { + widget.onInteractionStart?.call( + ScaleStartDetails( + focalPoint: event.position, + localFocalPoint: event.localPosition, + ), + ); + if (!_gestureIsSupported(_GestureType.scale)) { + 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; @@ -903,6 +919,7 @@ class _InteractiveViewerState extends State with TickerProvid final Offset focalPointScene = _transformationController!.toScene( event.localPosition, ); + _transformationController!.value = _matrixScale( _transformationController!.value, scaleChange, @@ -917,22 +934,16 @@ class _InteractiveViewerState extends State with TickerProvid _transformationController!.value, focalPointSceneScaled - focalPointScene, ); - if (widget.onInteractionStart != null) { - widget.onInteractionStart!( - ScaleStartDetails(focalPoint: focalPointSceneScaled) - ); - } - if (widget.onInteractionUpdate != null) { - widget.onInteractionUpdate!(ScaleUpdateDetails( - rotation: 0.0, - scale: scaleChange, - horizontalScale: 1.0, - verticalScale: 1.0, - )); - } - if (widget.onInteractionEnd != null) { - widget.onInteractionEnd!(ScaleEndDetails()); - } + + widget.onInteractionUpdate?.call(ScaleUpdateDetails( + focalPoint: event.position, + localFocalPoint: event.localPosition, + rotation: 0.0, + scale: scaleChange, + horizontalScale: 1.0, + verticalScale: 1.0, + )); + widget.onInteractionEnd?.call(ScaleEndDetails()); } } diff --git a/packages/flutter/test/widgets/interactive_viewer_test.dart b/packages/flutter/test/widgets/interactive_viewer_test.dart index 76cbb95e82..617e8620ef 100644 --- a/packages/flutter/test/widgets/interactive_viewer_test.dart +++ b/packages/flutter/test/widgets/interactive_viewer_test.dart @@ -625,6 +625,8 @@ void main() { testWidgets('Scale with mouse returns onInteraction properties', (WidgetTester tester) async{ final TransformationController transformationController = TransformationController(); + Offset focalPoint; + Offset localFocalPoint; double scaleChange; Velocity currentVelocity; bool calledStart; @@ -639,6 +641,8 @@ void main() { }, onInteractionUpdate: (ScaleUpdateDetails details){ scaleChange = details.scale; + focalPoint = details.focalPoint; + localFocalPoint = details.localFocalPoint; }, onInteractionEnd: (ScaleEndDetails details){ currentVelocity = details.velocity; @@ -656,11 +660,73 @@ void main() { const Velocity noMovement = Velocity(pixelsPerSecond: Offset(0,0)); final double afterScaling = transformationController.value.getMaxScaleOnAxis(); - expect(scaleChange,greaterThan(1.0)); - expect(afterScaling, isNot(equals(null))); - expect(afterScaling, isNot(equals(1.0))); + expect(scaleChange, greaterThan(1.0)); + expect(afterScaling, scaleChange); expect(currentVelocity, equals(noMovement)); expect(calledStart, equals(true)); + // Focal points are given in coordinates outside of InteractiveViewer, + // with local being in relation to the viewport. + expect(focalPoint, center); + expect(localFocalPoint, const Offset(100, 100)); + + // The scene point is the same as localFocalPoint because the center of + // the scene is at the center of the viewport. + final Offset scenePoint = transformationController.toScene(localFocalPoint); + expect(scenePoint, const Offset(100, 100)); + }); + + testWidgets('onInteraction can be used to get scene point', (WidgetTester tester) async{ + final TransformationController transformationController = TransformationController(); + Offset focalPoint; + Offset localFocalPoint; + double scaleChange; + Velocity currentVelocity; + bool calledStart; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: InteractiveViewer( + transformationController: transformationController, + onInteractionStart: (ScaleStartDetails details){ + calledStart = true; + }, + onInteractionUpdate: (ScaleUpdateDetails details){ + scaleChange = details.scale; + focalPoint = details.focalPoint; + localFocalPoint = details.localFocalPoint; + }, + onInteractionEnd: (ScaleEndDetails details){ + currentVelocity = details.velocity; + }, + child: Container(width: 200.0, height: 200.0), + ), + ), + ), + ), + ); + + final Offset center = tester.getCenter(find.byType(InteractiveViewer)); + final Offset offCenter = Offset(center.dx - 20.0, center.dy - 20.0); + await scrollAt(offCenter, tester, const Offset(0.0, -20.0)); + await tester.pumpAndSettle(); + const Velocity noMovement = Velocity(pixelsPerSecond: Offset(0,0)); + final double afterScaling = transformationController.value.getMaxScaleOnAxis(); + + expect(scaleChange, greaterThan(1.0)); + expect(afterScaling, scaleChange); + expect(currentVelocity, equals(noMovement)); + expect(calledStart, equals(true)); + // Focal points are given in coordinates outside of InteractiveViewer, + // with local being in relation to the viewport. + expect(focalPoint, offCenter); + expect(localFocalPoint, const Offset(80, 80)); + + // The top left corner of the viewport is not at the top left corner of + // the scene. + final Offset scenePoint = transformationController.toScene(Offset.zero); + expect(scenePoint.dx, greaterThan(0.0)); + expect(scenePoint.dy, greaterThan(0.0)); }); testWidgets('viewport changes size', (WidgetTester tester) async {