scale.dart 7.96 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 7
import 'arena.dart';
import 'recognizer.dart';
import 'constants.dart';
8
import 'events.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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
/// Details for [GestureScaleStartCallback].
class ScaleStartDetails {
  /// Creates details for [GestureScaleStartCallback].
  ///
  /// The [focalPoint] argument must not be null.
  ScaleStartDetails({ this.focalPoint: Point.origin }) {
    assert(focalPoint != null);
  }

  /// The initial focal point of the pointers in contact with the screen.
  /// Reported in global coordinates.
  final Point focalPoint;
}

/// 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.
  ScaleUpdateDetails({ this.focalPoint: Point.origin, this.scale: 1.0 }) {
    assert(focalPoint != null);
    assert(scale != null && scale >= 0.0);
  }

  /// The focal point of the pointers in contact with the screen. Reported in
  /// global coordinates.
  final Point focalPoint;

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

/// 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;
}

77 78
/// Signature for when the pointers in contact with the screen have established
/// a focal point and initial scale of 1.0.
79
typedef void GestureScaleStartCallback(ScaleStartDetails details);
80 81 82

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

/// Signature for when the pointers are no longer in contact with the screen.
86
typedef void GestureScaleEndCallback(ScaleEndDetails details);
87 88 89 90 91 92

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

94 95 96 97 98 99 100
/// 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].
101
class ScaleGestureRecognizer extends OneSequenceGestureRecognizer {
102 103
  /// The pointers in contact with the screen have established a focal point and
  /// initial scale of 1.0.
104
  GestureScaleStartCallback onStart;
105 106 107

  /// The pointers in contact with the screen have indicated a new focal point
  /// and/or scale.
108
  GestureScaleUpdateCallback onUpdate;
109 110

  /// The pointers are no longer in contact with the screen.
111
  GestureScaleEndCallback onEnd;
112

113
  ScaleState _state = ScaleState.ready;
114 115 116

  double _initialSpan;
  double _currentSpan;
Ian Hickson's avatar
Ian Hickson committed
117
  Map<int, Point> _pointerLocations;
118
  Map<int, VelocityTracker> _velocityTrackers = new Map<int, VelocityTracker>();
119

120
  double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0;
121

122
  @override
Ian Hickson's avatar
Ian Hickson committed
123
  void addPointer(PointerEvent event) {
124
    startTrackingPointer(event.pointer);
125
    _velocityTrackers[event.pointer] = new VelocityTracker();
126 127
    if (_state == ScaleState.ready) {
      _state = ScaleState.possible;
128 129
      _initialSpan = 0.0;
      _currentSpan = 0.0;
Ian Hickson's avatar
Ian Hickson committed
130
      _pointerLocations = new Map<int, Point>();
131 132 133
    }
  }

134
  @override
Ian Hickson's avatar
Ian Hickson committed
135
  void handleEvent(PointerEvent event) {
136
    assert(_state != ScaleState.ready);
137
    bool configChanged = false;
Ian Hickson's avatar
Ian Hickson committed
138
    if (event is PointerMoveEvent) {
139 140 141
      VelocityTracker tracker = _velocityTrackers[event.pointer];
      assert(tracker != null);
      tracker.addPosition(event.timeStamp, event.position);
Ian Hickson's avatar
Ian Hickson committed
142 143 144 145 146 147 148
      _pointerLocations[event.pointer] = event.position;
    } else if (event is PointerDownEvent) {
      configChanged = true;
      _pointerLocations[event.pointer] = event.position;
    } else if (event is PointerUpEvent) {
      configChanged = true;
      _pointerLocations.remove(event.pointer);
149 150
    }

151
    _update(configChanged, event.pointer);
152 153 154 155

    stopTrackingIfPointerNoLongerDown(event);
  }

156
  void _update(bool configChanged, int pointer) {
157 158 159
    int count = _pointerLocations.keys.length;

    // Compute the focal point
Ian Hickson's avatar
Ian Hickson committed
160
    Point focalPoint = Point.origin;
161 162
    for (int pointer in _pointerLocations.keys)
      focalPoint += _pointerLocations[pointer].toOffset();
Ian Hickson's avatar
Ian Hickson committed
163
    focalPoint = new Point(focalPoint.x / count, focalPoint.y / count);
164 165 166 167 168 169 170 171 172

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

    if (configChanged) {
      _initialSpan = _currentSpan;
173
      if (_state == ScaleState.started) {
174 175 176 177 178 179 180 181 182
        if (onEnd != null) {
          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);
183
            onEnd(new ScaleEndDetails(velocity: velocity));
184
          } else {
185
            onEnd(new ScaleEndDetails(velocity: Velocity.zero));
186 187
          }
        }
188
        _state = ScaleState.accepted;
189 190 191
      }
    }

192 193
    if (_state == ScaleState.ready)
      _state = ScaleState.possible;
194

195 196
    if (_state == ScaleState.possible &&
        (_currentSpan - _initialSpan).abs() > kScaleSlop) {
197 198 199
      resolve(GestureDisposition.accepted);
    }

200 201
    if (_state == ScaleState.accepted && !configChanged) {
      _state = ScaleState.started;
202
      if (onStart != null)
203
        onStart(new ScaleStartDetails(focalPoint: focalPoint));
204 205
    }

206
    if (_state == ScaleState.started && onUpdate != null)
207
      onUpdate(new ScaleUpdateDetails(scale: _scaleFactor, focalPoint: focalPoint));
208 209
  }

210
  @override
211
  void acceptGesture(int pointer) {
212 213
    if (_state != ScaleState.accepted) {
      _state = ScaleState.accepted;
214
      _update(false, pointer);
215 216 217
    }
  }

218
  @override
219 220
  void didStopTrackingLastPointer(int pointer) {
    switch(_state) {
221
      case ScaleState.possible:
222 223
        resolve(GestureDisposition.rejected);
        break;
224 225
      case ScaleState.ready:
        assert(false);  // We should have not seen a pointer yet
226
        break;
227
      case ScaleState.accepted:
228
        break;
229 230
      case ScaleState.started:
        assert(false);  // We should be in the accepted state when user is done
231 232
        break;
    }
233
    _state = ScaleState.ready;
234
  }
235

236 237 238 239 240 241
  @override
  void dispose() {
    _velocityTrackers.clear();
    super.dispose();
  }

242
  @override
243
  String toStringShort() => 'scale';
244
}