scale.dart 17.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 7
import 'dart:math' as math;

8 9
import 'package:vector_math/vector_math_64.dart';

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

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

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

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

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

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

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

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

64
  @override
65
  String toString() => 'ScaleStartDetails(focalPoint: $focalPoint, localFocalPoint: $localFocalPoint)';
66 67 68 69 70 71
}

/// Details for [GestureScaleUpdateCallback].
class ScaleUpdateDetails {
  /// Creates details for [GestureScaleUpdateCallback].
  ///
72 73
  /// The [focalPoint], [scale], [horizontalScale], [verticalScale], [rotation]
  /// arguments must not be null. The [scale], [horizontalScale], and [verticalScale]
74
  /// argument must be greater than or equal to zero.
75
  ScaleUpdateDetails({
76
    this.focalPoint = Offset.zero,
77
    Offset? localFocalPoint,
78
    this.scale = 1.0,
79 80
    this.horizontalScale = 1.0,
    this.verticalScale = 1.0,
81
    this.rotation = 0.0,
82
  }) : assert(focalPoint != null),
83
       assert(scale != null && scale >= 0.0),
84 85
       assert(horizontalScale != null && horizontalScale >= 0.0),
       assert(verticalScale != null && verticalScale >= 0.0),
86 87
       assert(rotation != null),
       localFocalPoint = localFocalPoint ?? focalPoint;
88

89 90 91
  /// The focal point of the pointers in contact with the screen.
  ///
  /// Reported in global coordinates.
92 93 94 95 96
  ///
  /// See also:
  ///
  ///  * [localFocalPoint], which is the same value reported in local
  ///    coordinates.
97
  final Offset focalPoint;
98

99 100 101 102 103 104 105 106 107 108 109
  /// 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;

110 111 112 113 114 115 116 117 118
  /// 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.
119
  final double scale;
120

121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
  /// 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;

143
  /// The angle implied by the first two pointers to enter in contact with
144 145 146
  /// the screen.
  ///
  /// Expressed in radians.
147 148
  final double rotation;

149
  @override
150
  String toString() => 'ScaleUpdateDetails(focalPoint: $focalPoint, localFocalPoint: $localFocalPoint, scale: $scale, horizontalScale: $horizontalScale, verticalScale: $verticalScale, rotation: $rotation)';
151 152 153 154 155 156 157
}

/// Details for [GestureScaleEndCallback].
class ScaleEndDetails {
  /// Creates details for [GestureScaleEndCallback].
  ///
  /// The [velocity] argument must not be null.
158
  ScaleEndDetails({ this.velocity = Velocity.zero })
159
    : assert(velocity != null);
160 161 162

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

  @override
  String toString() => 'ScaleEndDetails(velocity: $velocity)';
166 167
}

168 169
/// Signature for when the pointers in contact with the screen have established
/// a focal point and initial scale of 1.0.
170
typedef GestureScaleStartCallback = void Function(ScaleStartDetails details);
171 172 173

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

/// Signature for when the pointers are no longer in contact with the screen.
177
typedef GestureScaleEndCallback = void Function(ScaleEndDetails details);
178 179 180 181 182 183

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

185 186 187 188 189 190 191 192 193 194 195 196 197 198

/// 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,
199
    this.pointerEndId = 1,
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
  }) : 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;

}


215 216 217
/// Recognizes a scale gesture.
///
/// [ScaleGestureRecognizer] tracks the pointers in contact with the screen and
Ian Hickson's avatar
Ian Hickson committed
218 219 220 221
/// 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].
222
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
223
  /// Create a gesture recognizer for interactions intended for scaling content.
224 225 226
  ///
  /// {@macro flutter.gestures.gestureRecognizer.kind}
  ScaleGestureRecognizer({
227 228
    Object? debugOwner,
    PointerDeviceKind? kind,
229
  }) : super(debugOwner: debugOwner, kind: kind);
230

231 232
  /// The pointers in contact with the screen have established a focal point and
  /// initial scale of 1.0.
233
  GestureScaleStartCallback? onStart;
234 235 236

  /// The pointers in contact with the screen have indicated a new focal point
  /// and/or scale.
237
  GestureScaleUpdateCallback? onUpdate;
238 239

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

242
  _ScaleState _state = _ScaleState.ready;
243

244 245 246 247 248 249 250 251 252 253 254 255 256 257
  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
258
  final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
259

260
  double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
261

262 263 264 265
  double get _horizontalScaleFactor => _initialHorizontalSpan > 0.0 ? _currentHorizontalSpan / _initialHorizontalSpan : 1.0;

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

