scale.dart 25.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:math' as math;

7
import 'constants.dart';
8
import 'events.dart';
9
import 'recognizer.dart';
10
import 'velocity_tracker.dart';
11

12 13 14 15 16 17 18
export 'dart:ui' show Offset, PointerDeviceKind;

export 'events.dart' show PointerDownEvent, PointerEvent, PointerPanZoomStartEvent;
export 'recognizer.dart' show DragStartBehavior;
export 'velocity_tracker.dart' show Velocity;


19
/// The possible states of a [ScaleGestureRecognizer].
20
enum _ScaleState {
21
  /// The recognizer is ready to start recognizing a gesture.
22
  ready,
23

24
  /// The sequence of pointer events seen thus far is consistent with a scale
25
  /// gesture but the gesture has not been accepted definitively.
26
  possible,
27

28
  /// The sequence of pointer events seen thus far has been accepted
29
  /// definitively as a scale gesture.
30
  accepted,
31

32
  /// The sequence of pointer events seen thus far has been accepted
33 34 35
  /// definitively as a scale gesture and the pointers established a focal point
  /// and initial scale.
  started,
36 37
}

38 39 40 41 42 43 44 45 46 47 48 49 50 51
class _PointerPanZoomData {
  _PointerPanZoomData({
    required this.focalPoint,
    required this.scale,
    required this.rotation
  });
  Offset focalPoint;
  double scale;
  double rotation;

  @override
  String toString() => '_PointerPanZoomData(focalPoint: $focalPoint, scale: $scale, angle: $rotation)';
}

52 53 54 55 56
/// Details for [GestureScaleStartCallback].
class ScaleStartDetails {
  /// Creates details for [GestureScaleStartCallback].
  ///
  /// The [focalPoint] argument must not be null.
57
  ScaleStartDetails({ this.focalPoint = Offset.zero, Offset? localFocalPoint, this.pointerCount = 0 })
58
    : assert(focalPoint != null), localFocalPoint = localFocalPoint ?? focalPoint;
59 60

  /// The initial focal point of the pointers in contact with the screen.
61
  ///
62
  /// Reported in global coordinates.
63 64 65 66 67
  ///
  /// See also:
  ///
  ///  * [localFocalPoint], which is the same value reported in local
  ///    coordinates.
68
  final Offset focalPoint;
69

70 71 72 73 74 75 76 77 78 79 80
  /// The initial focal point of the pointers in contact with the screen.
  ///
  /// Reported in local coordinates. Defaults to [focalPoint] if not set in the
  /// constructor.
  ///
  /// See also:
  ///
  ///  * [focalPoint], which is the same value reported in global
  ///    coordinates.
  final Offset localFocalPoint;

81 82 83 84 85
  /// The number of pointers being tracked by the gesture recognizer.
  ///
  /// Typically this is the number of fingers being used to pan the widget using the gesture
  /// recognizer.
  final int pointerCount;
86

87
  @override
88
  String toString() => 'ScaleStartDetails(focalPoint: $focalPoint, localFocalPoint: $localFocalPoint, pointersCount: $pointerCount)';
89 90 91 92 93 94
}

