scale.dart 9.85 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 'arena.dart';
import 'constants.dart';
7
import 'events.dart';
8
import 'recognizer.dart';
9
import 'velocity_tracker.dart';
10

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

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

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

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

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

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

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

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

  /// The focal point of the pointers in contact with the screen. Reported in
  /// global coordinates.
60
  final Offset focalPoint;
61 62 63 64

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

  @override
  String toString() => 'ScaleUpdateDetails(focalPoint: $focalPoint, scale: $scale)';
68 69 70 71 72 73 74
}

/// Details for [GestureScaleEndCallback].
class ScaleEndDetails {
  /// Creates details for [GestureScaleEndCallback].
  ///
  /// The [velocity] argument must not be null.
75 76
  ScaleEndDetails({ this.velocity: Velocity.zero })
    : assert(velocity != null);
77 78 79

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

  @override
  String toString() => 'ScaleEndDetails(velocity: $velocity)';
83 84
}

85 86
/// Signature for when the pointers in contact with the screen have established
/// a focal point and initial scale of 1.0.
87
typedef void GestureScaleStartCallback(ScaleStartDetails details);
88 89 90

/// Signature for when the pointers in contact with the screen have indicated a
/// new focal point and/or scale.
91
typedef void GestureScaleUpdateCallback(ScaleUpdateDetails details);
92 93

/// Signature for when the pointers are no longer in contact with the screen.
94
typedef void GestureScaleEndCallback(ScaleEndDetails details);
95 96 97 98 99 100

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

102 103 104 105 106 107 108
/// Recognizes a scale gesture.
///
/// [ScaleGestureRecognizer] tracks the pointers in contact with the screen and
/// calculates their focal point and indiciated scale. When a focal pointer is
/// established, the recognizer calls [onStart]. As the focal point and scale
/// change, the recognizer calls [onUpdate]. When the pointers are no longer in
/// contact with the screen, the recognizer calls [onEnd].
109
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
110 111 112
  /// Create a gesture recognizer for interactions intended for scaling content.
  ScaleGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner);

113 114
  /// The pointers in contact with the screen have established a focal point and
  /// initial scale of 1.0.
115
  GestureScaleStartCallback onStart;
116 117 118

  /// The pointers in contact with the screen have indicated a new focal point
  /// and/or scale.
119
  GestureScaleUpdateCallback onUpdate;
120 121

  /// The pointers are no longer in contact with the screen.
122
  GestureScaleEndCallback onEnd;
123

124
  _ScaleState _state = _ScaleState.ready;
125

126 127
  Offset _initialFocalPoint;
  Offset _currentFocalPoint;
128 129
  double _initialSpan;
  double _currentSpan;
130
  Map<int, Offset> _pointerLocations;
131
  final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{};
132

133
  double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
134

135
  @override
Ian Hickson's avatar
Ian Hickson committed
136
  void addPointer(PointerEvent event) {
137
    startTrackingPointer(event.pointer);
138
    _velocityTrackers[event.pointer] = new VelocityTracker();
139 140
    if (_state == _ScaleState.ready) {
      _state = _ScaleState.possible;
141 142
      _initialSpan = 0.0;
      _currentSpan = 0.0;
143
      _pointerLocations = <int, Offset>{};
144 145 146
    }
  }

147
  @override
Ian Hickson's avatar
Ian Hickson committed
148
  void handleEvent(PointerEvent event) {
149 150 151
    assert(_state != _ScaleState.ready);
    bool didChangeConfiguration = false;
    bool shouldStartIfAccepted = false;
Ian Hickson's avatar
Ian Hickson committed
152
    if (event is PointerMoveEvent) {
153
      final VelocityTracker tracker = _velocityTrackers[event.pointer];
154
      assert(tracker != null);
155 156
      if (!event.synthesized)
        tracker.addPosition(event.timeStamp, event.position);
Ian Hickson's avatar
Ian Hickson committed
157
      _pointerLocations[event.pointer] = event.position;
158
      shouldStartIfAccepted = true;
Ian Hickson's avatar
Ian Hickson committed
159 160
    } else if (event is PointerDownEvent) {
      _pointerLocations[event.pointer] = event.position;
161 162 163
      didChangeConfiguration = true;
      shouldStartIfAccepted = true;
    } else if (event is PointerUpEvent || event is PointerCancelEvent) {
Ian Hickson's avatar
Ian Hickson committed
164
      _pointerLocations.remove(event.pointer);
165
      didChangeConfiguration = true;
166 167
    }

168 169 170
    _update();
    if (!didChangeConfiguration || _reconfigure(event.pointer))
      _advanceStateMachine(shouldStartIfAccepted);
171 172 173
    stopTrackingIfPointerNoLongerDown(event);
  }

