scale.dart 13.1 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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 'arena.dart';
import 'constants.dart';
9
import 'events.dart';
10
import 'recognizer.dart';
11
import 'velocity_tracker.dart';
12

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

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

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

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

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

  /// The initial focal point of the pointers in contact with the screen.
  /// Reported in global coordinates.
42
  final Offset focalPoint;
43 44 45

  @override
  String toString() => 'ScaleStartDetails(focalPoint: $focalPoint)';
46 47 48 49 50 51
}

/// Details for [GestureScaleUpdateCallback].
class ScaleUpdateDetails {
  /// Creates details for [GestureScaleUpdateCallback].
  ///
52
  /// The [focalPoint], [scale], [rotation] arguments must not be null. The [scale]
53
  /// argument must be greater than or equal to zero.
54
  ScaleUpdateDetails({
55 56
    this.focalPoint = Offset.zero,
    this.scale = 1.0,
57
    this.rotation = 0.0,
58
  }) : assert(focalPoint != null),
59 60
       assert(scale != null && scale >= 0.0),
       assert(rotation != null);
61 62 63

  /// The focal point of the pointers in contact with the screen. Reported in
  /// global coordinates.
64
  final Offset focalPoint;
65 66 67 68

  /// The scale implied by the pointers in contact with the screen. A value
  /// greater than or equal to zero.
  final double scale;
69

70 71 72 73
  /// The angle implied by the first two pointers to enter in contact with
  /// the screen. Expressed in radians.
  final double rotation;

74
  @override
75
  String toString() => 'ScaleUpdateDetails(focalPoint: $focalPoint, scale: $scale, rotation: $rotation)';
76 77 78 79 80 81 82
}

/// Details for [GestureScaleEndCallback].
class ScaleEndDetails {
  /// Creates details for [GestureScaleEndCallback].
  ///
  /// The [velocity] argument must not be null.
83
  ScaleEndDetails({ this.velocity = Velocity.zero })
84
    : assert(velocity != null);
85 86 87

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

  @override
  String toString() => 'ScaleEndDetails(velocity: $velocity)';
91 92
}

93 94
/// Signature for when the pointers in contact with the screen have established
/// a focal point and initial scale of 1.0.
95
typedef GestureScaleStartCallback = void Function(ScaleStartDetails details);
96 97 98

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

/// Signature for when the pointers are no longer in contact with the screen.
102
typedef GestureScaleEndCallback = void Function(ScaleEndDetails details);
103 104 105 106 107 108

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

110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139

/// 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,
    this.pointerEndId = 1
  }) : 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;

}


140 141 142
/// Recognizes a scale gesture.
///
/// [ScaleGestureRecognizer] tracks the pointers in contact with the screen and
Ian Hickson's avatar
Ian Hickson committed
143 144 145 146
/// 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].
147
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
148 149 150
  /// Create a gesture recognizer for interactions intended for scaling content.
  ScaleGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);

151 152
  /// The pointers in contact with the screen have established a focal point and
  /// initial scale of 1.0.
153
  GestureScaleStartCallback onStart;
154 155 156

  /// The pointers in contact with the screen have indicated a new focal point
  /// and/or scale.
157
  GestureScaleUpdateCallback onUpdate;
158 159

  /// The pointers are no longer in contact with the screen.
160
  GestureScaleEndCallback onEnd;
161

162
  _ScaleState _state = _ScaleState.ready;
163

164 165
  Offset _initialFocalPoint;
  Offset _currentFocalPoint;
166 167
  double _initialSpan;
  double _currentSpan;
168 169
  _LineBetweenPointers _initialLine;
  _LineBetweenPointers _currentLine;
170
  Map<int, Offset> _pointerLocations;
171
  List<int> _pointerQueue; /// A queue to sort pointers in order of entrance
172
  final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
173

174
  double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
175

176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
  double _computeRotationFactor() {
    if (_initialLine == null || _currentLine == null) {
      return 0.0;
    }
    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);

    return angle2 - angle1;
  }

196
  @override
Ian Hickson's avatar
Ian Hickson committed
197
  void addPointer(PointerEvent event) {
198
    startTrackingPointer(event.pointer);
199
    _velocityTrackers[event.pointer] = VelocityTracker();
200 201
    if (_state == _ScaleState.ready) {
      _state = _ScaleState.possible;
202 203
      _initialSpan = 0.0;
      _currentSpan = 0.0;
204
      _pointerLocations = <int, Offset>{};
205
      _pointerQueue = <int>[];
206 207 208
    }
  }

209
  @override
Ian Hickson's avatar
Ian Hickson committed
210
  void handleEvent(PointerEvent event) {
211 212 213
    assert(_state != _ScaleState.ready);
    bool didChangeConfiguration = false;
    bool shouldStartIfAccepted = false;
Ian Hickson's avatar
Ian Hickson committed
214
    if (event is PointerMoveEvent) {
215
      final VelocityTracker tracker = _velocityTrackers[event.pointer];
216
      assert(tracker != null);
217 218
      if (!event.synthesized)
        tracker.addPosition(event.timeStamp, event.position);
Ian Hickson's avatar
Ian Hickson committed
219
      _pointerLocations[event.pointer] = event.position;
220
      shouldStartIfAccepted = true;
Ian Hickson's avatar
Ian Hickson committed
221 222
    } else if (event is PointerDownEvent) {
      _pointerLocations[event.pointer] = event.position;
223
      _pointerQueue.add(event.pointer);
224 225 226
      didChangeConfiguration = true;
      shouldStartIfAccepted = true;
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
Ian Hickson's avatar
Ian Hickson committed
227
      _pointerLocations.remove(event.pointer);
228
      _pointerQueue.remove(event.pointer);
229
      didChangeConfiguration = true;
230 231
    }

232
    _updateLines();
233
    _update();
234

235 236
    if (!didChangeConfiguration || _reconfigure(event.pointer))
      _advanceStateMachine(shouldStartIfAccepted);
237 238 239
    stopTrackingIfPointerNoLongerDown(event);
  }