/// Details for [GestureScaleUpdateCallback].
class ScaleUpdateDetails {
  /// Creates details for [GestureScaleUpdateCallback].
  ///
95 96
  /// The [focalPoint], [scale], [horizontalScale], [verticalScale], [rotation]
  /// arguments must not be null. The [scale], [horizontalScale], and [verticalScale]
97
  /// argument must be greater than or equal to zero.
98
  ScaleUpdateDetails({
99
    this.focalPoint = Offset.zero,
100
    Offset? localFocalPoint,
101
    this.scale = 1.0,
102 103
    this.horizontalScale = 1.0,
    this.verticalScale = 1.0,
104
    this.rotation = 0.0,
105
    this.pointerCount = 0,
106
    this.focalPointDelta = Offset.zero,
107
  }) : assert(focalPoint != null),
108
       assert(focalPointDelta != null),
109
       assert(scale != null && scale >= 0.0),
110 111
       assert(horizontalScale != null && horizontalScale >= 0.0),
       assert(verticalScale != null && verticalScale >= 0.0),
112 113
       assert(rotation != null),
       localFocalPoint = localFocalPoint ?? focalPoint;
114

115 116
  /// The amount the gesture's focal point has moved in the coordinate space of
  /// the event receiver since the previous update.
117 118
  ///
  /// Defaults to zero if not specified in the constructor.
119
  final Offset focalPointDelta;
120

121 122 123
  /// The focal point of the pointers in contact with the screen.
  ///
  /// Reported in global coordinates.
124 125 126 127 128
  ///
  /// See also:
  ///
  ///  * [localFocalPoint], which is the same value reported in local
  ///    coordinates.
129
  final Offset focalPoint;
130

131 132 133 134 135 136 137 138 139 140 141
  /// The focal point of the pointers in contact with the screen.
  ///
  /// Reported in local coordinates. Defaults to [focalPoint] if not set in the
  /// constructor.
  ///
  /// See also:
  ///
  ///  * [focalPoint], which is the same value reported in global
  ///    coordinates.
  final Offset localFocalPoint;

142 143 144 145 146 147 148 149 150
  /// The scale implied by the average distance between the pointers in contact
  /// with the screen.
  ///
  /// This value must be greater than or equal to zero.
  ///
  /// See also:
  ///
  ///  * [horizontalScale], which is the scale along the horizontal axis.
  ///  * [verticalScale], which is the scale along the vertical axis.
151
  final double scale;
152

153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
  /// The scale implied by the average distance along the horizontal axis
  /// between the pointers in contact with the screen.
  ///
  /// This value must be greater than or equal to zero.
  ///
  /// See also:
  ///
  ///  * [scale], which is the general scale implied by the pointers.
  ///  * [verticalScale], which is the scale along the vertical axis.
  final double horizontalScale;

  /// The scale implied by the average distance along the vertical axis
  /// between the pointers in contact with the screen.
  ///
  /// This value must be greater than or equal to zero.
  ///
  /// See also:
  ///
  ///  * [scale], which is the general scale implied by the pointers.
  ///  * [horizontalScale], which is the scale along the horizontal axis.
  final double verticalScale;

175
  /// The angle implied by the first two pointers to enter in contact with
176 177 178
  /// the screen.
  ///
  /// Expressed in radians.
179 180
  final double rotation;

181 182 183 184 185 186
  /// The number of pointers being tracked by the gesture recognizer.
  ///
  /// Typically this is the number of fingers being used to pan the widget using the gesture
  /// recognizer.
  final int pointerCount;

187
  @override
188 189 190 191 192 193 194
  String toString() => 'ScaleUpdateDetails('
    'focalPoint: $focalPoint,'
    ' localFocalPoint: $localFocalPoint,'
    ' scale: $scale,'
    ' horizontalScale: $horizontalScale,'
    ' verticalScale: $verticalScale,'
    ' rotation: $rotation,'
195
    ' pointerCount: $pointerCount,'
196
    ' focalPointDelta: $focalPointDelta)';
197 198 199 200 201 202 203
}

/// Details for [GestureScaleEndCallback].
class ScaleEndDetails {
  /// Creates details for [GestureScaleEndCallback].
  ///
  /// The [velocity] argument must not be null.
204
  ScaleEndDetails({ this.velocity = Velocity.zero, this.pointerCount = 0 })
205
    : assert(velocity != null);
206 207 208

  /// The velocity of the last pointer to be lifted off of the screen.
  final Velocity velocity;
209

210 211 212 213 214 215
  /// The number of pointers being tracked by the gesture recognizer.
  ///
  /// Typically this is the number of fingers being used to pan the widget using the gesture
  /// recognizer.
  final int pointerCount;

216
  @override
217
  String toString() => 'ScaleEndDetails(velocity: $velocity, pointerCount: $pointerCount)';
218 219
}

220 221
/// Signature for when the pointers in contact with the screen have established
/// a focal point and initial scale of 1.0.
222
typedef GestureScaleStartCallback = void Function(ScaleStartDetails details);
223 224 225

