scale.dart 21.3 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 8
import 'package:vector_math/vector_math_64.dart';

9 10
import 'arena.dart';
import 'constants.dart';
11
import 'events.dart';
12
import 'recognizer.dart';
13
import 'velocity_tracker.dart';
14

15
/// The possible states of a [ScaleGestureRecognizer].
16
enum _ScaleState {
17
  /// The recognizer is ready to start recognizing a gesture.
18
  ready,
19

20
  /// The sequence of pointer events seen thus far is consistent with a scale
21
  /// gesture but the gesture has not been accepted definitively.
22
  possible,
23

24
  /// The sequence of pointer events seen thus far has been accepted
25
  /// definitively as a scale gesture.
26
  accepted,
27

28
  /// The sequence of pointer events seen thus far has been accepted
29 30 31
  /// definitively as a scale gesture and the pointers established a focal point
  /// and initial scale.
  started,
32 33
}

34 35 36 37 38
/// Details for [GestureScaleStartCallback].
class ScaleStartDetails {
  /// Creates details for [GestureScaleStartCallback].
  ///
  /// The [focalPoint] argument must not be null.
39
  ScaleStartDetails({ this.focalPoint = Offset.zero, Offset? localFocalPoint, this.pointerCount = 0 })
40
    : assert(focalPoint != null), localFocalPoint = localFocalPoint ?? focalPoint;
41 42

  /// The initial focal point of the pointers in contact with the screen.
43
  ///
44
  /// Reported in global coordinates.
45 46 47 48 49
  ///
  /// See also:
  ///
  ///  * [localFocalPoint], which is the same value reported in local
  ///    coordinates.
50
  final Offset focalPoint;
51

52 53 54 55 56 57 58 59 60 61 62
  /// 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;

63 64 65 66 67
  /// 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;
68

69
  @override
70
  String toString() => 'ScaleStartDetails(focalPoint: $focalPoint, localFocalPoint: $localFocalPoint, pointersCount: $pointerCount)';
71 72 73 74 75 76
}

/// Details for [GestureScaleUpdateCallback].
class ScaleUpdateDetails {
  /// Creates details for [GestureScaleUpdateCallback].
  ///
77 78
  /// The [focalPoint], [scale], [horizontalScale], [verticalScale], [rotation]
  /// arguments must not be null. The [scale], [horizontalScale], and [verticalScale]
79
  /// argument must be greater than or equal to zero.
80
  ScaleUpdateDetails({
81
    this.focalPoint = Offset.zero,
82
    Offset? localFocalPoint,
83
    this.scale = 1.0,
84 85
    this.horizontalScale = 1.0,
    this.verticalScale = 1.0,
86
    this.rotation = 0.0,
87
    this.pointerCount = 0,
88
    this.focalPointDelta = Offset.zero,
89
  }) : assert(focalPoint != null),
90
       assert(focalPointDelta != null),
91
       assert(scale != null && scale >= 0.0),
92 93
       assert(horizontalScale != null && horizontalScale >= 0.0),
       assert(verticalScale != null && verticalScale >= 0.0),
94 95
       assert(rotation != null),
       localFocalPoint = localFocalPoint ?? focalPoint;
96

97 98
  /// The amount the gesture's focal point has moved in the coordinate space of
  /// the event receiver since the previous update.
99 100
  ///
  /// Defaults to zero if not specified in the constructor.
101
  final Offset focalPointDelta;
102

103 104 105
  /// The focal point of the pointers in contact with the screen.
  ///
  /// Reported in global coordinates.
106 107 108 109 110
  ///
  /// See also:
  ///
  ///  * [localFocalPoint], which is the same value reported in local
  ///    coordinates.
111
  final Offset focalPoint;
112

113 114 115 116 117 118 119 120 121 122 123
  /// 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;

124 125 126 127 128 129 130 131 132
  /// 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.
133
  final double scale;
134

135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
  /// 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;

157
  /// The angle implied by the first two pointers to enter in contact with
158 159 160
  /// the screen.
  ///
  /// Expressed in radians.
161 162
  final double rotation;

163 164 165 166 167 168
  /// 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;

169
  @override
170 171 172 173 174 175 176
  String toString() => 'ScaleUpdateDetails('
    'focalPoint: $focalPoint,'
    ' localFocalPoint: $localFocalPoint,'
    ' scale: $scale,'
    ' horizontalScale: $horizontalScale,'
    ' verticalScale: $verticalScale,'
    ' rotation: $rotation,'
177 178
    ' pointerCount: $pointerCount,'
    ' focalPointDelta: $localFocalPoint)';
179 180 181 182 183 184 185
}

/// Details for [GestureScaleEndCallback].
class ScaleEndDetails {
  /// Creates details for [GestureScaleEndCallback].
  ///
  /// The [velocity] argument must not be null.
186
  ScaleEndDetails({ this.velocity = Velocity.zero, this.pointerCount = 0 })
187
    : assert(velocity != null);
188 189 190

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

192 193 194 195 196 197
  /// 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;

198
  @override
199
  String toString() => 'ScaleEndDetails(velocity: $velocity, pointerCount: $pointerCount)';
200 201
}

202 203
/// Signature for when the pointers in contact with the screen have established
/// a focal point and initial scale of 1.0.
204
typedef GestureScaleStartCallback = void Function(ScaleStartDetails details);
205 206 207

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

/// Signature for when the pointers are no longer in contact with the screen.
211
typedef GestureScaleEndCallback = void Function(ScaleEndDetails details);
212 213 214 215 216 217

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

219 220 221 222 223

/// 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.
224
class _LineBetweenPointers {
225 226 227 228 229 230 231 232

  /// 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,
233
    this.pointerEndId = 1,
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248
  }) : 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;

}


249 250 251
/// Recognizes a scale gesture.
///
/// [ScaleGestureRecognizer] tracks the pointers in contact with the screen and
Ian Hickson's avatar
Ian Hickson committed
252 253 254 255
/// 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].
256
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
257
  /// Create a gesture recognizer for interactions intended for scaling content.
258
  ///
259
  /// {@macro flutter.gestures.GestureRecognizer.supportedDevices}
260
  ScaleGestureRecognizer({
261
    Object? debugOwner,
262 263 264 265
    @Deprecated(
      'Migrate to supportedDevices. '
      'This feature was deprecated after v2.3.0-1.0.pre.',
    )
266
    PointerDeviceKind? kind,
267
    Set<PointerDeviceKind>? supportedDevices,
268 269
    this.dragStartBehavior = DragStartBehavior.down,
  }) : assert(dragStartBehavior != null),
270 271 272 273 274
       super(
         debugOwner: debugOwner,
         kind: kind,
         supportedDevices: supportedDevices,
       );
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293

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

298 299
  /// The pointers in contact with the screen have established a focal point and
  /// initial scale of 1.0.
300 301 302 303 304 305
  ///
  /// This won't be called until the gesture arena has determined that this
  /// GestureRecognizer has won the gesture.
  ///
  /// See also:
  ///
306
  /// * https://flutter.dev/docs/development/ui/advanced/gestures#gesture-disambiguation,
307
  ///   which provides more information about the gesture arena.
308
  GestureScaleStartCallback? onStart;
309 310 311

  /// The pointers in contact with the screen have indicated a new focal point
  /// and/or scale.
312
  GestureScaleUpdateCallback? onUpdate;
313 314

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

317
  _ScaleState _state = _ScaleState.ready;
318

319 320 321
  Matrix4? _lastTransform;

  late Offset _initialFocalPoint;
322
  Offset? _currentFocalPoint;
323 324 325 326 327 328
  late double _initialSpan;
  late double _currentSpan;
  late double _initialHorizontalSpan;
  late double _currentHorizontalSpan;
  late double _initialVerticalSpan;
  late double _currentVerticalSpan;
329
  late Offset _localFocalPoint;
330 331 332 333
  _LineBetweenPointers? _initialLine;
  _LineBetweenPointers? _currentLine;
  late Map<int, Offset> _pointerLocations;
  late List<int> _pointerQueue; // A queue to sort pointers in order of entrance
334
  final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
335
  late Offset _delta;
336

337
  double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
338

339 340 341 342
  double get _horizontalScaleFactor => _initialHorizontalSpan > 0.0 ? _currentHorizontalSpan / _initialHorizontalSpan : 1.0;

  double get _verticalScaleFactor => _initialVerticalSpan > 0.0 ? _currentVerticalSpan / _initialVerticalSpan : 1.0;

343 344 345 346
  double _computeRotationFactor() {
    if (_initialLine == null || _currentLine == null) {
      return 0.0;
    }
347 348 349 350
    final double fx = _initialLine!.pointerStartLocation.dx;
    final double fy = _initialLine!.pointerStartLocation.dy;
    final double sx = _initialLine!.pointerEndLocation.dx;
    final double sy = _initialLine!.pointerEndLocation.dy;
351

352 353 354 355
    final double nfx = _currentLine!.pointerStartLocation.dx;
    final double nfy = _currentLine!.pointerStartLocation.dy;
    final double nsx = _currentLine!.pointerEndLocation.dx;
    final double nsy = _currentLine!.pointerEndLocation.dy;
356 357 358 359 360 361 362

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

    return angle2 - angle1;
  }

363
  @override
