Commit 11d1d54c authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Changes to the drag gesture detectors and the velocity tracker (#7363)

- more dartdocs for the drag typedefs

- more toStrings to aid debugging

- require the position for DragUpdateDetails since we were omitting it
  in some places

- add the primaryVelocity to DragEndDetails so that consumers don't
  have to themselves track the axis in question

- fix the velocity tracker so that it doesn't walk the null data.
  Previously, near time t=0 (which pretty much only matters in tests,
  but it does matter there) we would walk the velocity data and then
  also walk missing data, treating it as Point.zero with t=0.

- simplify some of the velocity tracker; e.g. instead of trying (and
  failing?) to clear the velocity tracker when the pointer stalls,
  just drop the data before a stall during the velocity estimation
  (where we redundantly had another bigger horizon anyway).
parent 629255eb
......@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'arena.dart';
import 'recognizer.dart';
import 'constants.dart';
......@@ -14,7 +16,14 @@ enum _DragState {
accepted,
}
/// Details for [GestureDragDownCallback].
/// Details object for callbacks that use [GestureDragDownCallback].
///
/// See also:
///
/// * [DragGestureRecognizer.onDown], which uses [GestureDragDownCallback].
/// * [DragStartDetails], the details for [GestureDragStartCallback].
/// * [DragUpdateDetails], the details for [GestureDragUpdateCallback].
/// * [DragEndDetails], the details for [GestureDragEndCallback].
class DragDownDetails {
/// Creates details for a [GestureDragDownCallback].
///
......@@ -24,15 +33,30 @@ class DragDownDetails {
}
/// The global position at which the pointer contacted the screen.
///
/// Defaults to the origin if not specified in the constructor.
final Point globalPosition;
@override
String toString() => '$runtimeType($globalPosition)';
}
/// Signature for when a pointer has contacted the screen and might begin to move.
/// Signature for when a pointer has contacted the screen and might begin to
/// move.
///
/// The `details` object provides the position of the touch.
///
/// See [DragGestureRecognizer.onDown].
typedef void GestureDragDownCallback(DragDownDetails details);
/// Details for [GestureDragStartCallback].
/// Details object for callbacks that use [GestureDragStartCallback].
///
/// See also:
///
/// * [DragGestureRecognizer.onStart], which uses [GestureDragStartCallback].
/// * [DragDownDetails], the details for [GestureDragDownCallback].
/// * [DragUpdateDetails], the details for [GestureDragUpdateCallback].
/// * [DragEndDetails], the details for [GestureDragEndCallback].
class DragStartDetails {
/// Creates details for a [GestureDragStartCallback].
///
......@@ -42,15 +66,30 @@ class DragStartDetails {
}
/// The global position at which the pointer contacted the screen.
///
/// Defaults to the origin if not specified in the constructor.
final Point globalPosition;
@override
String toString() => '$runtimeType($globalPosition)';
}
/// Signature for when a pointer has contacted the screen and has begun to move.
///
/// The `details` object provides the position of the touch when it first
/// touched the surface.
///
/// See [DragGestureRecognizer.onStart].
typedef void GestureDragStartCallback(DragStartDetails details);
/// Details for [GestureDragUpdateCallback].
/// Details object for callbacks that use [GestureDragUpdateCallback].
///
/// See also:
///
/// * [DragGestureRecognizer.onUpdate], which uses [GestureDragUpdateCallback].
/// * [DragDownDetails], the details for [GestureDragDownCallback].
/// * [DragStartDetails], the details for [GestureDragStartCallback].
/// * [DragEndDetails], the details for [GestureDragEndCallback].
class DragUpdateDetails {
/// Creates details for a [DragUpdateDetails].
///
......@@ -58,10 +97,12 @@ class DragUpdateDetails {
///
/// If [primaryDelta] is non-null, then its value must match one of the
/// coordinates of [delta] and the other coordinate must be zero.
///
/// The [globalPosition] argument must be provided and must not be null.
DragUpdateDetails({
this.delta: Offset.zero,
this.primaryDelta,
this.globalPosition
@required this.globalPosition
}) {
assert(primaryDelta == null
|| (primaryDelta == delta.dx && delta.dy == 0.0)
......@@ -73,6 +114,8 @@ class DragUpdateDetails {
/// If the [GestureDragUpdateCallback] is for a one-dimensional drag (e.g.,
/// a horizontal or vertical drag), then this offset contains only the delta
/// in that direction (i.e., the coordinate in the other direction is zero).
///
/// Defaults to zero if not specified in the constructor.
final Offset delta;
/// The amount the pointer has moved along the primary axis since the previous
......@@ -83,29 +126,67 @@ class DragUpdateDetails {
/// [delta] along the primary axis (e.g., horizontal or vertical,
/// respectively). Otherwise, if the [GestureDragUpdateCallback] is for a
/// two-dimensional drag (e.g., a pan), then this value is null.
///
/// Defaults to null if not specified in the constructor.
final double primaryDelta;
/// The pointer's global position.
/// The pointer's global position when it triggered this update.
final Point globalPosition;
@override
String toString() => '$runtimeType($delta)';
}
/// Signature for when a pointer that is in contact with the screen and moving
/// has moved again.
///
/// The `details` object provides the position of the touch and the distance it
/// has travelled since the last update.
///
/// See [DragGestureRecognizer.onUpdate].
typedef void GestureDragUpdateCallback(DragUpdateDetails details);
/// Details for [GestureDragEndCallback].
/// Details object for callbacks that use [GestureDragEndCallback].
///
/// See also:
///
/// * [DragGestureRecognizer.onEnd], which uses [GestureDragEndCallback].
/// * [DragDownDetails], the details for [GestureDragDownCallback].
/// * [DragStartDetails], the details for [GestureDragStartCallback].
/// * [DragUpdateDetails], the details for [GestureDragUpdateCallback].
class DragEndDetails {
/// Creates details for a [GestureDragEndCallback].
///
/// The [velocity] argument must not be null.
DragEndDetails({ this.velocity: Velocity.zero }) {
DragEndDetails({
this.velocity: Velocity.zero,
this.primaryVelocity,
}) {
assert(velocity != null);
assert(primaryVelocity == null
|| primaryVelocity == velocity.pixelsPerSecond.dx
|| primaryVelocity == velocity.pixelsPerSecond.dy);
}
/// The velocity the pointer was moving when it stopped contacting the screen.
///
/// Defaults to zero if not specified in the constructor.
final Velocity velocity;
/// The velocity the pointer was moving along the primary axis when it stopped
/// contacting the screen, in logical pixels per second.
///
/// If the [GestureDragEndCallback] is for a one-dimensional drag (e.g., a
/// horizontal or vertical drag), then this value contains the component of
/// [velocity] along the primary axis (e.g., horizontal or vertical,
/// respectively). Otherwise, if the [GestureDragEndCallback] is for a
/// two-dimensional drag (e.g., a pan), then this value is null.
///
/// Defaults to null if not specified in the constructor.
final double primaryVelocity;
@override
String toString() => '$runtimeType($velocity)';
}
/// Signature for when a pointer that was previously in contact with the screen
......@@ -147,17 +228,29 @@ bool _isFlingGesture(Velocity velocity) {
/// * [PanGestureRecognizer]
abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
/// A pointer has contacted the screen and might begin to move.
///
/// The position of the pointer is provided in the callback's `details`
/// argument, which is a [DragDownDetails] object.
GestureDragDownCallback onDown;
/// A pointer has contacted the screen and has begun to move.
///
/// The position of the pointer is provided in the callback's `details`
/// argument, which is a [DragStartDetails] object.
GestureDragStartCallback onStart;
/// A pointer that is in contact with the screen and moving has moved again.
///
/// The distance travelled by the pointer since the last update is provided in
/// the callback's `details` argument, which is a [DragUpdateDetails] object.
GestureDragUpdateCallback onUpdate;
/// A pointer that was previously in contact with the screen and moving is no
/// longer in contact with the screen and was moving at a specific velocity
/// when it stopped contacting the screen.
///
/// The velocity is provided in the callback's `details` argument, which is a
/// [DragEndDetails] object.
GestureDragEndCallback onEnd;
/// The pointer that previously triggered [onDown] did not complete.
......@@ -168,7 +261,7 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
Offset _pendingDragOffset;
Offset _getDeltaForDetails(Offset delta);
double _getPrimaryDeltaForDetails(Offset delta);
double _getPrimaryValueFromOffset(Offset value);
bool get _hasSufficientPendingDragDeltaToAccept;
Map<int, VelocityTracker> _velocityTrackers = new Map<int, VelocityTracker>();
......@@ -198,8 +291,8 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
if (onUpdate != null) {
invokeCallback/*<Null>*/('onUpdate', () => onUpdate(new DragUpdateDetails( // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
delta: _getDeltaForDetails(delta),
primaryDelta: _getPrimaryDeltaForDetails(delta),
globalPosition: event.position
primaryDelta: _getPrimaryValueFromOffset(delta),
globalPosition: event.position,
)));
}
} else {
......@@ -217,12 +310,16 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
_state = _DragState.accepted;
Offset delta = _pendingDragOffset;
_pendingDragOffset = Offset.zero;
if (onStart != null)
invokeCallback/*<Null>*/('onStart', () => onStart(new DragStartDetails(globalPosition: _initialPosition))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
if (onStart != null) {
invokeCallback/*<Null>*/('onStart', () => onStart(new DragStartDetails( // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
globalPosition: _initialPosition,
)));
}
if (delta != Offset.zero && onUpdate != null) {
invokeCallback/*<Null>*/('onUpdate', () => onUpdate(new DragUpdateDetails( // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
delta: _getDeltaForDetails(delta),
primaryDelta: _getPrimaryDeltaForDetails(delta)
primaryDelta: _getPrimaryValueFromOffset(delta),
globalPosition: _initialPosition,
)));
}
}
......@@ -253,9 +350,15 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer {
final Offset pixelsPerSecond = velocity.pixelsPerSecond;
if (pixelsPerSecond.distanceSquared > kMaxFlingVelocity * kMaxFlingVelocity)
velocity = new Velocity(pixelsPerSecond: (pixelsPerSecond / pixelsPerSecond.distance) * kMaxFlingVelocity);
invokeCallback/*<Null>*/('onEnd', () => onEnd(new DragEndDetails(velocity: velocity))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
invokeCallback/*<Null>*/('onEnd', () => onEnd(new DragEndDetails( // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
velocity: velocity,
primaryVelocity: _getPrimaryValueFromOffset(velocity.pixelsPerSecond),
)));
} else {
invokeCallback/*<Null>*/('onEnd', () => onEnd(new DragEndDetails(velocity: Velocity.zero))); // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
invokeCallback/*<Null>*/('onEnd', () => onEnd(new DragEndDetails( // ignore: STRONG_MODE_INVALID_CAST_FUNCTION_EXPR, https://github.com/dart-lang/sdk/issues/27504
velocity: Velocity.zero,
primaryVelocity: 0.0,
)));
}
}
_velocityTrackers.clear();
......@@ -283,7 +386,7 @@ class VerticalDragGestureRecognizer extends DragGestureRecognizer {
Offset _getDeltaForDetails(Offset delta) => new Offset(0.0, delta.dy);
@override
double _getPrimaryDeltaForDetails(Offset delta) => delta.dy;
double _getPrimaryValueFromOffset(Offset value) => value.dy;
@override
String toStringShort() => 'vertical drag';
......@@ -304,7 +407,7 @@ class HorizontalDragGestureRecognizer extends DragGestureRecognizer {
Offset _getDeltaForDetails(Offset delta) => new Offset(delta.dx, 0.0);
@override
double _getPrimaryDeltaForDetails(Offset delta) => delta.dx;
double _getPrimaryValueFromOffset(Offset value) => value.dx;
@override
String toStringShort() => 'horizontal drag';
......@@ -326,7 +429,7 @@ class PanGestureRecognizer extends DragGestureRecognizer {
Offset _getDeltaForDetails(Offset delta) => delta;
@override
double _getPrimaryDeltaForDetails(Offset delta) => null;
double _getPrimaryValueFromOffset(Offset value) => null;
@override
String toStringShort() => 'pan';
......
......@@ -84,7 +84,10 @@ abstract class MultiDragPointerState {
if (_client != null) {
assert(pendingDelta == null);
// Call client last to avoid reentrancy.
_client.update(new DragUpdateDetails(delta: event.delta));
_client.update(new DragUpdateDetails(
delta: event.delta,
globalPosition: event.position,
));
} else {
assert(pendingDelta != null);
_pendingDelta += event.delta;
......@@ -124,7 +127,10 @@ abstract class MultiDragPointerState {
assert(client != null);
assert(pendingDelta != null);
_client = client;
final DragUpdateDetails details = new DragUpdateDetails(delta: pendingDelta);
final DragUpdateDetails details = new DragUpdateDetails(
delta: pendingDelta,
globalPosition: initialPosition,
);
_pendingDelta = null;
// Call client last to avoid reentrancy.
_client.update(details);
......
......@@ -16,6 +16,10 @@ import 'pointer_router.dart';
export 'pointer_router.dart' show PointerRouter;
/// Generic signature for callbacks passed to
/// [GestureRecognizer.invokeCallback]. This allows the
/// [GestureRecognizer.invokeCallback] mechanism to be generically used with
/// anonymous functions that return objects of particular types.
typedef T RecognizerCallback<T>();
/// The base class that all GestureRecognizers should inherit from.
......@@ -51,7 +55,8 @@ abstract class GestureRecognizer extends GestureArenaMember {
/// recognizer looks for, like 'tap' or 'horizontal drag'.
String toStringShort() => toString();
/// Invoke a callback provided by the application and log any exceptions.
/// Invoke a callback provided by the application, catching and logging any
/// exceptions.
@protected
dynamic/*=T*/ invokeCallback/*<T>*/(String name, RecognizerCallback<dynamic/*=T*/> callback) {
dynamic/*=T*/ result;
......
......@@ -25,28 +25,32 @@ abstract class _VelocityTrackerStrategy {
}
class _Movement {
Duration eventTime = Duration.ZERO;
Point position = Point.origin;
const _Movement(this.eventTime, this.position);
final Duration eventTime;
final Point position;
@override
String toString() => 'Movement($position at $eventTime)';
}
class _LeastSquaresVelocityTrackerStrategy extends _VelocityTrackerStrategy {
static const int kHistorySize = 20;
static const int kHorizonMilliseconds = 100;
_LeastSquaresVelocityTrackerStrategy(this.degree);
final int degree;
final List<_Movement> _movements = new List<_Movement>(kHistorySize);
int _index = 0;
static const int kHistorySize = 20;
static const int kHorizonMilliseconds = 40;
@override
void addMovement(Duration timeStamp, Point position) {
_index += 1;
if (_index == kHistorySize)
_index = 0;
_Movement movement = _getMovement(_index);
movement.eventTime = timeStamp;
movement.position = position;
_movements[_index] = new _Movement(timeStamp, position);
}
@override
......@@ -58,9 +62,15 @@ class _LeastSquaresVelocityTrackerStrategy extends _VelocityTrackerStrategy {
List<double> time = new List<double>();
int m = 0;
int index = _index;
_Movement newestMovement = _getMovement(index);
_Movement newestMovement = _movements[index];
if (newestMovement == null)
return null;
do {
_Movement movement = _getMovement(index);
_Movement movement = _movements[index];
if (movement == null)
break;
double age = (newestMovement.eventTime - movement.eventTime).inMilliseconds.toDouble();
if (age > kHorizonMilliseconds)
......@@ -76,9 +86,6 @@ class _LeastSquaresVelocityTrackerStrategy extends _VelocityTrackerStrategy {
m += 1;
} while (m < kHistorySize);
if (m == 0) // because we broke out of the loop above after age > kHorizonMilliseconds
return null; // no data
// Calculate a least squares polynomial fit.
int n = degree;
if (n > m - 1)
......@@ -118,15 +125,6 @@ class _LeastSquaresVelocityTrackerStrategy extends _VelocityTrackerStrategy {
_index = -1;
}
_Movement _getMovement(int i) {
_Movement result = _movements[i];
if (result == null) {
result = new _Movement();
_movements[i] = result;
}
return result;
}
}
/// A velocity in two dimensions.
......@@ -184,25 +182,26 @@ class Velocity {
/// The quality of the velocity estimation will be better if more data
/// points have been received.
class VelocityTracker {
/// The maximum length of time between two move events to allow
/// before assuming the pointer stopped.
static const Duration kAssumePointerMoveStoppedTime = const Duration(milliseconds: 40);
/// Creates a velocity tracker.
VelocityTracker() : _strategy = _createStrategy();
Duration _lastTimeStamp = const Duration();
// VelocityTracker is designed to easily be adapted to using different
// algorithms in the future, potentially picking algorithms on the fly based
// on hardware or other environment factors.
//
// For now, though, we just use the _LeastSquaresVelocityTrackerStrategy
// defined above.
// TODO(ianh): Simplify this. We don't see to need multiple stategies.
static _VelocityTrackerStrategy _createStrategy() {
return new _LeastSquaresVelocityTrackerStrategy(2);
}
_VelocityTrackerStrategy _strategy;
/// Add a given position corresponding to a specific time.
///
/// If [kAssumePointerMoveStoppedTime] has elapsed since the last
/// call, then earlier data will be discarded.
void addPosition(Duration timeStamp, Point position) {
if (timeStamp - _lastTimeStamp >= kAssumePointerMoveStoppedTime)
_strategy.clear();
_lastTimeStamp = timeStamp;
_strategy.addMovement(timeStamp, position);
}
......@@ -225,8 +224,4 @@ class VelocityTracker {
}
return null;
}
static _VelocityTrackerStrategy _createStrategy() {
return new _LeastSquaresVelocityTrackerStrategy(2);
}
}
......@@ -2552,7 +2552,8 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
if (onHorizontalDragUpdate != null) {
final double primaryDelta = size.width * -scrollFactor;
onHorizontalDragUpdate(new DragUpdateDetails(
delta: new Offset(primaryDelta, 0.0), primaryDelta: primaryDelta
delta: new Offset(primaryDelta, 0.0), primaryDelta: primaryDelta,
globalPosition: localToGlobal(size.center(Point.origin)),
));
}
break;
......@@ -2560,7 +2561,8 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
if (onHorizontalDragUpdate != null) {
final double primaryDelta = size.width * scrollFactor;
onHorizontalDragUpdate(new DragUpdateDetails(
delta: new Offset(primaryDelta, 0.0), primaryDelta: primaryDelta
delta: new Offset(primaryDelta, 0.0), primaryDelta: primaryDelta,
globalPosition: localToGlobal(size.center(Point.origin)),
));
}
break;
......@@ -2568,7 +2570,8 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
if (onVerticalDragUpdate != null) {
final double primaryDelta = size.height * -scrollFactor;
onVerticalDragUpdate(new DragUpdateDetails(
delta: new Offset(0.0, primaryDelta), primaryDelta: primaryDelta
delta: new Offset(0.0, primaryDelta), primaryDelta: primaryDelta,
globalPosition: localToGlobal(size.center(Point.origin)),
));
}
break;
......@@ -2576,7 +2579,8 @@ class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticsA
if (onVerticalDragUpdate != null) {
final double primaryDelta = size.height * scrollFactor;
onVerticalDragUpdate(new DragUpdateDetails(
delta: new Offset(0.0, primaryDelta), primaryDelta: primaryDelta
delta: new Offset(0.0, primaryDelta), primaryDelta: primaryDelta,
globalPosition: localToGlobal(size.center(Point.origin)),
));
}
break;
......
......@@ -133,8 +133,10 @@ void main() {
HorizontalDragGestureRecognizer drag = new HorizontalDragGestureRecognizer();
Velocity velocity;
double primaryVelocity;
drag.onEnd = (DragEndDetails details) {
velocity = details.velocity;
primaryVelocity = details.primaryVelocity;
};
TestPointer pointer = new TestPointer(5);
......@@ -155,7 +157,16 @@ void main() {
tester.route(pointer.move(const Point(120.0, 25.0), timeStamp: const Duration(milliseconds: 20)));
tester.route(pointer.up(timeStamp: const Duration(milliseconds: 20)));
expect(velocity.pixelsPerSecond.dx, inInclusiveRange(0.99 * kMaxFlingVelocity, kMaxFlingVelocity));
expect(velocity.pixelsPerSecond.dy, moreOrLessEquals(0.0));
expect(primaryVelocity, velocity.pixelsPerSecond.dx);
drag.dispose();
});
testGesture('Drag details', (GestureTester tester) {
expect(new DragDownDetails(), hasOneLineDescription);
expect(new DragStartDetails(), hasOneLineDescription);
expect(new DragUpdateDetails(globalPosition: Point.origin), hasOneLineDescription);
expect(new DragEndDetails(), hasOneLineDescription);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment