| // 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. |
| |
| import 'arena.dart'; |
| import 'constants.dart'; |
| import 'events.dart'; |
| import 'recognizer.dart'; |
| import 'velocity_tracker.dart'; |
| |
| /// The possible states of a [ScaleGestureRecognizer]. |
| enum _ScaleState { |
| /// The recognizer is ready to start recognizing a gesture. |
| ready, |
| |
| /// The sequence of pointer events seen thus far is consistent with a scale |
| /// gesture but the gesture has not been accepted definitively. |
| possible, |
| |
| /// The sequence of pointer events seen thus far has been accepted |
| /// definitively as a scale gesture. |
| accepted, |
| |
| /// The sequence of pointer events seen thus far has been accepted |
| /// definitively as a scale gesture and the pointers established a focal point |
| /// and initial scale. |
| started, |
| } |
| |
| /// Details for [GestureScaleStartCallback]. |
| class ScaleStartDetails { |
| /// Creates details for [GestureScaleStartCallback]. |
| /// |
| /// The [focalPoint] argument must not be null. |
| ScaleStartDetails({ this.focalPoint: Offset.zero }) |
| : assert(focalPoint != null); |
| |
| /// The initial focal point of the pointers in contact with the screen. |
| /// Reported in global coordinates. |
| final Offset focalPoint; |
| |
| @override |
| String toString() => 'ScaleStartDetails(focalPoint: $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: Offset.zero, |
| 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 Offset focalPoint; |
| |
| /// The scale implied by the pointers in contact with the screen. A value |
| /// greater than or equal to zero. |
| final double scale; |
| |
| @override |
| String toString() => 'ScaleUpdateDetails(focalPoint: $focalPoint, scale: $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; |
| |
| @override |
| String toString() => 'ScaleEndDetails(velocity: $velocity)'; |
| } |
| |
| /// Signature for when the pointers in contact with the screen have established |
| /// a focal point and initial scale of 1.0. |
| typedef void GestureScaleStartCallback(ScaleStartDetails details); |
| |
| /// Signature for when the pointers in contact with the screen have indicated a |
| /// new focal point and/or scale. |
| typedef void GestureScaleUpdateCallback(ScaleUpdateDetails details); |
| |
| /// Signature for when the pointers are no longer in contact with the screen. |
| typedef void GestureScaleEndCallback(ScaleEndDetails details); |
| |
| bool _isFlingGesture(Velocity velocity) { |
| assert(velocity != null); |
| final double speedSquared = velocity.pixelsPerSecond.distanceSquared; |
| return speedSquared > kMinFlingVelocity * kMinFlingVelocity; |
| } |
| |
| /// Recognizes a scale gesture. |
| /// |
| /// [ScaleGestureRecognizer] tracks the pointers in contact with the screen and |
| /// calculates their focal point and indicated 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]. |
| class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { |
| /// Create a gesture recognizer for interactions intended for scaling content. |
| ScaleGestureRecognizer({ Object debugOwner }) : super(debugOwner: debugOwner); |
| |
| /// The pointers in contact with the screen have established a focal point and |
| /// initial scale of 1.0. |
| GestureScaleStartCallback onStart; |
| |
| /// The pointers in contact with the screen have indicated a new focal point |
| /// and/or scale. |
| GestureScaleUpdateCallback onUpdate; |
| |
| /// The pointers are no longer in contact with the screen. |
| GestureScaleEndCallback onEnd; |
| |
| _ScaleState _state = _ScaleState.ready; |
| |
| Offset _initialFocalPoint; |
| Offset _currentFocalPoint; |
| double _initialSpan; |
| double _currentSpan; |
| Map<int, Offset> _pointerLocations; |
| final Map<int, VelocityTracker> _velocityTrackers = <int, VelocityTracker>{}; |
| |
| double get _scaleFactor => _initialSpan > 0.0 ? _currentSpan / _initialSpan : 1.0; |
| |
| @override |
| void addPointer(PointerEvent event) { |
| startTrackingPointer(event.pointer); |
| _velocityTrackers[event.pointer] = new VelocityTracker(); |
| if (_state == _ScaleState.ready) { |
| _state = _ScaleState.possible; |
| _initialSpan = 0.0; |
| _currentSpan = 0.0; |
| _pointerLocations = <int, Offset>{}; |
| } |
| } |
| |
| @override |
| void handleEvent(PointerEvent event) { |
| assert(_state != _ScaleState.ready); |
| bool didChangeConfiguration = false; |
| bool shouldStartIfAccepted = false; |
| if (event is PointerMoveEvent) { |
| final VelocityTracker tracker = _velocityTrackers[event.pointer]; |
| assert(tracker != null); |
| if (!event.synthesized) |
| tracker.addPosition(event.timeStamp, event.position); |
| _pointerLocations[event.pointer] = event.position; |
| shouldStartIfAccepted = true; |
| } else if (event is PointerDownEvent) { |
| _pointerLocations[event.pointer] = event.position; |
| didChangeConfiguration = true; |
| shouldStartIfAccepted = true; |
| } else if (event is PointerUpEvent || event is PointerCancelEvent) { |
| _pointerLocations.remove(event.pointer); |
| didChangeConfiguration = true; |
| } |
| |
| _update(); |
| if (!didChangeConfiguration || _reconfigure(event.pointer)) |
| _advanceStateMachine(shouldStartIfAccepted); |
| stopTrackingIfPointerNoLongerDown(event); |
| } |
| |
| void _update() { |
| final int count = _pointerLocations.keys.length; |
| |
| // Compute the focal point |
| Offset focalPoint = Offset.zero; |
| for (int pointer in _pointerLocations.keys) |
| focalPoint += _pointerLocations[pointer]; |
| _currentFocalPoint = count > 0 ? focalPoint / count.toDouble() : Offset.zero; |
| |
| // Span is the average deviation from focal point |
| double totalDeviation = 0.0; |
| for (int pointer in _pointerLocations.keys) |
| totalDeviation += (_currentFocalPoint - _pointerLocations[pointer]).distance; |
| _currentSpan = count > 0 ? totalDeviation / count : 0.0; |
| } |
| |
| 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 (_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 |
| } |
| } |
| _state = _ScaleState.accepted; |
| return false; |
| } |
| return true; |
| } |
| |
| void _advanceStateMachine(bool shouldStartIfAccepted) { |
| if (_state == _ScaleState.ready) |
| _state = _ScaleState.possible; |
| |
| 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) { |
| resolve(GestureDisposition.accepted); |
| } |
| |
| if (_state == _ScaleState.accepted && shouldStartIfAccepted) { |
| _state = _ScaleState.started; |
| _dispatchOnStartCallbackIfNeeded(); |
| } |
| |
| 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 |
| } |
| |
| @override |
| void acceptGesture(int pointer) { |
| if (_state == _ScaleState.possible) { |
| _state = _ScaleState.started; |
| _dispatchOnStartCallbackIfNeeded(); |
| } |
| } |
| |
| @override |
| void rejectGesture(int pointer) { |
| stopTrackingPointer(pointer); |
| } |
| |
| @override |
| void didStopTrackingLastPointer(int pointer) { |
| switch (_state) { |
| case _ScaleState.possible: |
| resolve(GestureDisposition.rejected); |
| break; |
| case _ScaleState.ready: |
| assert(false); // We should have not seen a pointer yet |
| break; |
| case _ScaleState.accepted: |
| break; |
| case _ScaleState.started: |
| assert(false); // We should be in the accepted state when user is done |
| break; |
| } |
| _state = _ScaleState.ready; |
| } |
| |
| @override |
| void dispose() { |
| _velocityTrackers.clear(); |
| super.dispose(); |
| } |
| |
| @override |
| String get debugDescription => 'scale'; |
| } |