364
  void addAllowedPointer(PointerDownEvent event) {
365
    super.addAllowedPointer(event);
366
    _velocityTrackers[event.pointer] = VelocityTracker.withKind(event.kind);
367 368
    if (_state == _ScaleState.ready) {
      _state = _ScaleState.possible;
369 370
      _initialSpan = 0.0;
      _currentSpan = 0.0;
371 372 373 374
      _initialHorizontalSpan = 0.0;
      _currentHorizontalSpan = 0.0;
      _initialVerticalSpan = 0.0;
      _currentVerticalSpan = 0.0;
375
      _pointerLocations = <int, Offset>{};
376
      _pointerQueue = <int>[];
377 378 379
    }
  }

380
  @override
Ian Hickson's avatar
Ian Hickson committed
381
  void handleEvent(PointerEvent event) {
382 383 384
    assert(_state != _ScaleState.ready);
    bool didChangeConfiguration = false;
    bool shouldStartIfAccepted = false;
Ian Hickson's avatar
Ian Hickson committed
385
    if (event is PointerMoveEvent) {
386
      final VelocityTracker tracker = _velocityTrackers[event.pointer]!;
387 388
      if (!event.synthesized)
        tracker.addPosition(event.timeStamp, event.position);
Ian Hickson's avatar
Ian Hickson committed
389
      _pointerLocations[event.pointer] = event.position;
390
      shouldStartIfAccepted = true;
391
      _lastTransform = event.transform;
Ian Hickson's avatar
Ian Hickson committed
392 393
    } else if (event is PointerDownEvent) {
      _pointerLocations[event.pointer] = event.position;
394
      _pointerQueue.add(event.pointer);
395 396
      didChangeConfiguration = true;
      shouldStartIfAccepted = true;
397
      _lastTransform = event.transform;
398
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
Ian Hickson's avatar
Ian Hickson committed
399
      _pointerLocations.remove(event.pointer);
400
      _pointerQueue.remove(event.pointer);
401
      didChangeConfiguration = true;
402
      _lastTransform = event.transform;
403 404
    }

405
    _updateLines();
406
    _update();
407

408
    if (!didChangeConfiguration || _reconfigure(event.pointer))
409
      _advanceStateMachine(shouldStartIfAccepted, event.kind);
410 411 412
    stopTrackingIfPointerNoLongerDown(event);
  }

413
  void _update() {
414
    final int count = _pointerLocations.keys.length;
415

416 417
    final Offset? previousFocalPoint = _currentFocalPoint;

418
    // Compute the focal point
419
    Offset focalPoint = Offset.zero;
420
    for (final int pointer in _pointerLocations.keys)
421
      focalPoint += _pointerLocations[pointer]!;
422
    _currentFocalPoint = count > 0 ? focalPoint / count.toDouble() : Offset.zero;
423

424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
    if (previousFocalPoint == null) {
      _localFocalPoint = PointerEvent.transformPosition(
        _lastTransform,
        _currentFocalPoint!,
      );
      _delta = Offset.zero;
    } else {
      final Offset localPreviousFocalPoint = _localFocalPoint;
      _localFocalPoint = PointerEvent.transformPosition(
        _lastTransform,
        _currentFocalPoint!,
      );
      _delta = _localFocalPoint - localPreviousFocalPoint;
    }

439 440 441
    // 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.
442
    double totalDeviation = 0.0;
443 444
    double totalHorizontalDeviation = 0.0;
    double totalVerticalDeviation = 0.0;
445
    for (final int pointer in _pointerLocations.keys) {
446 447 448
      totalDeviation += (_currentFocalPoint! - _pointerLocations[pointer]!).distance;
      totalHorizontalDeviation += (_currentFocalPoint!.dx - _pointerLocations[pointer]!.dx).abs();
      totalVerticalDeviation += (_currentFocalPoint!.dy - _pointerLocations[pointer]!.dy).abs();
449
    }
450
    _currentSpan = count > 0 ? totalDeviation / count : 0.0;
451 452
    _currentHorizontalSpan = count > 0 ? totalHorizontalDeviation / count : 0.0;
    _currentVerticalSpan = count > 0 ? totalVerticalDeviation / count : 0.0;
453
  }
454

455
  /// Updates [_initialLine] and [_currentLine] accordingly to the situation of
456
  /// the registered pointers.
457 458 459 460 461 462 463
  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 &&
464 465
      _initialLine!.pointerStartId == _pointerQueue[0] &&
      _initialLine!.pointerEndId == _pointerQueue[1]) {
466 467 468
      /// Rotation updated, set the [_currentLine]
      _currentLine = _LineBetweenPointers(
        pointerStartId: _pointerQueue[0],
469
        pointerStartLocation: _pointerLocations[_pointerQueue[0]]!,
470
        pointerEndId: _pointerQueue[1],
471
        pointerEndLocation: _pointerLocations[_pointerQueue[1]]!,
472 473 474 475 476
      );
    } else {
      /// A new rotation process is on the way, set the [_initialLine]
      _initialLine = _LineBetweenPointers(
        pointerStartId: _pointerQueue[0],
477
        pointerStartLocation: _pointerLocations[_pointerQueue[0]]!,
478
        pointerEndId: _pointerQueue[1],
479
        pointerEndLocation: _pointerLocations[_pointerQueue[1]]!,
480
      );
481
      _currentLine = _initialLine;
482 483 484
    }
  }

