scale.dart 20.2 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 68
  /// 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;

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
  }) : assert(focalPoint != null),
89
       assert(scale != null && scale >= 0.0),
90 91
       assert(horizontalScale != null && horizontalScale >= 0.0),
       assert(verticalScale != null && verticalScale >= 0.0),
92 93
       assert(rotation != null),
       localFocalPoint = localFocalPoint ?? focalPoint;
94

95 96 97
  /// The focal point of the pointers in contact with the screen.
  ///
  /// Reported in global coordinates.
98 99 100 101 102
  ///
  /// See also:
  ///
  ///  * [localFocalPoint], which is the same value reported in local
  ///    coordinates.
103
  final Offset focalPoint;
104

105 106 107 108 109 110 111 112 113 114 115
  /// 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;

116 117 118 119 120 121 122 123 124
  /// 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.
125
  final double scale;
126

127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
  /// 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;

149
  /// The angle implied by the first two pointers to enter in contact with
150 151 152
  /// the screen.
  ///
  /// Expressed in radians.
153 154
  final double rotation;

155 156 157 158 159 160
  /// 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;

161
  @override
162 163 164 165 166 167 168 169
  String toString() => 'ScaleUpdateDetails('
    'focalPoint: $focalPoint,'
    ' localFocalPoint: $localFocalPoint,'
    ' scale: $scale,'
    ' horizontalScale: $horizontalScale,'
    ' verticalScale: $verticalScale,'
    ' rotation: $rotation,'
    ' pointerCount: $pointerCount)';
170 171 172 173 174 175 176
}

/// Details for [GestureScaleEndCallback].
class ScaleEndDetails {
  /// Creates details for [GestureScaleEndCallback].
  ///
  /// The [velocity] argument must not be null.
177
  ScaleEndDetails({ this.velocity = Velocity.zero, this.pointerCount = 0 })
178
    : assert(velocity != null);
179 180 181

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

183 184 185 186 187 188
  /// 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;

189
  @override
190
  String toString() => 'ScaleEndDetails(velocity: $velocity, pointerCount: $pointerCount)';
191 192
}

193 194
/// Signature for when the pointers in contact with the screen have established
/// a focal point and initial scale of 1.0.
195
typedef GestureScaleStartCallback = void Function(ScaleStartDetails details);
196 197 198

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

/// Signature for when the pointers are no longer in contact with the screen.
202
typedef GestureScaleEndCallback = void Function(ScaleEndDetails details);
203 204 205 206 207 208

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

210 211 212 213 214 215 216 217 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.
class _LineBetweenPointers{

  /// 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,
224
    this.pointerEndId = 1,
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
  }) : 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;

}


240 241 242
/// Recognizes a scale gesture.
///
/// [ScaleGestureRecognizer] tracks the pointers in contact with the screen and
Ian Hickson's avatar
Ian Hickson committed
243 244 245 246
/// 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].
247
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
248
  /// Create a gesture recognizer for interactions intended for scaling content.
249
  ///
250
  /// {@macro flutter.gestures.GestureRecognizer.kind}