/// Signature for when the pointers in contact with the screen have indicated a
/// new focal point and/or scale.
226
typedef GestureScaleUpdateCallback = void Function(ScaleUpdateDetails details);
227 228

/// Signature for when the pointers are no longer in contact with the screen.
229
typedef GestureScaleEndCallback = void Function(ScaleEndDetails details);
230 231 232 233 234 235

bool _isFlingGesture(Velocity velocity) {
  assert(velocity != null);
  final double speedSquared = velocity.pixelsPerSecond.distanceSquared;
  return speedSquared > kMinFlingVelocity * kMinFlingVelocity;
}
236

237 238 239 240 241

/// Defines a line between two pointers on screen.
///
/// [_LineBetweenPointers] is an abstraction of a line between two pointers in
/// contact with the screen. Used to track the rotation of a scale gesture.
242
class _LineBetweenPointers {
243 244 245 246 247 248 249 250

  /// Creates a [_LineBetweenPointers]. None of the [pointerStartLocation], [pointerStartId]
  /// [pointerEndLocation] and [pointerEndId] must be null. [pointerStartId] and [pointerEndId]
  /// should be different.
  _LineBetweenPointers({
    this.pointerStartLocation = Offset.zero,
    this.pointerStartId = 0,
    this.pointerEndLocation = Offset.zero,
251
    this.pointerEndId = 1,
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
  }) : assert(pointerStartLocation != null && pointerEndLocation != null),
       assert(pointerStartId != null && pointerEndId != null),
       assert(pointerStartId != pointerEndId);

  // The location and the id of the pointer that marks the start of the line.
  final Offset pointerStartLocation;
  final int pointerStartId;

  // The location and the id of the pointer that marks the end of the line.
  final Offset pointerEndLocation;
  final int pointerEndId;

}


267 268 269
/// Recognizes a scale gesture.
///
/// [ScaleGestureRecognizer] tracks the pointers in contact with the screen and
Ian Hickson's avatar
Ian Hickson committed
270 271 272 273
/// calculates their focal point, indicated scale, and rotation. When a focal
/// pointer is established, the recognizer calls [onStart]. As the focal point,
/// scale, rotation change, the recognizer calls [onUpdate]. When the pointers
/// are no longer in contact with the screen, the recognizer calls [onEnd].
274
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
275
  /// Create a gesture recognizer for interactions intended for scaling content.
276
  ///
277
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
278
  ScaleGestureRecognizer({
279
    super.debugOwner,
280 281 282 283
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
284 285
    super.kind,
    super.supportedDevices,
286
    this.dragStartBehavior = DragStartBehavior.down,
287
  }) : assert(dragStartBehavior != null);
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306

  /// 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:
  ///
307
  /// * https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation,
308 309
  ///   which provides more information about the gesture arena.
  DragStartBehavior dragStartBehavior;
310

311 312
  /// The pointers in contact with the screen have established a focal point and
  /// initial scale of 1.0.
313 314 315 316 317 318
  ///
  /// This won't be called until the gesture arena has determined that this
  /// GestureRecognizer has won the gesture.
  ///
  /// See also:
  ///
319
  /// * https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation,
320
  ///   which provides more information about the gesture arena.
321
  GestureScaleStartCallback? onStart;
322 323 324

  /// The pointers in contact with the screen have indicated a new focal point
  /// and/or scale.
325
  GestureScaleUpdateCallback? onUpdate;
326 327

  /// The pointers are no longer in contact with the screen.
328
  GestureScaleEndCallback? onEnd;
329

330
  _ScaleState _state = _ScaleState.ready;
331

332 333 334
  Matrix4? _lastTransform;

  late Offset _initialFocalPoint;
335
  Offset? _currentFocalPoint;
336 337 338 339 340 341
  late double _initialSpan;
  late double _currentSpan;
  late double _initialHorizontalSpan;
  late double _currentHorizontalSpan;
  late double _initialVerticalSpan;
  late double _currentVerticalSpan;
342
  late Offset _localFocalPoint;
343 344
  _LineBetweenPointers? _initialLine;
  _LineBetweenPointers? _currentLine;