240
  void _update() {
241
    final int count = _pointerLocations.keys.length;
242 243

    // Compute the focal point
244
    Offset focalPoint = Offset.zero;
245
    for (int pointer in _pointerLocations.keys)
246 247
      focalPoint += _pointerLocations[pointer];
    _currentFocalPoint = count > 0 ? focalPoint / count.toDouble() : Offset.zero;
248 249 250 251

    // Span is the average deviation from focal point
    double totalDeviation = 0.0;
    for (int pointer in _pointerLocations.keys)
252
      totalDeviation += (_currentFocalPoint - _pointerLocations[pointer]).distance;
253
    _currentSpan = count > 0 ? totalDeviation / count : 0.0;
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 279 280 281 282 283 284 285
  /// Updates [_initialLine] and [_currentLine] accordingly to the situation of
  /// the registered pointers
  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 &&
      _initialLine.pointerStartId == _pointerQueue[0] &&
      _initialLine.pointerEndId == _pointerQueue[1]) {
      /// Rotation updated, set the [_currentLine]
      _currentLine = _LineBetweenPointers(
        pointerStartId: _pointerQueue[0],
        pointerStartLocation: _pointerLocations[_pointerQueue[0]],
        pointerEndId: _pointerQueue[1],
        pointerEndLocation: _pointerLocations[ _pointerQueue[1]]
      );
    } else {
      /// A new rotation process is on the way, set the [_initialLine]
      _initialLine = _LineBetweenPointers(
        pointerStartId: _pointerQueue[0],
        pointerStartLocation: _pointerLocations[_pointerQueue[0]],
        pointerEndId: _pointerQueue[1],
        pointerEndLocation: _pointerLocations[ _pointerQueue[1]]
      );
      _currentLine = null;
    }
  }

286 287 288
  bool _reconfigure(int pointer) {
    _initialFocalPoint = _currentFocalPoint;
    _initialSpan = _currentSpan;
289
    _initialLine = _currentLine;
290 291 292 293 294 295
    if (_state == _ScaleState.started) {
      if (onEnd != null) {
        final VelocityTracker tracker = _velocityTrackers[pointer];
        assert(tracker != null);

        Velocity velocity = tracker.getVelocity();
296
        if (_isFlingGesture(velocity)) {
297 298
          final Offset pixelsPerSecond = velocity.pixelsPerSecond;
          if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
299 300
            velocity = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
          invokeCallback<void>('onEnd', () => onEnd(ScaleEndDetails(velocity: velocity)));
301
        } else {
302
          invokeCallback<void>('onEnd', () => onEnd(ScaleEndDetails(velocity: Velocity.zero)));
303
        }
304
      }
305 306
      _state = _ScaleState.accepted;
      return false;
307
    }
308 309
    return true;
  }
310

311 312 313
  void _advanceStateMachine(bool shouldStartIfAccepted) {
    if (_state == _ScaleState.ready)
      _state = _ScaleState.possible;
314

315 316 317 318 319 320
    if (_state == _ScaleState.possible) {
      final double spanDelta = (_currentSpan - _initialSpan).abs();
      final double focalPointDelta = (_currentFocalPoint - _initialFocalPoint).distance;
      if (spanDelta > kScaleSlop || focalPointDelta > kPanSlop)
        resolve(GestureDisposition.accepted);
    } else if (_state.index >= _ScaleState.accepted.index) {
321 322 323
      resolve(GestureDisposition.accepted);
    }

324 325 326
    if (_state == _ScaleState.accepted && shouldStartIfAccepted) {
      _state = _ScaleState.started;
      _dispatchOnStartCallbackIfNeeded();
327 328
    }

329
    if (_state == _ScaleState.started && onUpdate != null)
330
      invokeCallback<void>('onUpdate', () => onUpdate(ScaleUpdateDetails(scale: _scaleFactor, focalPoint: _currentFocalPoint, rotation: _computeRotationFactor())));
331 332 333 334 335
  }

  void _dispatchOnStartCallbackIfNeeded() {
    assert(_state == _ScaleState.started);
    if (onStart != null)
336
      invokeCallback<void>('onStart', () => onStart(ScaleStartDetails(focalPoint: _currentFocalPoint)));
337 338
  }

339
  @override
340
  void acceptGesture(int pointer) {
341 342 343
    if (_state == _ScaleState.possible) {
      _state = _ScaleState.started;
      _dispatchOnStartCallbackIfNeeded();
344 345 346
    }
  }

347 348 349 350 351
  @override
  void rejectGesture(int pointer) {
    stopTrackingPointer(pointer);
  }

352
  @override
353
  void didStopTrackingLastPointer(int pointer) {
354
    switch (_state) {
355
      case _ScaleState.possible:
356 357
        resolve(GestureDisposition.rejected);
        break;
358
      case _ScaleState.ready:
359
        assert(false); // We should have not seen a pointer yet
360
        break;
361
      case _ScaleState.accepted:
362
        break;
363
      case _ScaleState.started:
364
        assert(false); // We should be in the accepted state when user is done
365 366
        break;
    }
367
    _state = _ScaleState.ready;
368
  }
369

370 371 372 373 374 375
  @override
  void dispose() {
    _velocityTrackers.clear();
    super.dispose();
  }

376
  @override
377
  String get debugDescription => 'scale';
378
}