251
  ScaleGestureRecognizer({
252 253
    Object? debugOwner,
    PointerDeviceKind? kind,
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278
    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;
279

280 281
  /// The pointers in contact with the screen have established a focal point and
  /// initial scale of 1.0.
282 283 284 285 286 287 288 289
  ///
  /// 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.
290
  GestureScaleStartCallback? onStart;
291 292 293

  /// The pointers in contact with the screen have indicated a new focal point
  /// and/or scale.
294
  GestureScaleUpdateCallback? onUpdate;
295 296

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

299
  _ScaleState _state = _ScaleState.ready;
300

301 302 303 304 305 306 307 308 309 310 311 312 313 314
  Matrix4? _lastTransform;

  late Offset _initialFocalPoint;
  late Offset _currentFocalPoint;
  late double _initialSpan;
  late double _currentSpan;
  late double _initialHorizontalSpan;
  late double _currentHorizontalSpan;
  late double _initialVerticalSpan;
  late double _currentVerticalSpan;
  _LineBetweenPointers? _initialLine;
  _LineBetweenPointers? _currentLine;
  late Map<int, Offset> _pointerLocations;
  late List<int> _pointerQueue; // A queue to sort pointers in order of entrance
315
  final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
316

317
  double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
318

319 320 321 322
  double get _horizontalScaleFactor => _initialHorizontalSpan > 0.0 ? _currentHorizontalSpan / _initialHorizontalSpan : 1.0;

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

323 324 325 326
  double _computeRotationFactor() {
    if (_initialLine == null || _currentLine == null) {
      return 0.0;
    }
327 328 329 330
    final double fx = _initialLine!.pointerStartLocation.dx;
    final double fy = _initialLine!.pointerStartLocation.dy;
    final double sx = _initialLine!.pointerEndLocation.dx;
    final double sy = _initialLine!.pointerEndLocation.dy;
331

332 333 334 335
    final double nfx = _currentLine!.pointerStartLocation.dx;
    final double nfy = _currentLine!.pointerStartLocation.dy;
    final double nsx = _currentLine!.pointerEndLocation.dx;
    final double nsy = _currentLine!.pointerEndLocation.dy;
336 337 338 339 340 341 342

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

    return angle2 - angle1;
  }

343
  @override
344
  void addAllowedPointer(PointerEvent event) {
345
    startTrackingPointer(event.pointer, event.transform);
346
    _velocityTrackers[event.pointer] = VelocityTracker.withKind(event.kind);
347 348
    if (_state == _ScaleState.ready) {
      _state = _ScaleState.possible;
349 350
      _initialSpan = 0.0;
      _currentSpan = 0.0;
351 352 353 354
      _initialHorizontalSpan = 0.0;
      _currentHorizontalSpan = 0.0;
      _initialVerticalSpan = 0.0;
      _currentVerticalSpan = 0.0;
355
      _pointerLocations = <int, Offset>{};
356
      _pointerQueue = <int>[];
357 358 359
    }
  }

360
  @override
Ian Hickson's avatar
Ian Hickson committed
361
  void handleEvent(PointerEvent event) {
362 363 364
    assert(_state != _ScaleState.ready);
    bool didChangeConfiguration = false;
    bool shouldStartIfAccepted = false;
Ian Hickson's avatar
Ian Hickson committed
365
    if (event is PointerMoveEvent) {
366
      final VelocityTracker tracker = _velocityTrackers[event.pointer]!;
367 368
      if (!event.synthesized)
        tracker.addPosition(event.timeStamp, event.position);
Ian Hickson's avatar
Ian Hickson committed
369
      _pointerLocations[event.pointer] = event.position;
370
      shouldStartIfAccepted = true;
371
      _lastTransform = event.transform;
Ian Hickson's avatar
Ian Hickson committed
372 373
    } else if (event is PointerDownEvent) {
      _pointerLocations[event.pointer] = event.position;
374
      _pointerQueue.add(event.pointer);
375 376
      didChangeConfiguration = true;
      shouldStartIfAccepted = true;
377
      _lastTransform = event.transform;
378
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
Ian Hickson's avatar
Ian Hickson committed
379
      _pointerLocations.remove(event.pointer);
380
      _pointerQueue.remove(event.pointer);
381
      didChangeConfiguration = true;
382
      _lastTransform = event.transform;
383 384
    }

385
    _updateLines();
386
    _update();
387

388
    if (!didChangeConfiguration || _reconfigure(event.pointer))
389
      _advanceStateMachine(shouldStartIfAccepted, event.kind);
390 391 392
    stopTrackingIfPointerNoLongerDown(event);
  }

393
  void _update() {
394
    final int count = _pointerLocations.keys.length;
395 396

    // Compute the focal point
397
    Offset focalPoint = Offset.zero;
398
    for (final int pointer in _pointerLocations.keys)
399
      focalPoint += _pointerLocations[pointer]!;
400
    _currentFocalPoint = count > 0 ? focalPoint / count.toDouble() : Offset.zero;
401

402 403 404
    // 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.
405
    double totalDeviation = 0.0;
406 407
    double totalHorizontalDeviation = 0.0;
    double totalVerticalDeviation = 0.0;
408
    for (final int pointer in _pointerLocations.keys) {
409 410 411
      totalDeviation += (_currentFocalPoint - _pointerLocations[pointer]!).distance;
      totalHorizontalDeviation += (_currentFocalPoint.dx - _pointerLocations[pointer]!.dx).abs();
      totalVerticalDeviation += (_currentFocalPoint.dy - _pointerLocations[pointer]!.dy).abs();
412
    }
413
    _currentSpan = count > 0 ? totalDeviation / count : 0.0;
414 415
    _currentHorizontalSpan = count > 0 ? totalHorizontalDeviation / count : 0.0;
    _currentVerticalSpan = count > 0 ? totalVerticalDeviation / count : 0.0;
416
  }
417

418
  /// Updates [_initialLine] and [_currentLine] accordingly to the situation of
419
  /// the registered pointers.
420 421 422 423 424 425 426
  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 &&
427 428
      _initialLine!.pointerStartId == _pointerQueue[0] &&
      _initialLine!.pointerEndId == _pointerQueue[1]) {
429 430 431
      /// Rotation updated, set the [_currentLine]
      _currentLine = _LineBetweenPointers(
        pointerStartId: _pointerQueue[0],
432
        pointerStartLocation: _pointerLocations[_pointerQueue[0]]!,
433
        pointerEndId: _pointerQueue[1],
434
        pointerEndLocation: _pointerLocations[_pointerQueue[1]]!,
435 436 437 438 439
      );
    } else {
      /// A new rotation process is on the way, set the [_initialLine]
      _initialLine = _LineBetweenPointers(
        pointerStartId: _pointerQueue[0],
440
        pointerStartLocation: _pointerLocations[_pointerQueue[0]]!,
441
        pointerEndId: _pointerQueue[1],
442
        pointerEndLocation: _pointerLocations[_pointerQueue[1]]!,
443 444 445 446 447
      );
      _currentLine = null;
    }
  }