345 346
  final Map<int, Offset> _pointerLocations = <int, Offset>{};
  final List<int> _pointerQueue = <int>[]; // A queue to sort pointers in order of entrance
347
  final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
348
  late Offset _delta;
349 350 351
  final Map<int, _PointerPanZoomData> _pointerPanZooms = <int, _PointerPanZoomData>{};
  double _initialPanZoomScaleFactor = 1;
  double _initialPanZoomRotationFactor = 0;
352

353
  double get _pointerScaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
354

355
  double get _pointerHorizontalScaleFactor => _initialHorizontalSpan > 0.0 ? _currentHorizontalSpan / _initialHorizontalSpan : 1.0;
356

357
  double get _pointerVerticalScaleFactor => _initialVerticalSpan > 0.0 ? _currentVerticalSpan / _initialVerticalSpan : 1.0;
358

359 360 361 362
  double get _scaleFactor {
    double scale = _pointerScaleFactor;
    for (final _PointerPanZoomData p in _pointerPanZooms.values) {
      scale *= p.scale / _initialPanZoomScaleFactor;
363
    }
364 365
    return scale;
  }
366

367 368 369 370 371 372 373
  double get _horizontalScaleFactor {
    double scale = _pointerHorizontalScaleFactor;
    for (final _PointerPanZoomData p in _pointerPanZooms.values) {
      scale *= p.scale / _initialPanZoomScaleFactor;
    }
    return scale;
  }
374

375 376 377 378 379 380 381
  double get _verticalScaleFactor {
    double scale = _pointerVerticalScaleFactor;
    for (final _PointerPanZoomData p in _pointerPanZooms.values) {
      scale *= p.scale / _initialPanZoomScaleFactor;
    }
    return scale;
  }
382

383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
  int get _pointerCount {
    return _pointerPanZooms.length + _pointerQueue.length;
  }

  double _computeRotationFactor() {
    double factor = 0.0;
    if (_initialLine != null && _currentLine != null) {
      final double fx = _initialLine!.pointerStartLocation.dx;
      final double fy = _initialLine!.pointerStartLocation.dy;
      final double sx = _initialLine!.pointerEndLocation.dx;
      final double sy = _initialLine!.pointerEndLocation.dy;

      final double nfx = _currentLine!.pointerStartLocation.dx;
      final double nfy = _currentLine!.pointerStartLocation.dy;
      final double nsx = _currentLine!.pointerEndLocation.dx;
      final double nsy = _currentLine!.pointerEndLocation.dy;

      final double angle1 = math.atan2(fy - sy, fx - sx);
      final double angle2 = math.atan2(nfy - nsy, nfx - nsx);

      factor = angle2 - angle1;
    }
    for (final _PointerPanZoomData p in _pointerPanZooms.values) {
      factor += p.rotation;
    }
    factor -= _initialPanZoomRotationFactor;
    return factor;
410 411
  }

412
  @override
413
  void addAllowedPointer(PointerDownEvent event) {
414
    super.addAllowedPointer(event);
415
    _velocityTrackers[event.pointer] = VelocityTracker.withKind(event.kind);
416 417
    if (_state == _ScaleState.ready) {
      _state = _ScaleState.possible;
418 419
      _initialSpan = 0.0;
      _currentSpan = 0.0;
420 421 422 423
      _initialHorizontalSpan = 0.0;
      _currentHorizontalSpan = 0.0;
      _initialVerticalSpan = 0.0;
      _currentVerticalSpan = 0.0;
424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
    }
  }

  @override
  bool isPointerPanZoomAllowed(PointerPanZoomStartEvent event) => true;

  @override
  void addAllowedPointerPanZoom(PointerPanZoomStartEvent event) {
    super.addAllowedPointerPanZoom(event);
    startTrackingPointer(event.pointer, event.transform);
    _velocityTrackers[event.pointer] = VelocityTracker.withKind(event.kind);
    if (_state == _ScaleState.ready) {
      _state = _ScaleState.possible;
      _initialPanZoomScaleFactor = 1.0;
      _initialPanZoomRotationFactor = 0.0;
439 440 441
    }
  }

442
  @override
