InteractiveViewer Scale Jump (#71497)

diff --git a/packages/flutter/lib/src/gestures/scale.dart b/packages/flutter/lib/src/gestures/scale.dart
index a4ec980..99226ed 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 @@
   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 @@
     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 2b34aae..e8de056 100644
--- a/packages/flutter/lib/src/widgets/gesture_detector.dart
+++ b/packages/flutter/lib/src/widgets/gesture_detector.dart
@@ -897,7 +897,8 @@
           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 8d7b008..ecf6e3c 100644
--- a/packages/flutter/lib/src/widgets/interactive_viewer.dart
+++ b/packages/flutter/lib/src/widgets/interactive_viewer.dart
@@ -1078,6 +1078,7 @@
       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 7fa937d..127f8bd 100644
--- a/packages/flutter/test/widgets/interactive_viewer_test.dart
+++ b/packages/flutter/test/widgets/interactive_viewer_test.dart
@@ -907,6 +907,84 @@
       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', () {