scale.dart 9.68 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
  ScaleStartDetails({ this.focalPoint: Offset.zero }) {
36 37 38 39 40
    assert(focalPoint != null);
  }

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

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

/// 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.
53
  ScaleUpdateDetails({ this.focalPoint: Offset.zero, this.scale: 1.0 }) {
54 55 56 57 58 59
    assert(focalPoint != null);
    assert(scale != null && scale >= 0.0);
  }

  /// 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 75 76 77 78 79 80
}

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

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

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

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

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

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

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

103 104 105 106 107 108 109
/// 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].
110
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
111 112
  /// The pointers in contact with the screen have established a focal point and
  /// initial scale of 1.0.
113
  GestureScaleStartCallback onStart;
114 115 116

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

  /// The pointers are no longer in contact with the screen.
120
  GestureScaleEndCallback onEnd;
121

122
  _ScaleState _state = _ScaleState.ready;
123

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

131
  double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
132

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

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

165 166 167
    _update();
    if (!didChangeConfiguration || _reconfigure(event.pointer))
      _advanceStateMachine(shouldStartIfAccepted);
168 169 170
    stopTrackingIfPointerNoLongerDown(event);
  }

171
  void _update() {
172
    final int count = _pointerLocations.keys.length;
173 174

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

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

187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
  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();
        if (velocity != null && _isFlingGesture(velocity)) {
          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
203
        }
204
      }
205 206
      _state = _ScaleState.accepted;
      return false;
207
    }
208 209
    return true;
  }
210

211 212 213
  void _advanceStateMachine(bool shouldStartIfAccepted) {
    if (_state == _ScaleState.ready)
      _state = _ScaleState.possible;
214

215 216 217 218 219 220
    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) {
221 222 223
      resolve(GestureDisposition.accepted);
    }

224 225 226
    if (_state == _ScaleState.accepted && shouldStartIfAccepted) {
      _state = _ScaleState.started;
      _dispatchOnStartCallbackIfNeeded();
227 228
    }

229 230 231 232 233 234 235 236
    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
237 238
  }

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

247 248 249 250 251
  @override
  void rejectGesture(int pointer) {
    stopTrackingPointer(pointer);
  }

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

270 271 272 273 274 275
  @override
  void dispose() {
    _velocityTrackers.clear();
    super.dispose();
  }

276
  @override
277
  String toStringShort() => 'scale';
278
}