485
  bool _reconfigure(int pointer) {
486
    _initialFocalPoint = _currentFocalPoint!;
487
    _initialSpan = _currentSpan;
488
    _initialLine = _currentLine;
489 490
    _initialHorizontalSpan = _currentHorizontalSpan;
    _initialVerticalSpan = _currentVerticalSpan;
491 492
    if (_state == _ScaleState.started) {
      if (onEnd != null) {
493
        final VelocityTracker tracker = _velocityTrackers[pointer]!;
494 495

        Velocity velocity = tracker.getVelocity();
496
        if (_isFlingGesture(velocity)) {
497 498
          final Offset pixelsPerSecond = velocity.pixelsPerSecond;
          if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
499
            velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
500
          invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerQueue.length)));
501
        } else {
502
          invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: Velocity.zero, pointerCount: _pointerQueue.length)));
503
        }
504
      }
505 506
      _state = _ScaleState.accepted;
      return false;
507
    }
508 509
    return true;
  }
510

511
  void _advanceStateMachine(bool shouldStartIfAccepted, PointerDeviceKind pointerDeviceKind) {
512 513
    if (_state == _ScaleState.ready)
      _state = _ScaleState.possible;
514

515 516
    if (_state == _ScaleState.possible) {
      final double spanDelta = (_currentSpan - _initialSpan).abs();
517
      final double focalPointDelta = (_currentFocalPoint! - _initialFocalPoint).distance;
518
      if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind, gestureSettings))
519 520
        resolve(GestureDisposition.accepted);
    } else if (_state.index >= _ScaleState.accepted.index) {
521 522 523
      resolve(GestureDisposition.accepted);
    }

524 525 526
    if (_state == _ScaleState.accepted && shouldStartIfAccepted) {
      _state = _ScaleState.started;
      _dispatchOnStartCallbackIfNeeded();
527 528
    }

529
    if (_state == _ScaleState.started && onUpdate != null)
530
      invokeCallback<void>('onUpdate', () {
531
        onUpdate!(ScaleUpdateDetails(
532 533 534
          scale: _scaleFactor,
          horizontalScale: _horizontalScaleFactor,
          verticalScale: _verticalScaleFactor,
535 536
          focalPoint: _currentFocalPoint!,
          localFocalPoint: _localFocalPoint,
537
          rotation: _computeRotationFactor(),
538
          pointerCount: _pointerQueue.length,
539
          focalPointDelta: _delta,
540 541
        ));
      });
542 543 544 545 546
  }

  void _dispatchOnStartCallbackIfNeeded() {
    assert(_state == _ScaleState.started);
    if (onStart != null)
547
      invokeCallback<void>('onStart', () {
548
        onStart!(ScaleStartDetails(
549 550
          focalPoint: _currentFocalPoint!,
          localFocalPoint: _localFocalPoint,
551
          pointerCount: _pointerQueue.length,
552 553
        ));
      });
554 555
  }

556
  @override
557
  void acceptGesture(int pointer) {
558 559 560
    if (_state == _ScaleState.possible) {
      _state = _ScaleState.started;
      _dispatchOnStartCallbackIfNeeded();
561
      if (dragStartBehavior == DragStartBehavior.start) {
562
        _initialFocalPoint = _currentFocalPoint!;
563 564 565 566 567
        _initialSpan = _currentSpan;
        _initialLine = _currentLine;
        _initialHorizontalSpan = _currentHorizontalSpan;
        _initialVerticalSpan = _currentVerticalSpan;
      }
568 569 570
    }
  }

571 572 573 574 575
  @override
  void rejectGesture(int pointer) {
    stopTrackingPointer(pointer);
  }

576
  @override
577
  void didStopTrackingLastPointer(int pointer) {
578
    switch (_state) {
579
      case _ScaleState.possible:
580 581
        resolve(GestureDisposition.rejected);
        break;
582
      case _ScaleState.ready:
583
        assert(false); // We should have not seen a pointer yet
584
        break;
585
      case _ScaleState.accepted:
586
        break;
587
      case _ScaleState.started:
588
        assert(false); // We should be in the accepted state when user is done
589 590
        break;
    }
591
    _state = _ScaleState.ready;
592
  }
593

594 595 596 597 598 599
  @override
  void dispose() {
    _velocityTrackers.clear();
    super.dispose();
  }

600
  @override
601
  String get debugDescription => 'scale';
602
}