Ian Hickson's avatar
Ian Hickson committed
443
  void handleEvent(PointerEvent event) {
444 445 446
    assert(_state != _ScaleState.ready);
    bool didChangeConfiguration = false;
    bool shouldStartIfAccepted = false;
Ian Hickson's avatar
Ian Hickson committed
447
    if (event is PointerMoveEvent) {
448
      final VelocityTracker tracker = _velocityTrackers[event.pointer]!;
449
      if (!event.synthesized) {
450
        tracker.addPosition(event.timeStamp, event.position);
451
      }
Ian Hickson's avatar
Ian Hickson committed
452
      _pointerLocations[event.pointer] = event.position;
453
      shouldStartIfAccepted = true;
454
      _lastTransform = event.transform;
Ian Hickson's avatar
Ian Hickson committed
455 456
    } else if (event is PointerDownEvent) {
      _pointerLocations[event.pointer] = event.position;
457
      _pointerQueue.add(event.pointer);
458 459
      didChangeConfiguration = true;
      shouldStartIfAccepted = true;
460
      _lastTransform = event.transform;
461
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
Ian Hickson's avatar
Ian Hickson committed
462
      _pointerLocations.remove(event.pointer);
463
      _pointerQueue.remove(event.pointer);
464
      didChangeConfiguration = true;
465
      _lastTransform = event.transform;
466 467 468 469 470 471 472 473 474 475 476
    } else if (event is PointerPanZoomStartEvent) {
      assert(_pointerPanZooms[event.pointer] == null);
      _pointerPanZooms[event.pointer] = _PointerPanZoomData(
        focalPoint: event.position,
        scale: 1,
        rotation: 0
      );
      didChangeConfiguration = true;
      shouldStartIfAccepted = true;
    } else if (event is PointerPanZoomUpdateEvent) {
      assert(_pointerPanZooms[event.pointer] != null);
477
      if (!event.synthesized) {
478
        _velocityTrackers[event.pointer]!.addPosition(event.timeStamp, event.pan);
479
      }
480 481 482 483 484 485 486 487 488 489 490
      _pointerPanZooms[event.pointer] = _PointerPanZoomData(
        focalPoint: event.position + event.pan,
        scale: event.scale,
        rotation: event.rotation
      );
      _lastTransform = event.transform;
      shouldStartIfAccepted = true;
    } else if (event is PointerPanZoomEndEvent) {
      assert(_pointerPanZooms[event.pointer] != null);
      _pointerPanZooms.remove(event.pointer);
      didChangeConfiguration = true;
491 492
    }

493
    _updateLines();
494
    _update();
495

496
    if (!didChangeConfiguration || _reconfigure(event.pointer)) {
497
      _advanceStateMachine(shouldStartIfAccepted, event.kind);
498
    }
499 500 501
    stopTrackingIfPointerNoLongerDown(event);
  }

502
  void _update() {
503 504
    final Offset? previousFocalPoint = _currentFocalPoint;

505
    // Compute the focal point
506
    Offset focalPoint = Offset.zero;
507
    for (final int pointer in _pointerLocations.keys) {
508
      focalPoint += _pointerLocations[pointer]!;
509 510
    }
    for (final _PointerPanZoomData p in _pointerPanZooms.values) {
511
      focalPoint += p.focalPoint;
512
    }
513
    _currentFocalPoint = _pointerCount > 0 ? focalPoint / _pointerCount.toDouble() : Offset.zero;
514

515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
    if (previousFocalPoint == null) {
      _localFocalPoint = PointerEvent.transformPosition(
        _lastTransform,
        _currentFocalPoint!,
      );
      _delta = Offset.zero;
    } else {
      final Offset localPreviousFocalPoint = _localFocalPoint;
      _localFocalPoint = PointerEvent.transformPosition(
        _lastTransform,
        _currentFocalPoint!,
      );
      _delta = _localFocalPoint - localPreviousFocalPoint;
    }

530 531 532
    final int count = _pointerLocations.keys.length;

    Offset pointerFocalPoint = Offset.zero;
533
    for (final int pointer in _pointerLocations.keys) {
534
      pointerFocalPoint += _pointerLocations[pointer]!;
535 536
    }
    if (count > 0) {
537
      pointerFocalPoint = pointerFocalPoint / count.toDouble();
538
    }
539

540 541 542
    // Span is the average deviation from focal point. Horizontal and vertical
    // spans are the average deviations from the focal point's horizontal and
    // vertical coordinates, respectively.
543
    double totalDeviation = 0.0;
544 545
    double totalHorizontalDeviation = 0.0;
    double totalVerticalDeviation = 0.0;
546
    for (final int pointer in _pointerLocations.keys) {
547 548 549
      totalDeviation += (pointerFocalPoint - _pointerLocations[pointer]!).distance;
      totalHorizontalDeviation += (pointerFocalPoint.dx - _pointerLocations[pointer]!.dx).abs();
      totalVerticalDeviation += (pointerFocalPoint.dy - _pointerLocations[pointer]!.dy).abs();
550
    }
551
    _currentSpan = count > 0 ? totalDeviation / count : 0.0;
552 553
    _currentHorizontalSpan = count > 0 ? totalHorizontalDeviation / count : 0.0;
    _currentVerticalSpan = count > 0 ? totalVerticalDeviation / count : 0.0;
554
  }