266 267 268 269
  double _computeRotationFactor() {
    if (_initialLine == null || _currentLine == null) {
      return 0.0;
    }
270 271 272 273
    final double fx = _initialLine!.pointerStartLocation.dx;
    final double fy = _initialLine!.pointerStartLocation.dy;
    final double sx = _initialLine!.pointerEndLocation.dx;
    final double sy = _initialLine!.pointerEndLocation.dy;
274

275 276 277 278
    final double nfx = _currentLine!.pointerStartLocation.dx;
    final double nfy = _currentLine!.pointerStartLocation.dy;
    final double nsx = _currentLine!.pointerEndLocation.dx;
    final double nsy = _currentLine!.pointerEndLocation.dy;
279 280 281 282 283 284 285

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

    return angle2 - angle1;
  }

286
  @override
287
  void addAllowedPointer(PointerEvent event) {
288
    startTrackingPointer(event.pointer, event.transform);
289
    _velocityTrackers[event.pointer] = VelocityTracker.withKind(event.kind);
290 291
    if (_state == _ScaleState.ready) {
      _state = _ScaleState.possible;
292 293
      _initialSpan = 0.0;
      _currentSpan = 0.0;
294 295 296 297
      _initialHorizontalSpan = 0.0;
      _currentHorizontalSpan = 0.0;
      _initialVerticalSpan = 0.0;
      _currentVerticalSpan = 0.0;
298
      _pointerLocations = <int, Offset>{};
299
      _pointerQueue = <int>[];
300 301 302
    }
  }

303
  @override
Ian Hickson's avatar
Ian Hickson committed
304
  void handleEvent(PointerEvent event) {
305 306 307
    assert(_state != _ScaleState.ready);
    bool didChangeConfiguration = false;
    bool shouldStartIfAccepted = false;
Ian Hickson's avatar
Ian Hickson committed
308
    if (event is PointerMoveEvent) {
309
      final VelocityTracker tracker = _velocityTrackers[event.pointer]!;
310 311
      if (!event.synthesized)
        tracker.addPosition(event.timeStamp, event.position);
Ian Hickson's avatar
Ian Hickson committed
312
      _pointerLocations[event.pointer] = event.position;
313
      shouldStartIfAccepted = true;
314
      _lastTransform = event.transform;
Ian Hickson's avatar
Ian Hickson committed
315 316
    } else if (event is PointerDownEvent) {
      _pointerLocations[event.pointer] = event.position;
317
      _pointerQueue.add(event.pointer);
318 319
      didChangeConfiguration = true;
      shouldStartIfAccepted = true;
320
      _lastTransform = event.transform;
321
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
Ian Hickson's avatar
Ian Hickson committed
322
      _pointerLocations.remove(event.pointer);
323
      _pointerQueue.remove(event.pointer);
324
      didChangeConfiguration = true;
325
      _lastTransform = event.transform;
326 327
    }

328
    _updateLines();
329
    _update();
330

331
    if (!didChangeConfiguration || _reconfigure(event.pointer))
332
      _advanceStateMachine(shouldStartIfAccepted, event.kind);
333 334 335
    stopTrackingIfPointerNoLongerDown(event);
  }

336
  void _update() {
337
    final int count = _pointerLocations.keys.length;
338 339

    // Compute the focal point
340
    Offset focalPoint = Offset.zero;
341
    for (final int pointer in _pointerLocations.keys)
342
      focalPoint += _pointerLocations[pointer]!;
343
    _currentFocalPoint = count > 0 ? focalPoint / count.toDouble() : Offset.zero;
344

345 346 347
    // 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.
348
    double totalDeviation = 0.0;
349 350
    double totalHorizontalDeviation = 0.0;
    double totalVerticalDeviation = 0.0;
351
    for (final int pointer in _pointerLocations.keys) {
352 353 354
      totalDeviation += (_currentFocalPoint - _pointerLocations[pointer]!).distance;
      totalHorizontalDeviation += (_currentFocalPoint.dx - _pointerLocations[pointer]!.dx).abs();
      totalVerticalDeviation += (_currentFocalPoint.dy - _pointerLocations[pointer]!.dy).abs();
355
    }
356
    _currentSpan = count > 0 ? totalDeviation / count : 0.0;
357 358
    _currentHorizontalSpan = count > 0 ? totalHorizontalDeviation / count : 0.0;
    _currentVerticalSpan = count > 0 ? totalVerticalDeviation / count : 0.0;
359
  }
360

361
  /// Updates [_initialLine] and [_currentLine] accordingly to the situation of
362
  /// the registered pointers.
363 364 365 366 367 368 369
  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 &&
