Commit 4fb47600 authored by Adam Barth's avatar Adam Barth

Add a Velocity class to be explicit about units

We were using an Offset, which represented pixels/second, but it wasn't
clear to clients whether that was pixels/ms. Now we use a Velocity class
that is explict about the units.

Fixes #1510
Fixes #785
parent a724d6cc
...@@ -16,19 +16,19 @@ enum DragState { ...@@ -16,19 +16,19 @@ enum DragState {
typedef void GestureDragStartCallback(Point globalPosition); typedef void GestureDragStartCallback(Point globalPosition);
typedef void GestureDragUpdateCallback(double delta); typedef void GestureDragUpdateCallback(double delta);
typedef void GestureDragEndCallback(Offset velocity); typedef void GestureDragEndCallback(Velocity velocity);
typedef void GesturePanStartCallback(Point globalPosition); typedef void GesturePanStartCallback(Point globalPosition);
typedef void GesturePanUpdateCallback(Offset delta); typedef void GesturePanUpdateCallback(Offset delta);
typedef void GesturePanEndCallback(Offset velocity); typedef void GesturePanEndCallback(Velocity velocity);
typedef void _GesturePolymorphicUpdateCallback<T>(T delta); typedef void _GesturePolymorphicUpdateCallback<T>(T delta);
bool _isFlingGesture(Offset velocity) { bool _isFlingGesture(Velocity velocity) {
assert(velocity != null); assert(velocity != null);
double velocitySquared = velocity.dx * velocity.dx + velocity.dy * velocity.dy; final double speedSquared = velocity.pixelsPerSecond.distanceSquared;
return velocitySquared > kMinFlingVelocity * kMinFlingVelocity return speedSquared > kMinFlingVelocity * kMinFlingVelocity
&& velocitySquared < kMaxFlingVelocity * kMaxFlingVelocity; && speedSquared < kMaxFlingVelocity * kMaxFlingVelocity;
} }
abstract class _DragGestureRecognizer<T extends dynamic> extends OneSequenceGestureRecognizer { abstract class _DragGestureRecognizer<T extends dynamic> extends OneSequenceGestureRecognizer {
...@@ -110,11 +110,11 @@ abstract class _DragGestureRecognizer<T extends dynamic> extends OneSequenceGest ...@@ -110,11 +110,11 @@ abstract class _DragGestureRecognizer<T extends dynamic> extends OneSequenceGest
VelocityTracker tracker = _velocityTrackers[pointer]; VelocityTracker tracker = _velocityTrackers[pointer];
assert(tracker != null); assert(tracker != null);
Offset velocity = tracker.getVelocity(); Velocity velocity = tracker.getVelocity();
if (velocity != null && _isFlingGesture(velocity)) if (velocity != null && _isFlingGesture(velocity))
onEnd(velocity); onEnd(velocity);
else else
onEnd(Offset.zero); onEnd(Velocity.zero);
} }
_velocityTrackers.clear(); _velocityTrackers.clear();
} }
......
...@@ -16,7 +16,7 @@ typedef Drag GestureMultiDragStartCallback(Point position); ...@@ -16,7 +16,7 @@ typedef Drag GestureMultiDragStartCallback(Point position);
class Drag { class Drag {
void move(Offset offset) { } void move(Offset offset) { }
void end(Offset velocity) { } void end(Velocity velocity) { }
void cancel() { } void cancel() { }
} }
......
...@@ -208,6 +208,28 @@ class _LeastSquaresVelocityTrackerStrategy extends _VelocityTrackerStrategy { ...@@ -208,6 +208,28 @@ class _LeastSquaresVelocityTrackerStrategy extends _VelocityTrackerStrategy {
} }
/// A velocity in two dimensions.
class Velocity {
const Velocity({ this.pixelsPerSecond });
/// A velocity that isn't moving at all.
static const Velocity zero = const Velocity(pixelsPerSecond: Offset.zero);
/// The number of pixels per second of velocity in the x and y directions.
final Offset pixelsPerSecond;
bool operator ==(dynamic other) {
if (other is! Velocity)
return false;
final Velocity typedOther = other;
return pixelsPerSecond == typedOther.pixelsPerSecond;
}
int get hashCode => pixelsPerSecond.hashCode;
String toString() => 'Velocity(${pixelsPerSecond.dx.toStringAsFixed(1)}, ${pixelsPerSecond.dy.toStringAsFixed(1)})';
}
/// Computes a pointer velocity based on data from PointerMove events. /// Computes a pointer velocity based on data from PointerMove events.
/// ///
/// The input data is provided by calling addPosition(). Adding data /// The input data is provided by calling addPosition(). Adding data
...@@ -248,12 +270,14 @@ class VelocityTracker { ...@@ -248,12 +270,14 @@ class VelocityTracker {
/// ///
/// getVelocity() will return null if no estimate is available or if /// getVelocity() will return null if no estimate is available or if
/// the velocity is zero. /// the velocity is zero.
Offset getVelocity() { Velocity getVelocity() {
_Estimate estimate = _strategy.getEstimate(); _Estimate estimate = _strategy.getEstimate();
if (estimate != null && estimate.degree >= 1) { if (estimate != null && estimate.degree >= 1) {
return new Offset( // convert from pixels/ms to pixels/s return new Velocity(
pixelsPerSecond: new Offset( // convert from pixels/ms to pixels/s
estimate.xCoefficients[1] * 1000, estimate.xCoefficients[1] * 1000,
estimate.yCoefficients[1] * 1000 estimate.yCoefficients[1] * 1000
)
); );
} }
return null; return null;
......
...@@ -59,11 +59,11 @@ class _BottomSheetState extends State<BottomSheet> { ...@@ -59,11 +59,11 @@ class _BottomSheetState extends State<BottomSheet> {
config.animationController.value -= delta / (_childHeight ?? delta); config.animationController.value -= delta / (_childHeight ?? delta);
} }
void _handleDragEnd(Offset velocity) { void _handleDragEnd(Velocity velocity) {
if (_dismissUnderway) if (_dismissUnderway)
return; return;
if (velocity.dy > _kMinFlingVelocity) { if (velocity.pixelsPerSecond.dy > _kMinFlingVelocity) {
double flingVelocity = -velocity.dy / _childHeight; double flingVelocity = -velocity.pixelsPerSecond.dy / _childHeight;
config.animationController.fling(velocity: flingVelocity); config.animationController.fling(velocity: flingVelocity);
if (flingVelocity < 0.0) if (flingVelocity < 0.0)
config.onClosing(); config.onClosing();
......
...@@ -135,11 +135,11 @@ class DrawerControllerState extends State<DrawerController> { ...@@ -135,11 +135,11 @@ class DrawerControllerState extends State<DrawerController> {
_controller.value += delta / _width; _controller.value += delta / _width;
} }
void _settle(Offset velocity) { void _settle(Velocity velocity) {
if (_controller.isDismissed) if (_controller.isDismissed)
return; return;
if (velocity.dx.abs() >= _kMinFlingVelocity) { if (velocity.pixelsPerSecond.dx.abs() >= _kMinFlingVelocity) {
_controller.fling(velocity: velocity.dx / _width); _controller.fling(velocity: velocity.pixelsPerSecond.dx / _width);
} else if (_controller.value < 0.5) { } else if (_controller.value < 0.5) {
close(); close();
} else { } else {
......
...@@ -145,7 +145,7 @@ class _RenderSlider extends RenderConstrainedBox { ...@@ -145,7 +145,7 @@ class _RenderSlider extends RenderConstrainedBox {
} }
} }
void _handleDragEnd(Offset velocity) { void _handleDragEnd(Velocity velocity) {
if (_active) { if (_active) {
_active = false; _active = false;
_currentDragValue = 0.0; _currentDragValue = 0.0;
......
...@@ -156,7 +156,7 @@ class _RenderSwitch extends RenderToggleable { ...@@ -156,7 +156,7 @@ class _RenderSwitch extends RenderToggleable {
} }
} }
void _handleDragEnd(Offset velocity) { void _handleDragEnd(Velocity velocity) {
if (position.value >= 0.5) if (position.value >= 0.5)
positionController.forward(); positionController.forward();
else else
......
...@@ -425,7 +425,7 @@ class _DialState extends State<_Dial> { ...@@ -425,7 +425,7 @@ class _DialState extends State<_Dial> {
_notifyOnChangedIfNeeded(); _notifyOnChangedIfNeeded();
} }
void _handlePanEnd(Offset velocity) { void _handlePanEnd(Velocity velocity) {
assert(_dragging); assert(_dragging);
_dragging = false; _dragging = false;
_position = null; _position = null;
......
...@@ -182,9 +182,9 @@ class _DismissableState extends State<Dismissable> { ...@@ -182,9 +182,9 @@ class _DismissableState extends State<Dismissable> {
_dismissController.value = _dragExtent.abs() / _size.width; _dismissController.value = _dragExtent.abs() / _size.width;
} }
bool _isFlingGesture(Offset velocity) { bool _isFlingGesture(Velocity velocity) {
double vx = velocity.dx; double vx = velocity.pixelsPerSecond.dx;
double vy = velocity.dy; double vy = velocity.pixelsPerSecond.dy;
if (_directionIsYAxis) { if (_directionIsYAxis) {
if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta) if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta)
return false; return false;
...@@ -211,7 +211,7 @@ class _DismissableState extends State<Dismissable> { ...@@ -211,7 +211,7 @@ class _DismissableState extends State<Dismissable> {
return false; return false;
} }
void _handleDragEnd(Offset velocity) { void _handleDragEnd(Velocity velocity) {
if (!_isActive || _dismissController.isAnimating) if (!_isActive || _dismissController.isAnimating)
return; return;
setState(() { setState(() {
...@@ -219,7 +219,7 @@ class _DismissableState extends State<Dismissable> { ...@@ -219,7 +219,7 @@ class _DismissableState extends State<Dismissable> {
if (_dismissController.isCompleted) { if (_dismissController.isCompleted) {
_startResizeAnimation(); _startResizeAnimation();
} else if (_isFlingGesture(velocity)) { } else if (_isFlingGesture(velocity)) {
double flingVelocity = _directionIsYAxis ? velocity.dy : velocity.dx; double flingVelocity = _directionIsYAxis ? velocity.pixelsPerSecond.dy : velocity.pixelsPerSecond.dx;
_dragExtent = flingVelocity.sign; _dragExtent = flingVelocity.sign;
_dismissController.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale); _dismissController.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale);
} else if (_dismissController.value > _kDismissCardThreshold) { } else if (_dismissController.value > _kDismissCardThreshold) {
......
...@@ -396,7 +396,7 @@ class _DragAvatar<T> extends Drag { ...@@ -396,7 +396,7 @@ class _DragAvatar<T> extends Drag {
_position += offset; _position += offset;
update(_position); update(_position);
} }
void end(Offset velocity) { void end(Velocity velocity) {
finish(_DragEndKind.dropped); finish(_DragEndKind.dropped);
} }
void cancel() { void cancel() {
......
...@@ -25,7 +25,8 @@ export 'package:flutter/gestures.dart' show ...@@ -25,7 +25,8 @@ export 'package:flutter/gestures.dart' show
GesturePanEndCallback, GesturePanEndCallback,
GestureScaleStartCallback, GestureScaleStartCallback,
GestureScaleUpdateCallback, GestureScaleUpdateCallback,
GestureScaleEndCallback; GestureScaleEndCallback,
Velocity;
/// A widget that detects gestures. /// A widget that detects gestures.
/// ///
...@@ -414,7 +415,7 @@ class _GestureSemantics extends OneChildRenderObjectWidget { ...@@ -414,7 +415,7 @@ class _GestureSemantics extends OneChildRenderObjectWidget {
if (onHorizontalDragUpdate != null) if (onHorizontalDragUpdate != null)
onHorizontalDragUpdate(delta); onHorizontalDragUpdate(delta);
if (onHorizontalDragEnd != null) if (onHorizontalDragEnd != null)
onHorizontalDragEnd(Offset.zero); onHorizontalDragEnd(Velocity.zero);
} else { } else {
assert(_watchingPans); assert(_watchingPans);
if (onPanStart != null) if (onPanStart != null)
...@@ -422,7 +423,7 @@ class _GestureSemantics extends OneChildRenderObjectWidget { ...@@ -422,7 +423,7 @@ class _GestureSemantics extends OneChildRenderObjectWidget {
if (onPanUpdate != null) if (onPanUpdate != null)
onPanUpdate(new Offset(delta, 0.0)); onPanUpdate(new Offset(delta, 0.0));
if (onPanEnd != null) if (onPanEnd != null)
onPanEnd(Offset.zero); onPanEnd(Velocity.zero);
} }
} }
...@@ -433,7 +434,7 @@ class _GestureSemantics extends OneChildRenderObjectWidget { ...@@ -433,7 +434,7 @@ class _GestureSemantics extends OneChildRenderObjectWidget {
if (onVerticalDragUpdate != null) if (onVerticalDragUpdate != null)
onVerticalDragUpdate(delta); onVerticalDragUpdate(delta);
if (onVerticalDragEnd != null) if (onVerticalDragEnd != null)
onVerticalDragEnd(Offset.zero); onVerticalDragEnd(Velocity.zero);
} else { } else {
assert(_watchingPans); assert(_watchingPans);
if (onPanStart != null) if (onPanStart != null)
...@@ -441,7 +442,7 @@ class _GestureSemantics extends OneChildRenderObjectWidget { ...@@ -441,7 +442,7 @@ class _GestureSemantics extends OneChildRenderObjectWidget {
if (onPanUpdate != null) if (onPanUpdate != null)
onPanUpdate(new Offset(0.0, delta)); onPanUpdate(new Offset(0.0, delta));
if (onPanEnd != null) if (onPanEnd != null)
onPanEnd(Offset.zero); onPanEnd(Velocity.zero);
} }
} }
......
...@@ -450,8 +450,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> { ...@@ -450,8 +450,8 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
scrollBy(pixelOffsetToScrollOffset(delta)); scrollBy(pixelOffsetToScrollOffset(delta));
} }
Future _handleDragEnd(Offset velocity) { Future _handleDragEnd(Velocity velocity) {
double scrollVelocity = pixelDeltaToScrollOffset(velocity) / Duration.MILLISECONDS_PER_SECOND; double scrollVelocity = pixelDeltaToScrollOffset(velocity.pixelsPerSecond) / Duration.MILLISECONDS_PER_SECOND;
// The gesture velocity properties are pixels/second, config min,max limits are pixels/ms // The gesture velocity properties are pixels/second, config min,max limits are pixels/ms
return fling(scrollVelocity.clamp(-kMaxFlingVelocity, kMaxFlingVelocity)).then((_) { return fling(scrollVelocity.clamp(-kMaxFlingVelocity, kMaxFlingVelocity)).then((_) {
dispatchOnScrollEnd(); dispatchOnScrollEnd();
......
...@@ -58,7 +58,7 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> { ...@@ -58,7 +58,7 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> {
_lastPointerDownLocation = null; _lastPointerDownLocation = null;
}); });
} }
void _handlePanEnd(Offset velocity) { void _handlePanEnd(Velocity velocity) {
assert(_lastPointerDownLocation != null); assert(_lastPointerDownLocation != null);
_SemanticsDebuggerListener.instance.handlePanEnd(_lastPointerDownLocation, velocity); _SemanticsDebuggerListener.instance.handlePanEnd(_lastPointerDownLocation, velocity);
setState(() { setState(() {
...@@ -329,16 +329,18 @@ class _SemanticsDebuggerListener implements mojom.SemanticsListener { ...@@ -329,16 +329,18 @@ class _SemanticsDebuggerListener implements mojom.SemanticsListener {
void handleLongPress(Point position) { void handleLongPress(Point position) {
_server.longPress(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeLongPressed)?.id ?? 0); _server.longPress(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeLongPressed)?.id ?? 0);
} }
void handlePanEnd(Point position, Offset velocity) { void handlePanEnd(Point position, Velocity velocity) {
if (velocity.dx.abs() == velocity.dy.abs()) double vx = velocity.pixelsPerSecond.dx;
double vy = velocity.pixelsPerSecond.dy;
if (vx.abs() == vy.abs())
return; return;
if (velocity.dx.abs() > velocity.dy.abs()) { if (vx.abs() > vy.abs()) {
if (velocity.dx.sign < 0) if (vx.sign < 0)
_server.scrollLeft(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledHorizontally)?.id ?? 0); _server.scrollLeft(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledHorizontally)?.id ?? 0);
else else
_server.scrollRight(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledHorizontally)?.id ?? 0); _server.scrollRight(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledHorizontally)?.id ?? 0);
} else { } else {
if (velocity.dy.sign < 0) if (vy.sign < 0)
_server.scrollUp(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledVertically)?.id ?? 0); _server.scrollUp(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledVertically)?.id ?? 0);
else else
_server.scrollDown(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledVertically)?.id ?? 0); _server.scrollDown(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledVertically)?.id ?? 0);
......
...@@ -30,7 +30,7 @@ void main() { ...@@ -30,7 +30,7 @@ void main() {
}; };
bool didEndPan = false; bool didEndPan = false;
pan.onEnd = (Offset velocity) { pan.onEnd = (Velocity velocity) {
didEndPan = true; didEndPan = true;
}; };
......
...@@ -12,10 +12,10 @@ bool _withinTolerance(double actual, double expected) { ...@@ -12,10 +12,10 @@ bool _withinTolerance(double actual, double expected) {
return diff.abs() < kTolerance; return diff.abs() < kTolerance;
} }
bool _checkVelocity(Offset actual, Offset expected) { bool _checkVelocity(Velocity actual, Offset expected) {
return (actual != null) return (actual != null)
&& _withinTolerance(actual.dx, expected.dx) && _withinTolerance(actual.pixelsPerSecond.dx, expected.dx)
&& _withinTolerance(actual.dy, expected.dy); && _withinTolerance(actual.pixelsPerSecond.dy, expected.dy);
} }
void main() { void main() {
......
...@@ -22,7 +22,7 @@ void main() { ...@@ -22,7 +22,7 @@ void main() {
onVerticalDragUpdate: (double scrollDelta) { onVerticalDragUpdate: (double scrollDelta) {
updatedDragDelta = scrollDelta; updatedDragDelta = scrollDelta;
}, },
onVerticalDragEnd: (Offset velocity) { onVerticalDragEnd: (Velocity velocity) {
didEndDrag = true; didEndDrag = true;
}, },
child: new Container( child: new Container(
...@@ -73,9 +73,9 @@ void main() { ...@@ -73,9 +73,9 @@ void main() {
Widget widget = new GestureDetector( Widget widget = new GestureDetector(
onVerticalDragUpdate: (double delta) { dragDistance += delta; }, onVerticalDragUpdate: (double delta) { dragDistance += delta; },
onVerticalDragEnd: (Offset velocity) { gestureCount += 1; }, onVerticalDragEnd: (Velocity velocity) { gestureCount += 1; },
onHorizontalDragUpdate: (_) { fail("gesture should not match"); }, onHorizontalDragUpdate: (_) { fail("gesture should not match"); },
onHorizontalDragEnd: (Offset velocity) { fail("gesture should not match"); }, onHorizontalDragEnd: (Velocity velocity) { fail("gesture should not match"); },
child: new Container( child: new Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
backgroundColor: const Color(0xFF00FF00) backgroundColor: const Color(0xFF00FF00)
......
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