555

556
  /// Updates [_initialLine] and [_currentLine] accordingly to the situation of
557
  /// the registered pointers.
558 559 560 561 562 563 564
  void _updateLines() {
    final int count = _pointerLocations.keys.length;
    assert(_pointerQueue.length >= count);
    /// In case of just one pointer registered, reconfigure [_initialLine]
    if (count < 2) {
      _initialLine = _currentLine;
    } else if (_initialLine != null &&
565 566
      _initialLine!.pointerStartId == _pointerQueue[0] &&
      _initialLine!.pointerEndId == _pointerQueue[1]) {
567 568 569
      /// Rotation updated, set the [_currentLine]
      _currentLine = _LineBetweenPointers(
        pointerStartId: _pointerQueue[0],
570
        pointerStartLocation: _pointerLocations[_pointerQueue[0]]!,
571
        pointerEndId: _pointerQueue[1],
572
        pointerEndLocation: _pointerLocations[_pointerQueue[1]]!,
573 574 575 576 577
      );
    } else {
      /// A new rotation process is on the way, set the [_initialLine]
      _initialLine = _LineBetweenPointers(
        pointerStartId: _pointerQueue[0],
578
        pointerStartLocation: _pointerLocations[_pointerQueue[0]]!,
579
        pointerEndId: _pointerQueue[1],
580
        pointerEndLocation: _pointerLocations[_pointerQueue[1]]!,
581
      );
582
      _currentLine = _initialLine;
583 584 585
    }
  }

586
  bool _reconfigure(int pointer) {
587
    _initialFocalPoint = _currentFocalPoint!;
588
    _initialSpan = _currentSpan;
589
    _initialLine = _currentLine;
590 591
    _initialHorizontalSpan = _currentHorizontalSpan;
    _initialVerticalSpan = _currentVerticalSpan;
592 593 594 595 596 597 598
    if (_pointerPanZooms.isEmpty) {
      _initialPanZoomScaleFactor = 1.0;
      _initialPanZoomRotationFactor = 0.0;
    } else {
      _initialPanZoomScaleFactor = _scaleFactor / _pointerScaleFactor;
      _initialPanZoomRotationFactor = _pointerPanZooms.values.map((_PointerPanZoomData x) => x.rotation).reduce((double a, double b) => a + b);
    }
599 600
    if (_state == _ScaleState.started) {
      if (onEnd != null) {
601
        final VelocityTracker tracker = _velocityTrackers[pointer]!;
602 603

        Velocity velocity = tracker.getVelocity();
604
        if (_isFlingGesture(velocity)) {
605
          final Offset pixelsPerSecond = velocity.pixelsPerSecond;
606
          if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity) {
607
            velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
608
          }
609
          invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerCount)));
610
        } else {
611
          invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(pointerCount: _pointerCount)));
612
        }
613
      }
614 615
      _state = _ScaleState.accepted;
      return false;
616
    }
617 618
    return true;
  }
619