448 449 450
  bool _reconfigure(int pointer) {
    _initialFocalPoint = _currentFocalPoint;
    _initialSpan = _currentSpan;
451
    _initialLine = _currentLine;
452 453
    _initialHorizontalSpan = _currentHorizontalSpan;
    _initialVerticalSpan = _currentVerticalSpan;
454 455
    if (_state == _ScaleState.started) {
      if (onEnd != null) {
456
        final VelocityTracker tracker = _velocityTrackers[pointer]!;
457 458

        Velocity velocity = tracker.getVelocity();
459
        if (_isFlingGesture(velocity)) {
460 461
          final Offset pixelsPerSecond = velocity.pixelsPerSecond;
          if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
462
            velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
463
          invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity, pointerCount: _pointerQueue.length)));
464
        } else {
465
          invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: Velocity.zero, pointerCount: _pointerQueue.length)));
466
        }
467
      }
468 469
      _state = _ScaleState.accepted;
      return false;
470
    }
471 472
    return true;
  }
473

474
  void _advanceStateMachine(bool shouldStartIfAccepted, PointerDeviceKind pointerDeviceKind) {
475 476
    if (_state == _ScaleState.ready)
      _state = _ScaleState.possible;
477

478 479 480
    if (_state == _ScaleState.possible) {
      final double spanDelta = (_currentSpan - _initialSpan).abs();
      final double focalPointDelta = (_currentFocalPoint - _initialFocalPoint).distance;
481
      if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind))
482 483
        resolve(GestureDisposition.accepted);
    } else if (_state.index >= _ScaleState.accepted.index) {
484 485 486
      resolve(GestureDisposition.accepted);
    }

487 488 489
    if (_state == _ScaleState.accepted && shouldStartIfAccepted) {
      _state = _ScaleState.started;
      _dispatchOnStartCallbackIfNeeded();
490 491
    }

492
    if (_state == _ScaleState.started && onUpdate != null)
493
      invokeCallback<void>('onUpdate', () {
494
        onUpdate!(ScaleUpdateDetails(
495 496 497 498
          scale: _scaleFactor,
          horizontalScale: _horizontalScaleFactor,
          verticalScale: _verticalScaleFactor,
          focalPoint: _currentFocalPoint,
499
          localFocalPoint: PointerEvent.transformPosition(_lastTransform, _currentFocalPoint),
500
          rotation: _computeRotationFactor(),
501
          pointerCount: _pointerQueue.length,
502 503
        ));
      });
504 505 506 507 508
  }

  void _dispatchOnStartCallbackIfNeeded() {
    assert(_state == _ScaleState.started);
    if (onStart != null)
509
      invokeCallback<void>('onStart', () {
510
        onStart!(ScaleStartDetails(
511 512
          focalPoint: _currentFocalPoint,
          localFocalPoint: PointerEvent.transformPosition(_lastTransform, _currentFocalPoint),
513
          pointerCount: _pointerQueue.length,
514 515
        ));
      });
516 517
  }

518
  @override
519
  void acceptGesture(int pointer) {
520 521 522
    if (_state == _ScaleState.possible) {
      _state = _ScaleState.started;
      _dispatchOnStartCallbackIfNeeded();
523 524 525 526 527 528 529
      if (dragStartBehavior == DragStartBehavior.start) {
        _initialFocalPoint = _currentFocalPoint;
        _initialSpan = _currentSpan;
        _initialLine = _currentLine;
        _initialHorizontalSpan = _currentHorizontalSpan;
        _initialVerticalSpan = _currentVerticalSpan;
      }
530 531 532
    }
  }

533 534 535 536 537
  @override
  void rejectGesture(int pointer) {
    stopTrackingPointer(pointer);
  }

538
  @override
539
  void didStopTrackingLastPointer(int pointer) {
540
    switch (_state) {
541
      case _ScaleState.possible:
542 543
        resolve(GestureDisposition.rejected);
        break;
544
      case _ScaleState.ready:
545
        assert(false); // We should have not seen a pointer yet
546
        break;
547
      case _ScaleState.accepted:
548
        break;
549
      case _ScaleState.started:
550
        assert(false); // We should be in the accepted state when user is done
551 552
        break;
    }
553
    _state = _ScaleState.ready;
554
  }
555

556 557 558 559 560 561
  @override
  void dispose() {
    _velocityTrackers.clear();
    super.dispose();
  }

562
  @override
563
  String get debugDescription => 'scale';
564
}