370 371
      _initialLine!.pointerStartId == _pointerQueue[0] &&
      _initialLine!.pointerEndId == _pointerQueue[1]) {
372 373 374
      /// Rotation updated, set the [_currentLine]
      _currentLine = _LineBetweenPointers(
        pointerStartId: _pointerQueue[0],
375
        pointerStartLocation: _pointerLocations[_pointerQueue[0]]!,
376
        pointerEndId: _pointerQueue[1],
377
        pointerEndLocation: _pointerLocations[_pointerQueue[1]]!,
378 379 380 381 382
      );
    } else {
      /// A new rotation process is on the way, set the [_initialLine]
      _initialLine = _LineBetweenPointers(
        pointerStartId: _pointerQueue[0],
383
        pointerStartLocation: _pointerLocations[_pointerQueue[0]]!,
384
        pointerEndId: _pointerQueue[1],
385
        pointerEndLocation: _pointerLocations[_pointerQueue[1]]!,
386 387 388 389 390
      );
      _currentLine = null;
    }
  }

391 392 393
  bool _reconfigure(int pointer) {
    _initialFocalPoint = _currentFocalPoint;
    _initialSpan = _currentSpan;
394
    _initialLine = _currentLine;
395 396
    _initialHorizontalSpan = _currentHorizontalSpan;
    _initialVerticalSpan = _currentVerticalSpan;
397 398
    if (_state == _ScaleState.started) {
      if (onEnd != null) {
399
        final VelocityTracker tracker = _velocityTrackers[pointer]!;
400 401

        Velocity velocity = tracker.getVelocity();
402
        if (_isFlingGesture(velocity)) {
403 404
          final Offset pixelsPerSecond = velocity.pixelsPerSecond;
          if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
405
            velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
406
          invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: velocity)));
407
        } else {
408
          invokeCallback<void>('onEnd', () => onEnd!(ScaleEndDetails(velocity: Velocity.zero)));
409
        }
410
      }
411 412
      _state = _ScaleState.accepted;
      return false;
413
    }
414 415
    return true;
  }
416

417
  void _advanceStateMachine(bool shouldStartIfAccepted, PointerDeviceKind pointerDeviceKind) {
418 419
    if (_state == _ScaleState.ready)
      _state = _ScaleState.possible;
420

421 422 423
    if (_state == _ScaleState.possible) {
      final double spanDelta = (_currentSpan - _initialSpan).abs();
      final double focalPointDelta = (_currentFocalPoint - _initialFocalPoint).distance;
424
      if (spanDelta > computeScaleSlop(pointerDeviceKind) || focalPointDelta > computePanSlop(pointerDeviceKind))
425 426
        resolve(GestureDisposition.accepted);
    } else if (_state.index >= _ScaleState.accepted.index) {
427 428 429
      resolve(GestureDisposition.accepted);
    }

430 431 432
    if (_state == _ScaleState.accepted && shouldStartIfAccepted) {
      _state = _ScaleState.started;
      _dispatchOnStartCallbackIfNeeded();
433 434
    }

435
    if (_state == _ScaleState.started && onUpdate != null)
436
      invokeCallback<void>('onUpdate', () {
437
        onUpdate!(ScaleUpdateDetails(
438 439 440 441
          scale: _scaleFactor,
          horizontalScale: _horizontalScaleFactor,
          verticalScale: _verticalScaleFactor,
          focalPoint: _currentFocalPoint,
442
          localFocalPoint: PointerEvent.transformPosition(_lastTransform, _currentFocalPoint),
443 444 445
          rotation: _computeRotationFactor(),
        ));
      });
446 447 448 449 450
  }

  void _dispatchOnStartCallbackIfNeeded() {
    assert(_state == _ScaleState.started);
    if (onStart != null)
451
      invokeCallback<void>('onStart', () {
452
        onStart!(ScaleStartDetails(
453 454 455 456
          focalPoint: _currentFocalPoint,
          localFocalPoint: PointerEvent.transformPosition(_lastTransform, _currentFocalPoint),
        ));
      });
457 458
  }

459
  @override
460
  void acceptGesture(int pointer) {
461 462 463
    if (_state == _ScaleState.possible) {
      _state = _ScaleState.started;
      _dispatchOnStartCallbackIfNeeded();
464 465 466
    }
  }

467 468 469 470 471
  @override
  void rejectGesture(int pointer) {
    stopTrackingPointer(pointer);
  }

472
  @override
473
  void didStopTrackingLastPointer(int pointer) {
474
    switch (_state) {
475
      case _ScaleState.possible:
476 477
        resolve(GestureDisposition.rejected);
        break;
478
      case _ScaleState.ready:
479
        assert(false); // We should have not seen a pointer yet
480
        break;
481
      case _ScaleState.accepted:
482
        break;
483
      case _ScaleState.started:
484
        assert(false); // We should be in the accepted state when user is done
485 486
        break;
    }
487
    _state = _ScaleState.ready;
488
  }
489

490 491 492 493 494 495
  @override
  void dispose() {
    _velocityTrackers.clear();
    super.dispose();
  }

496
  @override
497
  String get debugDescription => 'scale';
498
}