174
  void _update() {
175
    final int count = _pointerLocations.keys.length;
176 177

    // Compute the focal point
178
    Offset focalPoint = Offset.zero;
179
    for (int pointer in _pointerLocations.keys)
180 181
      focalPoint += _pointerLocations[pointer];
    _currentFocalPoint = count > 0 ? focalPoint / count.toDouble() : Offset.zero;
182 183 184 185

    // Span is the average deviation from focal point
    double totalDeviation = 0.0;
    for (int pointer in _pointerLocations.keys)
186
      totalDeviation += (_currentFocalPoint - _pointerLocations[pointer]).distance;
187
    _currentSpan = count > 0 ? totalDeviation / count : 0.0;
188
  }
189

190 191 192 193 194 195 196 197 198
  bool _reconfigure(int pointer) {
    _initialFocalPoint = _currentFocalPoint;
    _initialSpan = _currentSpan;
    if (_state == _ScaleState.started) {
      if (onEnd != null) {
        final VelocityTracker tracker = _velocityTrackers[pointer];
        assert(tracker != null);

        Velocity velocity = tracker.getVelocity();
199
        if (_isFlingGesture(velocity)) {
200 201 202 203 204 205
          final Offset pixelsPerSecond = velocity.pixelsPerSecond;
          if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
            velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
          invokeCallback<Null>('onEnd', () => onEnd(new ScaleEndDetails(velocity: velocity))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
        } else {
          invokeCallback<Null>('onEnd', () => onEnd(new ScaleEndDetails(velocity: Velocity.zero))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
206
        }
207
      }
208 209
      _state = _ScaleState.accepted;
      return false;
210
    }
211 212
    return true;
  }
213

214 215 216
  void _advanceStateMachine(bool shouldStartIfAccepted) {
    if (_state == _ScaleState.ready)
      _state = _ScaleState.possible;
217

218 219 220 221 222 223
    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) {
224 225 226
      resolve(GestureDisposition.accepted);
    }

227 228 229
    if (_state == _ScaleState.accepted && shouldStartIfAccepted) {
      _state = _ScaleState.started;
      _dispatchOnStartCallbackIfNeeded();
230 231
    }

232 233 234 235 236 237 238 239
    if (_state == _ScaleState.started && onUpdate != null)
      invokeCallback<Null>('onUpdate', () => onUpdate(new ScaleUpdateDetails(scale: _scaleFactor, focalPoint: _currentFocalPoint))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
  }

  void _dispatchOnStartCallbackIfNeeded() {
    assert(_state == _ScaleState.started);
    if (onStart != null)
      invokeCallback<Null>('onStart', () => onStart(new ScaleStartDetails(focalPoint: _currentFocalPoint))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
240 241
  }

242
  @override
243
  void acceptGesture(int pointer) {
244 245 246
    if (_state == _ScaleState.possible) {
      _state = _ScaleState.started;
      _dispatchOnStartCallbackIfNeeded();
247 248 249
    }
  }

250 251 252 253 254
  @override
  void rejectGesture(int pointer) {
    stopTrackingPointer(pointer);
  }

255
  @override
256 257
  void didStopTrackingLastPointer(int pointer) {
    switch(_state) {
258
      case _ScaleState.possible:
259 260
        resolve(GestureDisposition.rejected);
        break;
261
      case _ScaleState.ready:
262
        assert(false);  // We should have not seen a pointer yet
263
        break;
264
      case _ScaleState.accepted:
265
        break;
266
      case _ScaleState.started:
267
        assert(false);  // We should be in the accepted state when user is done
268 269
        break;
    }
270
    _state = _ScaleState.ready;
271
  }
272

273 274 275 276 277 278
  @override
  void dispose() {
    _velocityTrackers.clear();
    super.dispose();
  }

279
  @override
280
  String get debugDescription => 'scale';
281
}