From 4e3b01152edef534f55fce6c1e8696aef4b6ff0e Mon Sep 17 00:00:00 2001 From: Justin McCandless Date: Mon, 21 Dec 2020 16:14:05 -0800 Subject: [PATCH] InteractiveViewer Scale Jump (#71497) --- packages/flutter/lib/src/gestures/scale.dart | 42 +++++++++- .../lib/src/widgets/gesture_detector.dart | 3 +- .../lib/src/widgets/interactive_viewer.dart | 1 + .../test/widgets/interactive_viewer_test.dart | 78 +++++++++++++++++++ 4 files changed, 121 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/gestures/scale.dart b/packages/flutter/lib/src/gestures/scale.dart index a4ec980d87..99226ed805 100644 --- a/packages/flutter/lib/src/gestures/scale.dart +++ b/packages/flutter/lib/src/gestures/scale.dart @@ -2,7 +2,6 @@ // 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:vector_math/vector_math_64.dart'; @@ -226,10 +225,42 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { ScaleGestureRecognizer({ Object? debugOwner, PointerDeviceKind? kind, - }) : super(debugOwner: debugOwner, kind: kind); + this.dragStartBehavior = DragStartBehavior.down, + }) : assert(dragStartBehavior != null), + super(debugOwner: debugOwner, kind: kind); + + /// Determines what point is used as the starting point in all calculations + /// involving this gesture. + /// + /// When set to [DragStartBehavior.down], the scale is calculated starting + /// from the position where the pointer first contacted the screen. + /// + /// When set to [DragStartBehavior.start], the scale is calculated starting + /// from the position where the scale gesture began. The scale gesture may + /// begin after the time that the pointer first contacted the screen if there + /// are multiple listeners competing for the gesture. In that case, the + /// gesture arena waits to determine whether or not the gesture is a scale + /// gesture before giving the gesture to this GestureRecognizer. This happens + /// in the case of nested GestureDetectors, for example. + /// + /// Defaults to [DragStartBehavior.down]. + /// + /// See also: + /// + /// * [https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation], + /// which provides more information about the gesture arena. + DragStartBehavior dragStartBehavior; /// The pointers in contact with the screen have established a focal point and /// initial scale of 1.0. + /// + /// This won't be called until the gesture arena has determined that this + /// GestureRecognizer has won the gesture. + /// + /// See also: + /// + /// * [https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation], + /// which provides more information about the gesture arena. GestureScaleStartCallback? onStart; /// The pointers in contact with the screen have indicated a new focal point @@ -461,6 +492,13 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { if (_state == _ScaleState.possible) { _state = _ScaleState.started; _dispatchOnStartCallbackIfNeeded(); + if (dragStartBehavior == DragStartBehavior.start) { + _initialFocalPoint = _currentFocalPoint; + _initialSpan = _currentSpan; + _initialLine = _currentLine; + _initialHorizontalSpan = _currentHorizontalSpan; + _initialVerticalSpan = _currentVerticalSpan; + } } } diff --git a/packages/flutter/lib/src/widgets/gesture_detector.dart b/packages/flutter/lib/src/widgets/gesture_detector.dart index 2b34aae471..e8de056dc2 100644 --- a/packages/flutter/lib/src/widgets/gesture_detector.dart +++ b/packages/flutter/lib/src/widgets/gesture_detector.dart @@ -897,7 +897,8 @@ class GestureDetector extends StatelessWidget { instance ..onStart = onScaleStart ..onUpdate = onScaleUpdate - ..onEnd = onScaleEnd; + ..onEnd = onScaleEnd + ..dragStartBehavior = dragStartBehavior; }, ); } diff --git a/packages/flutter/lib/src/widgets/interactive_viewer.dart b/packages/flutter/lib/src/widgets/interactive_viewer.dart index 8d7b008fc8..ecf6e3cd22 100644 --- a/packages/flutter/lib/src/widgets/interactive_viewer.dart +++ b/packages/flutter/lib/src/widgets/interactive_viewer.dart @@ -1078,6 +1078,7 @@ class _InteractiveViewerState extends State with TickerProvid onPointerSignal: _receivedPointerSignal, child: GestureDetector( behavior: HitTestBehavior.opaque, // Necessary when panning off screen. + dragStartBehavior: DragStartBehavior.start, onScaleEnd: _onScaleEnd, onScaleStart: _onScaleStart, onScaleUpdate: _onScaleUpdate, diff --git a/packages/flutter/test/widgets/interactive_viewer_test.dart b/packages/flutter/test/widgets/interactive_viewer_test.dart index 7fa937d1de..127f8bd46e 100644 --- a/packages/flutter/test/widgets/interactive_viewer_test.dart +++ b/packages/flutter/test/widgets/interactive_viewer_test.dart @@ -907,6 +907,84 @@ void main() { await tester.pumpAndSettle(); expect(transformationController.value, equals(Matrix4.identity())); }); + + testWidgets('scale does not jump when wrapped in GestureDetector', (WidgetTester tester) async { + final TransformationController transformationController = TransformationController(); + double? initialScale; + double? scale; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: GestureDetector( + onTapUp: (TapUpDetails details) {}, + child: InteractiveViewer( + onInteractionUpdate: (ScaleUpdateDetails details) { + initialScale ??= details.scale; + scale = details.scale; + }, + transformationController: transformationController, + child: Container(width: 200.0, height: 200.0), + ), + ), + ), + ), + ), + ); + + expect(transformationController.value, equals(Matrix4.identity())); + expect(initialScale, null); + expect(scale, null); + + // Pinch to zoom isn't immediately detected for a small amount of + // movement due to the GestureDetector. + final Offset childOffset = tester.getTopLeft(find.byType(Container)); + final Offset childInterior = Offset( + childOffset.dx + 20.0, + childOffset.dy + 20.0, + ); + final Offset scaleStart1 = childInterior; + final Offset scaleStart2 = Offset(childInterior.dx + 10.0, childInterior.dy); + Offset scaleEnd1 = Offset(childInterior.dx - 10.0, childInterior.dy); + Offset scaleEnd2 = Offset(childInterior.dx + 20.0, childInterior.dy); + TestGesture gesture = await tester.createGesture(); + TestGesture gesture2 = await tester.createGesture(); + addTearDown(gesture.removePointer); + addTearDown(gesture2.removePointer); + await gesture.down(scaleStart1); + await gesture2.down(scaleStart2); + 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(initialScale, null); + expect(scale, null); + + // Pinch to zoom for a larger amount is detected. It starts smoothly at + // 1.0 despite the fact that the gesture has already moved a bit. + scaleEnd1 = Offset(childInterior.dx - 38.0, childInterior.dy); + scaleEnd2 = Offset(childInterior.dx + 48.0, childInterior.dy); + gesture = await tester.createGesture(); + gesture2 = await tester.createGesture(); + addTearDown(gesture.removePointer); + addTearDown(gesture2.removePointer); + await gesture.down(scaleStart1); + await gesture2.down(scaleStart2); + await tester.pump(); + await gesture.moveTo(scaleEnd1); + await gesture2.moveTo(scaleEnd2); + await tester.pump(); + await gesture.up(); + await gesture2.up(); + await tester.pumpAndSettle(); + expect(initialScale, 1.0); + expect(scale, greaterThan(1.0)); + expect(transformationController.value.getMaxScaleOnAxis(), greaterThan(1.0)); + }); }); group('getNearestPointOnLine', () {