620
  void _advanceStateMachine(bool shouldStartIfAccepted, PointerDeviceKind pointerDeviceKind) {
621
    if (_state == _ScaleState.ready) {
622
      _state = _ScaleState.possible;
623
    }
624

625 626
    if (_state == _ScaleState.possible) {
      final double spanDelta = (_currentSpan - _initialSpan).abs();
627
      final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance;
628
      if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind, gestureSettings) || math.max(_scaleFactor / _pointerScaleFactor, _pointerScaleFactor / _scaleFactor) > 1.05) {
629
        resolve(GestureDisposition.accepted);
630
      }
631
    } else if (_state.index >= _ScaleState.accepted.index) {
632 633 634
      resolve(GestureDisposition.accepted);
    }

635 636 637
    if (_state == _ScaleState.accepted && shouldStartIfAccepted) {
      _state = _ScaleState.started;
      _dispatchOnStartCallbackIfNeeded();
638 639
    }

640
    if (_state == _ScaleState.started && onUpdate != null) {
641
      invokeCallback<void>('onUpdate', () {
642
        onUpdate!(ScaleUpdateDetails(
643 644 645
          scale: _scaleFactor,
          horizontalScale: _horizontalScaleFactor,
          verticalScale: _verticalScaleFactor,
646 647
          focalPoint: _currentFocalPoint!,
          localFocalPoint: _localFocalPoint,
648
          rotation: _computeRotationFactor(),
649
          pointerCount: _pointerCount,
650
          focalPointDelta: _delta,
651 652
        ));
      });
653
    }
654 655 656 657
  }

  void _dispatchOnStartCallbackIfNeeded() {
    assert(_state == _ScaleState.started);
658
    if (onStart != null) {
659
      invokeCallback<void>('onStart', () {
660
        onStart!(ScaleStartDetails(
661 662
          focalPoint: _currentFocalPoint!,
          localFocalPoint: _localFocalPoint,
663
          pointerCount: _pointerCount,
664 665
        ));
      });
666
    }
667 668
  }

669
  @override
670
  void acceptGesture(int pointer) {
671 672 673
    if (_state == _ScaleState.possible) {
      _state = _ScaleState.started;
      _dispatchOnStartCallbackIfNeeded();
674
      if (dragStartBehavior == DragStartBehavior.start) {
675
        _initialFocalPoint = _currentFocalPoint!;
676 677 678 679
        _initialSpan = _currentSpan;
        _initialLine = _currentLine;
        _initialHorizontalSpan = _currentHorizontalSpan;
        _initialVerticalSpan = _currentVerticalSpan;
680 681 682 683 684 685 686
        if (_pointerPanZooms.isEmpty) {
          _initialPanZoomScaleFactor = 1.0;
          _initialPanZoomRotationFactor = 0.0;
        } else {
          _initialPanZoomScaleFactor = _scaleFactor / _pointerScaleFactor;
          _initialPanZoomRotationFactor = _pointerPanZooms.values.map((_PointerPanZoomData x) => x.rotation).reduce((double a, double b) => a + b);
        }
687
      }
688 689 690
    }
  }

691 692
  @override
  void rejectGesture(int pointer) {
693 694 695
    _pointerPanZooms.remove(pointer);
    _pointerLocations.remove(pointer);
    _pointerQueue.remove(pointer);
696 697 698
    stopTrackingPointer(pointer);
  }

699
  @override
700
  void didStopTrackingLastPointer(int pointer) {
701
    switch (_state) {
702
      case _ScaleState.possible:
703 704
        resolve(GestureDisposition.rejected);
        break;
705
      case _ScaleState.ready:
706
        assert(false); // We should have not seen a pointer yet
707
        break;
708
      case _ScaleState.accepted:
709
        break;
710
      case _ScaleState.started:
711
        assert(false); // We should be in the accepted state when user is done
712 713
        break;
    }
714
    _state = _ScaleState.ready;
715
  }
716

717 718 719 720 721 722
  @override
  void dispose() {
    _velocityTrackers.clear();
    super.dispose();
  }

723
  @override
724
  String get debugDescription => 'scale';
725
}