mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
282 lines
9.5 KiB
Dart
282 lines
9.5 KiB
Dart
// 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 GestureScaleStartCallback = void Function(ScaleStartDetails details);
|
|
|
|
/// Signature for when the pointers in contact with the screen have indicated a
|
|
/// new focal point and/or scale.
|
|
typedef GestureScaleUpdateCallback = void Function(ScaleUpdateDetails details);
|
|
|
|
/// Signature for when the pointers are no longer in contact with the screen.
|
|
typedef GestureScaleEndCallback = void Function(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] = 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 = Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
|
|
invokeCallback<void>('onEnd', () => onEnd(ScaleEndDetails(velocity: velocity)));
|
|
} else {
|
|
invokeCallback<void>('onEnd', () => onEnd(ScaleEndDetails(velocity: Velocity.zero)));
|
|
}
|
|
}
|
|
_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<void>('onUpdate', () => onUpdate(ScaleUpdateDetails(scale: _scaleFactor, focalPoint: _currentFocalPoint)));
|
|
}
|
|
|
|
void _dispatchOnStartCallbackIfNeeded() {
|
|
assert(_state == _ScaleState.started);
|
|
if (onStart != null)
|
|
invokeCallback<void>('onStart', () => onStart(ScaleStartDetails(focalPoint: _currentFocalPoint)));
|
|
}
|
|
|
|
@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';
|
|
}
|