From 4982a7f185aeaa6d6557dca0628774ac30bf276f Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Wed, 31 Mar 2021 14:14:04 -0700 Subject: [PATCH] InteractiveViewer should call onInteractionUpdate even when gesture is disabled (#78990) --- .../lib/src/widgets/interactive_viewer.dart | 47 +++--- .../test/widgets/interactive_viewer_test.dart | 141 +++++++++++++++++- 2 files changed, 167 insertions(+), 21 deletions(-) diff --git a/packages/flutter/lib/src/widgets/interactive_viewer.dart b/packages/flutter/lib/src/widgets/interactive_viewer.dart index bbe53de6777..86d16a2377e 100644 --- a/packages/flutter/lib/src/widgets/interactive_viewer.dart +++ b/packages/flutter/lib/src/widgets/interactive_viewer.dart @@ -253,11 +253,12 @@ 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. + /// already been updated to reflect the change caused by the interaction, + /// though a pan may cause an inertia animation after this is called as well. /// /// {@template flutter.widgets.InteractiveViewer.onInteractionEnd} - /// Will be called even if the interaction is disabled with - /// [panEnabled] or [scaleEnabled]. + /// Will be called even if the interaction is disabled with [panEnabled] or + /// [scaleEnabled] for both touch gestures and mouse interactions. /// /// A [GestureDetector] wrapping the InteractiveViewer will not respond to /// [GestureDetector.onScaleStart], [GestureDetector.onScaleUpdate], and @@ -294,7 +295,8 @@ 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. + /// already been updated to reflect the change caused by the interaction, if + /// the interation caused the matrix to change. /// /// {@macro flutter.widgets.InteractiveViewer.onInteractionEnd} /// @@ -796,6 +798,7 @@ class _InteractiveViewerState extends State with TickerProvid _gestureType ??= _getGestureType(details); } if (!_gestureIsSupported(_gestureType)) { + widget.onInteractionUpdate?.call(details); return; } @@ -839,6 +842,7 @@ class _InteractiveViewerState extends State with TickerProvid case _GestureType.rotate: if (details.rotation == 0.0) { + widget.onInteractionUpdate?.call(details); return; } final double desiredRotation = _rotationStart! + details.rotation; @@ -856,6 +860,7 @@ class _InteractiveViewerState extends State with TickerProvid // In an effort to keep the behavior similar whether or not scaleEnabled // is true, these gestures are thrown away. if (details.scale != 1.0) { + widget.onInteractionUpdate?.call(details); return; } _panAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene); @@ -871,12 +876,7 @@ class _InteractiveViewerState extends State with TickerProvid ); break; } - widget.onInteractionUpdate?.call(ScaleUpdateDetails( - focalPoint: details.focalPoint, - localFocalPoint: details.localFocalPoint, - scale: details.scale, - rotation: details.rotation, - )); + widget.onInteractionUpdate?.call(details); } // Handle the end of a gesture of _GestureType. All of pan, scale, and rotate @@ -932,25 +932,36 @@ class _InteractiveViewerState extends State with TickerProvid // Handle mousewheel scroll events. void _receivedPointerSignal(PointerSignalEvent event) { 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, ), ); + + // 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); + if (!_gestureIsSupported(_GestureType.scale)) { + 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()); return; } - // 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 08bc8e75f7c..ed62e2e4c2c 100644 --- a/packages/flutter/test/widgets/interactive_viewer_test.dart +++ b/packages/flutter/test/widgets/interactive_viewer_test.dart @@ -4,6 +4,7 @@ import 'dart:math' as math; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:vector_math/vector_math_64.dart' show Quad, Vector3, Matrix4; @@ -717,15 +718,15 @@ void main() { body: Center( child: InteractiveViewer( transformationController: transformationController, - onInteractionStart: (ScaleStartDetails details){ + onInteractionStart: (ScaleStartDetails details) { calledStart = true; }, - onInteractionUpdate: (ScaleUpdateDetails details){ + onInteractionUpdate: (ScaleUpdateDetails details) { scaleChange = details.scale; focalPoint = details.focalPoint; localFocalPoint = details.localFocalPoint; }, - onInteractionEnd: (ScaleEndDetails details){ + onInteractionEnd: (ScaleEndDetails details) { currentVelocity = details.velocity; }, child: const SizedBox(width: 200.0, height: 200.0), @@ -758,6 +759,140 @@ void main() { expect(scenePoint.dy, greaterThan(0.0)); }); + testWidgets('onInteraction is called even when disabled (touch)', (WidgetTester tester) async { + final TransformationController transformationController = TransformationController(); + bool calledStart = false; + bool calledUpdate = false; + bool calledEnd = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: InteractiveViewer( + transformationController: transformationController, + scaleEnabled: false, + onInteractionStart: (ScaleStartDetails details) { + calledStart = true; + }, + onInteractionUpdate: (ScaleUpdateDetails details) { + calledUpdate = true; + }, + onInteractionEnd: (ScaleEndDetails details) { + calledEnd = true; + }, + child: const SizedBox(width: 200.0, height: 200.0), + ), + ), + ), + ), + ); + + final Offset childOffset = tester.getTopLeft(find.byType(SizedBox)); + final Offset childInterior = Offset( + childOffset.dx + 20.0, + childOffset.dy + 20.0, + ); + TestGesture gesture = await tester.startGesture(childOffset); + + // Attempting to pan doesn't work because it's disabled, but the + // interaction methods are still called. + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.moveTo(childInterior); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + expect(transformationController.value, equals(Matrix4.identity())); + expect(calledStart, isTrue); + expect(calledUpdate, isTrue); + expect(calledEnd, isTrue); + + // Attempting to pinch to zoom doesn't work because it's disabled, but the + // interaction methods are still called. + calledStart = false; + calledUpdate = false; + calledEnd = false; + final Offset scaleStart1 = childInterior; + final Offset scaleStart2 = Offset(childInterior.dx + 10.0, childInterior.dy); + final Offset scaleEnd1 = Offset(childInterior.dx - 10.0, childInterior.dy); + final Offset scaleEnd2 = Offset(childInterior.dx + 20.0, childInterior.dy); + gesture = await tester.startGesture(scaleStart1); + final TestGesture gesture2 = await tester.startGesture(scaleStart2); + addTearDown(gesture2.removePointer); + await tester.pump(); + await gesture.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + expect(transformationController.value, equals(Matrix4.identity())); + expect(calledStart, isTrue); + expect(calledUpdate, isTrue); + expect(calledEnd, isTrue); + }, variant: const TargetPlatformVariant({ TargetPlatform.android, TargetPlatform.iOS })); + + testWidgets('onInteraction is called even when disabled (mouse)', (WidgetTester tester) async { + final TransformationController transformationController = TransformationController(); + bool calledStart = false; + bool calledUpdate = false; + bool calledEnd = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: InteractiveViewer( + transformationController: transformationController, + scaleEnabled: false, + onInteractionStart: (ScaleStartDetails details) { + calledStart = true; + }, + onInteractionUpdate: (ScaleUpdateDetails details) { + calledUpdate = true; + }, + onInteractionEnd: (ScaleEndDetails details) { + calledEnd = true; + }, + child: const SizedBox(width: 200.0, height: 200.0), + ), + ), + ), + ), + ); + + final Offset childOffset = tester.getTopLeft(find.byType(SizedBox)); + final Offset childInterior = Offset( + childOffset.dx + 20.0, + childOffset.dy + 20.0, + ); + final TestGesture gesture = await tester.startGesture(childOffset, kind: PointerDeviceKind.mouse); + + // Attempting to pan doesn't work because it's disabled, but the + // interaction methods are still called. + addTearDown(gesture.removePointer); + await tester.pump(); + await gesture.moveTo(childInterior); + await tester.pump(); + await gesture.up(); + await tester.pumpAndSettle(); + expect(transformationController.value, equals(Matrix4.identity())); + expect(calledStart, isTrue); + expect(calledUpdate, isTrue); + expect(calledEnd, isTrue); + + // Attempting to scroll with a mouse to zoom doesn't work because it's + // disabled, but the interaction methods are still called. + calledStart = false; + calledUpdate = false; + calledEnd = false; + await scrollAt(childInterior, tester, const Offset(0.0, -20.0)); + await tester.pumpAndSettle(); + expect(transformationController.value, equals(Matrix4.identity())); + expect(calledStart, isTrue); + expect(calledUpdate, isTrue); + expect(calledEnd, isTrue); + }, variant: const TargetPlatformVariant({ TargetPlatform.macOS, TargetPlatform.linux, TargetPlatform.windows })); + testWidgets('viewport changes size', (WidgetTester tester) async { final TransformationController transformationController = TransformationController(); await tester.pumpWidget(