diff --git a/packages/flutter/lib/src/gestures/converter.dart b/packages/flutter/lib/src/gestures/converter.dart index 2909f97a78..a5ea47d72d 100644 --- a/packages/flutter/lib/src/gestures/converter.dart +++ b/packages/flutter/lib/src/gestures/converter.dart @@ -170,7 +170,8 @@ class PointerEventConverter { radiusMin: datum.radiusMin, radiusMax: datum.radiusMax, orientation: datum.orientation, - tilt: datum.tilt + tilt: datum.tilt, + synthesized: true, ); state.lastPosition = position; } @@ -257,7 +258,8 @@ class PointerEventConverter { radiusMin: datum.radiusMin, radiusMax: datum.radiusMax, orientation: datum.orientation, - tilt: datum.tilt + tilt: datum.tilt, + synthesized: true, ); state.lastPosition = position; } diff --git a/packages/flutter/lib/src/gestures/events.dart b/packages/flutter/lib/src/gestures/events.dart index aeccd30227..0fdf87e61b 100644 --- a/packages/flutter/lib/src/gestures/events.dart +++ b/packages/flutter/lib/src/gestures/events.dart @@ -113,7 +113,8 @@ abstract class PointerEvent { this.radiusMin: 0.0, this.radiusMax: 0.0, this.orientation: 0.0, - this.tilt: 0.0 + this.tilt: 0.0, + this.synthesized: false, }); /// Time of event dispatch, relative to an arbitrary timeline. @@ -235,6 +236,18 @@ abstract class PointerEvent { /// the stylus is flat on that surface). final double tilt; + /// We occasionally synthesize PointerEvents that aren't exact translations + /// of [ui.PointerData] from the engine to cover small cross-OS discrepancies + /// in pointer behaviours. + /// + /// For instance, on end events, Android always drops any location changes + /// that happened between its reporting intervals when emiting the end events. + /// + /// On iOS, minor incorrect location changes from the previous move events + /// can be reported on end events. We synthesize a [PointerEvent] to cover + /// the difference between the 2 events in that case. + final bool synthesized; + @override String toString() => '$runtimeType($position)'; @@ -261,7 +274,8 @@ abstract class PointerEvent { 'radiusMin: $radiusMin, ' 'radiusMax: $radiusMax, ' 'orientation: $orientation, ' - 'tilt: $tilt' + 'tilt: $tilt, ' + 'synthesized: $synthesized' ')'; } } @@ -365,7 +379,8 @@ class PointerHoverEvent extends PointerEvent { double radiusMin: 0.0, double radiusMax: 0.0, double orientation: 0.0, - double tilt: 0.0 + double tilt: 0.0, + bool synthesized: false, }) : super( timeStamp: timeStamp, kind: kind, @@ -384,7 +399,8 @@ class PointerHoverEvent extends PointerEvent { radiusMin: radiusMin, radiusMax: radiusMax, orientation: orientation, - tilt: tilt + tilt: tilt, + synthesized: synthesized, ); } @@ -463,7 +479,8 @@ class PointerMoveEvent extends PointerEvent { double radiusMin: 0.0, double radiusMax: 0.0, double orientation: 0.0, - double tilt: 0.0 + double tilt: 0.0, + bool synthesized: false, }) : super( timeStamp: timeStamp, pointer: pointer, @@ -484,7 +501,8 @@ class PointerMoveEvent extends PointerEvent { radiusMin: radiusMin, radiusMax: radiusMax, orientation: orientation, - tilt: tilt + tilt: tilt, + synthesized: synthesized, ); } diff --git a/packages/flutter/lib/src/gestures/monodrag.dart b/packages/flutter/lib/src/gestures/monodrag.dart index 60f2985cee..15db9f3e81 100644 --- a/packages/flutter/lib/src/gestures/monodrag.dart +++ b/packages/flutter/lib/src/gestures/monodrag.dart @@ -130,7 +130,8 @@ abstract class DragGestureRecognizer extends OneSequenceGestureRecognizer { if (event is PointerMoveEvent) { final VelocityTracker tracker = _velocityTrackers[event.pointer]; assert(tracker != null); - tracker.addPosition(event.timeStamp, event.position); + if (!event.synthesized) + tracker.addPosition(event.timeStamp, event.position); final Offset delta = event.delta; if (_state == _DragState.accepted) { if (onUpdate != null) { diff --git a/packages/flutter/lib/src/gestures/multidrag.dart b/packages/flutter/lib/src/gestures/multidrag.dart index ee245bbcee..9ab4650087 100644 --- a/packages/flutter/lib/src/gestures/multidrag.dart +++ b/packages/flutter/lib/src/gestures/multidrag.dart @@ -62,7 +62,8 @@ abstract class MultiDragPointerState { void _move(PointerMoveEvent event) { assert(_arenaEntry != null); - _velocityTracker.addPosition(event.timeStamp, event.position); + if (!event.synthesized) + _velocityTracker.addPosition(event.timeStamp, event.position); if (_client != null) { assert(pendingDelta == null); // Call client last to avoid reentrancy. diff --git a/packages/flutter/lib/src/gestures/scale.dart b/packages/flutter/lib/src/gestures/scale.dart index 2fbc170368..2f5f423d26 100644 --- a/packages/flutter/lib/src/gestures/scale.dart +++ b/packages/flutter/lib/src/gestures/scale.dart @@ -152,7 +152,8 @@ class ScaleGestureRecognizer extends OneSequenceGestureRecognizer { if (event is PointerMoveEvent) { final VelocityTracker tracker = _velocityTrackers[event.pointer]; assert(tracker != null); - tracker.addPosition(event.timeStamp, event.position); + if (!event.synthesized) + tracker.addPosition(event.timeStamp, event.position); _pointerLocations[event.pointer] = event.position; shouldStartIfAccepted = true; } else if (event is PointerDownEvent) { diff --git a/packages/flutter/test/gestures/drag_test.dart b/packages/flutter/test/gestures/drag_test.dart index 3f6e2badd7..7b3d4b14be 100644 --- a/packages/flutter/test/gestures/drag_test.dart +++ b/packages/flutter/test/gestures/drag_test.dart @@ -236,6 +236,38 @@ void main() { drag.dispose(); }); + testGesture('Synthesized pointer events are ignored for velocity tracking', (GestureTester tester) { + final HorizontalDragGestureRecognizer drag = new HorizontalDragGestureRecognizer(); + + Velocity velocity; + drag.onEnd = (DragEndDetails details) { + velocity = details.velocity; + }; + + final TestPointer pointer = new TestPointer(1); + final PointerDownEvent down = pointer.down(const Offset(10.0, 25.0), timeStamp: const Duration(milliseconds: 10)); + drag.addPointer(down); + tester.closeArena(1); + tester.route(down); + tester.route(pointer.move(const Offset(20.0, 25.0), timeStamp: const Duration(milliseconds: 20))); + tester.route(pointer.move(const Offset(30.0, 25.0), timeStamp: const Duration(milliseconds: 30))); + tester.route(pointer.move(const Offset(40.0, 25.0), timeStamp: const Duration(milliseconds: 40))); + tester.route(pointer.move(const Offset(50.0, 25.0), timeStamp: const Duration(milliseconds: 50))); + tester.route(new PointerMoveEvent( + pointer: 1, + // Simulate a small synthesized wobble which would have slowed down the + // horizontal velocity from 1 px/ms and introduced a slight vertical velocity. + position: const Offset(51.0, 26.0), + timeStamp: const Duration(milliseconds: 60), + synthesized: true, + )); + tester.route(pointer.up(timeStamp: const Duration(milliseconds: 20))); + expect(velocity.pixelsPerSecond.dx, moreOrLessEquals(1000.0)); + expect(velocity.pixelsPerSecond.dy, moreOrLessEquals(0.0)); + + drag.dispose(); + }); + testGesture('Drag details', (GestureTester tester) { expect(new DragDownDetails(), hasOneLineDescription); expect(new DragStartDetails(), hasOneLineDescription);