diff --git a/packages/flutter/lib/base/hit_test.dart b/packages/flutter/lib/base/hit_test.dart index 9f784a03b9..64a17158b1 100644 --- a/packages/flutter/lib/base/hit_test.dart +++ b/packages/flutter/lib/base/hit_test.dart @@ -28,7 +28,11 @@ class HitTestEntry { } class HitTestResult { - final List path = new List(); + HitTestResult({ List path }) + : path = path != null ? path : new List(); + + final List path; + void add(HitTestEntry data) { path.add(data); } diff --git a/packages/flutter/lib/base/pointer_router.dart b/packages/flutter/lib/base/pointer_router.dart index 5b12864d62..14d94f7cdc 100644 --- a/packages/flutter/lib/base/pointer_router.dart +++ b/packages/flutter/lib/base/pointer_router.dart @@ -4,11 +4,9 @@ import 'dart:sky' as sky; -import 'package:sky/base/hit_test.dart'; - typedef void _Route(sky.PointerEvent event); -class PointerRouter extends HitTestTarget { +class PointerRouter { final Map> _routeMap = new Map>(); void addRoute(int pointer, _Route route) { @@ -26,15 +24,11 @@ class PointerRouter extends HitTestTarget { _routeMap.remove(pointer); } - EventDisposition handleEvent(sky.Event e, HitTestEntry entry) { - if (e is! sky.PointerEvent) - return EventDisposition.ignored; - sky.PointerEvent event = e; + void route(sky.PointerEvent event) { List<_Route> routes = _routeMap[event.pointer]; if (routes == null) - return EventDisposition.ignored; + return; for (_Route route in new List<_Route>.from(routes)) route(event); - return EventDisposition.processed; } } diff --git a/packages/flutter/lib/gestures/arena.dart b/packages/flutter/lib/gestures/arena.dart index c074b3b810..17e70da4ec 100644 --- a/packages/flutter/lib/gestures/arena.dart +++ b/packages/flutter/lib/gestures/arena.dart @@ -42,37 +42,61 @@ class GestureArenaEntry { } } +class _GestureArenaState { + final List members = new List(); + bool isOpen = true; + + void add(GestureArenaMember member) { + assert(isOpen); + members.add(member); + } +} + /// The first member to accept or the last member to not to reject wins. class GestureArena { - final Map> _arenas = new Map>(); + final Map _arenas = new Map(); static final GestureArena instance = new GestureArena(); GestureArenaEntry add(Object key, GestureArenaMember member) { - List members = _arenas.putIfAbsent(key, () => new List()); - members.add(member); + _GestureArenaState state = _arenas.putIfAbsent(key, () => new _GestureArenaState()); + state.add(member); return new GestureArenaEntry._(this, key, member); } + void close(Object key) { + _GestureArenaState state = _arenas[key]; + if (state == null) + return; // This arena either never existed or has been resolved. + state.isOpen = false; + _tryToResolveArena(key, state); + } + + void _tryToResolveArena(Object key, _GestureArenaState state) { + assert(_arenas[key] == state); + assert(!state.isOpen); + if (state.members.length == 1) { + _arenas.remove(key); + state.members.first.acceptGesture(key); + } else if (state.members.isEmpty) { + _arenas.remove(key); + } + } + void _resolve(Object key, GestureArenaMember member, GestureDisposition disposition) { - List members = _arenas[key]; - if (members == null) + _GestureArenaState state = _arenas[key]; + if (state == null) return; // This arena has already resolved. - assert(members != null); - assert(members.contains(member)); + assert(!state.isOpen); + assert(state.members.contains(member)); if (disposition == GestureDisposition.rejected) { - members.remove(member); + state.members.remove(member); member.rejectGesture(key); - if (members.length == 1) { - _arenas.remove(key); - members.first.acceptGesture(key); - } else if (members.isEmpty) { - _arenas.remove(key); - } + _tryToResolveArena(key, state); } else { assert(disposition == GestureDisposition.accepted); _arenas.remove(key); - for (GestureArenaMember rejectedMember in members) { + for (GestureArenaMember rejectedMember in state.members) { if (rejectedMember != member) rejectedMember.rejectGesture(key); } diff --git a/packages/flutter/lib/gestures/recognizer.dart b/packages/flutter/lib/gestures/recognizer.dart index 5e43dfeb40..436b65a118 100644 --- a/packages/flutter/lib/gestures/recognizer.dart +++ b/packages/flutter/lib/gestures/recognizer.dart @@ -118,12 +118,14 @@ abstract class PrimaryPointerGestureRecognizer extends GestureRecognizer { } void rejectGesture(int pointer) { - _stopTimer(); - if (pointer == primaryPointer) + if (pointer == primaryPointer) { + _stopTimer(); state = GestureRecognizerState.defunct; + } } void didStopTrackingLastPointer() { + _stopTimer(); state = GestureRecognizerState.ready; } diff --git a/packages/flutter/lib/rendering/sky_binding.dart b/packages/flutter/lib/rendering/sky_binding.dart index 0a4b0f689e..59f761c889 100644 --- a/packages/flutter/lib/rendering/sky_binding.dart +++ b/packages/flutter/lib/rendering/sky_binding.dart @@ -7,6 +7,7 @@ import 'dart:sky' as sky; import 'package:sky/base/pointer_router.dart'; import 'package:sky/base/hit_test.dart'; import 'package:sky/base/scheduler.dart' as scheduler; +import 'package:sky/gestures/arena.dart'; import 'package:sky/rendering/box.dart'; import 'package:sky/rendering/object.dart'; import 'package:sky/rendering/view.dart'; @@ -30,7 +31,12 @@ class PointerState { typedef void EventListener(sky.Event event); -class SkyBinding { +class BindingHitTestEntry extends HitTestEntry { + const BindingHitTestEntry(HitTestTarget target, this.result) : super(target); + final HitTestResult result; +} + +class SkyBinding extends HitTestTarget { SkyBinding({ RenderBox root: null, RenderView renderViewOverride }) { assert(_instance == null); @@ -88,8 +94,8 @@ class SkyBinding { } else if (event is sky.GestureEvent) { dispatchEvent(event, hitTest(new Point(event.x, event.y))); } else { - for (EventListener e in _eventListeners) - e(event); + for (EventListener listener in _eventListeners) + listener(event); } } @@ -130,7 +136,7 @@ class SkyBinding { HitTestResult hitTest(Point position) { HitTestResult result = new HitTestResult(); - result.add(new HitTestEntry(pointerRouter)); + result.add(new BindingHitTestEntry(this, result)); _renderView.hitTest(result, position: position); return result; } @@ -148,6 +154,16 @@ class SkyBinding { return disposition; } + EventDisposition handleEvent(sky.Event e, BindingHitTestEntry entry) { + if (e is! sky.PointerEvent) + return EventDisposition.ignored; + sky.PointerEvent event = e; + pointerRouter.route(event); + if (event.type == 'pointerdown') + GestureArena.instance.close(event.pointer); + return EventDisposition.processed; + } + String toString() => 'Render Tree:\n${_renderView}'; void debugDumpRenderTree() { diff --git a/packages/flutter/lib/widgets/framework.dart b/packages/flutter/lib/widgets/framework.dart index 14421ef876..b990424801 100644 --- a/packages/flutter/lib/widgets/framework.dart +++ b/packages/flutter/lib/widgets/framework.dart @@ -1273,12 +1273,9 @@ class WidgetSkyBinding extends SkyBinding { assert(SkyBinding.instance is WidgetSkyBinding); } - EventDisposition dispatchEvent(sky.Event event, HitTestResult result) { - assert(SkyBinding.instance == this); - EventDisposition disposition = super.dispatchEvent(event, result); - if (disposition == EventDisposition.consumed) - return EventDisposition.consumed; - for (HitTestEntry entry in result.path.reversed) { + EventDisposition handleEvent(sky.Event event, BindingHitTestEntry entry) { + EventDisposition disposition = EventDisposition.ignored; + for (HitTestEntry entry in entry.result.path.reversed) { if (entry.target is! RenderObject) continue; for (Widget target in RenderObjectWrapper.getWidgetsForRenderObject(entry.target)) { @@ -1293,7 +1290,7 @@ class WidgetSkyBinding extends SkyBinding { target = target._parent; } } - return disposition; + return combineEventDispositions(disposition, super.handleEvent(event, entry)); } void beginFrame(double timeStamp) { diff --git a/packages/unit/test/base/pointer_router_test.dart b/packages/unit/test/base/pointer_router_test.dart index 835b95088a..6381722df0 100644 --- a/packages/unit/test/base/pointer_router_test.dart +++ b/packages/unit/test/base/pointer_router_test.dart @@ -1,6 +1,5 @@ import 'dart:sky' as sky; -import 'package:sky/base/hit_test.dart'; import 'package:sky/base/pointer_router.dart'; import 'package:test/test.dart'; @@ -13,15 +12,18 @@ void main() { callbackRan = true; } + TestPointer pointer2 = new TestPointer(2); + TestPointer pointer3 = new TestPointer(3); + PointerRouter router = new PointerRouter(); router.addRoute(3, callback); - expect(router.handleEvent(new TestPointerEvent(pointer: 2), null), equals(EventDisposition.ignored)); + router.route(pointer2.down()); expect(callbackRan, isFalse); - expect(router.handleEvent(new TestPointerEvent(pointer: 3), null), equals(EventDisposition.processed)); + router.route(pointer3.down()); expect(callbackRan, isTrue); callbackRan = false; router.removeRoute(3, callback); - expect(router.handleEvent(new TestPointerEvent(pointer: 3), null), equals(EventDisposition.ignored)); + router.route(pointer3.up()); expect(callbackRan, isFalse); }); } diff --git a/packages/unit/test/engine/mock_events.dart b/packages/unit/test/engine/mock_events.dart index a596ae1b7c..c7feb148ed 100644 --- a/packages/unit/test/engine/mock_events.dart +++ b/packages/unit/test/engine/mock_events.dart @@ -1,5 +1,7 @@ import 'dart:sky' as sky; +export 'dart:sky' show Point; + class TestPointerEvent extends sky.PointerEvent { TestPointerEvent({ this.type, @@ -79,3 +81,60 @@ class TestGestureEvent extends sky.GestureEvent { double velocityX; double velocityY; } + +class TestPointer { + TestPointer([ this.pointer = 1 ]); + + int pointer; + bool isDown = false; + sky.Point location; + + sky.PointerEvent down([sky.Point newLocation = sky.Point.origin ]) { + assert(!isDown); + isDown = true; + location = newLocation; + return new TestPointerEvent( + type: 'pointerdown', + pointer: pointer, + x: location.x, + y: location.y + ); + } + + sky.PointerEvent move([sky.Point newLocation = sky.Point.origin ]) { + assert(isDown); + sky.Offset delta = newLocation - location; + location = newLocation; + return new TestPointerEvent( + type: 'pointermove', + pointer: pointer, + x: newLocation.x, + y: newLocation.y, + dx: delta.dx, + dy: delta.dy + ); + } + + sky.PointerEvent up() { + assert(isDown); + isDown = false; + return new TestPointerEvent( + type: 'pointerup', + pointer: pointer, + x: location.x, + y: location.y + ); + } + + sky.PointerEvent cancel() { + assert(isDown); + isDown = false; + return new TestPointerEvent( + type: 'pointercancel', + pointer: pointer, + x: location.x, + y: location.y + ); + } + +} diff --git a/packages/unit/test/gestures/arena_test.dart b/packages/unit/test/gestures/arena_test.dart index 5b188c9207..b0da6f4cab 100644 --- a/packages/unit/test/gestures/arena_test.dart +++ b/packages/unit/test/gestures/arena_test.dart @@ -52,6 +52,7 @@ void main() { GestureArenaEntry firstEntry = arena.add(primaryKey, first); arena.add(primaryKey, second); + arena.close(primaryKey); expect(firstAcceptRan, isFalse); expect(firstRejectRan, isFalse); diff --git a/packages/unit/test/gestures/long_press_test.dart b/packages/unit/test/gestures/long_press_test.dart index e38e5a5db0..b97b4bdc6f 100644 --- a/packages/unit/test/gestures/long_press_test.dart +++ b/packages/unit/test/gestures/long_press_test.dart @@ -1,5 +1,6 @@ import 'package:quiver/testing/async.dart'; import 'package:sky/base/pointer_router.dart'; +import 'package:sky/gestures/arena.dart'; import 'package:sky/gestures/long_press.dart'; import 'package:sky/gestures/show_press.dart'; import 'package:test/test.dart'; @@ -32,8 +33,9 @@ void main() { new FakeAsync().run((async) { longPress.addPointer(down); + GestureArena.instance.close(5); expect(longPressRecognized, isFalse); - router.handleEvent(down, null); + router.route(down); expect(longPressRecognized, isFalse); async.elapse(new Duration(milliseconds: 300)); expect(longPressRecognized, isFalse); @@ -55,12 +57,13 @@ void main() { new FakeAsync().run((async) { longPress.addPointer(down); + GestureArena.instance.close(5); expect(longPressRecognized, isFalse); - router.handleEvent(down, null); + router.route(down); expect(longPressRecognized, isFalse); async.elapse(new Duration(milliseconds: 300)); expect(longPressRecognized, isFalse); - router.handleEvent(up, null); + router.route(up); expect(longPressRecognized, isFalse); async.elapse(new Duration(seconds: 1)); expect(longPressRecognized, isFalse); @@ -87,9 +90,10 @@ void main() { new FakeAsync().run((async) { showPress.addPointer(down); longPress.addPointer(down); + GestureArena.instance.close(5); expect(showPressRecognized, isFalse); expect(longPressRecognized, isFalse); - router.handleEvent(down, null); + router.route(down); expect(showPressRecognized, isFalse); expect(longPressRecognized, isFalse); async.elapse(new Duration(milliseconds: 300)); diff --git a/packages/unit/test/gestures/scroll_test.dart b/packages/unit/test/gestures/scroll_test.dart index 8558382944..bc8963df5e 100644 --- a/packages/unit/test/gestures/scroll_test.dart +++ b/packages/unit/test/gestures/scroll_test.dart @@ -1,92 +1,78 @@ import 'dart:sky' as sky; import 'package:sky/base/pointer_router.dart'; +import 'package:sky/gestures/arena.dart'; import 'package:sky/gestures/scroll.dart'; +import 'package:sky/gestures/tap.dart'; import 'package:test/test.dart'; import '../engine/mock_events.dart'; -TestPointerEvent down = new TestPointerEvent( - pointer: 5, - type: 'pointerdown', - x: 10.0, - y: 10.0 -); - -TestPointerEvent move1 = new TestPointerEvent( - pointer: 5, - type: 'pointermove', - x: 20.0, - y: 20.0, - dx: 10.0, - dy: 10.0 -); - -TestPointerEvent move2 = new TestPointerEvent( - pointer: 5, - type: 'pointermove', - x: 20.0, - y: 25.0, - dx: 0.0, - dy: 5.0 -); - -TestPointerEvent up = new TestPointerEvent( - pointer: 5, - type: 'pointerup', - x: 20.0, - y: 25.0 -); - void main() { - test('Should recognize scroll', () { + test('Should recognize pan', () { PointerRouter router = new PointerRouter(); - PanGestureRecognizer scroll = new PanGestureRecognizer(router: router); + PanGestureRecognizer pan = new PanGestureRecognizer(router: router); + TapGestureRecognizer tap = new TapGestureRecognizer(router: router); - bool didStartScroll = false; - scroll.onStart = () { - didStartScroll = true; + bool didStartPan = false; + pan.onStart = () { + didStartPan = true; }; - sky.Offset updateOffset; - scroll.onUpdate = (sky.Offset offset) { - updateOffset = offset; + sky.Offset updatedScrollDelta; + pan.onUpdate = (sky.Offset offset) { + updatedScrollDelta = offset; }; - bool didEndScroll = false; - scroll.onEnd = () { - didEndScroll = true; + bool didEndPan = false; + pan.onEnd = () { + didEndPan = true; }; - scroll.addPointer(down); - expect(didStartScroll, isFalse); - expect(updateOffset, isNull); - expect(didEndScroll, isFalse); + bool didTap = false; + tap.onTap = () { + didTap = true; + }; - router.handleEvent(down, null); - expect(didStartScroll, isFalse); - expect(updateOffset, isNull); - expect(didEndScroll, isFalse); + TestPointer pointer = new TestPointer(5); + sky.PointerEvent down = pointer.down(new Point(10.0, 10.0)); + pan.addPointer(down); + tap.addPointer(down); + GestureArena.instance.close(5); + expect(didStartPan, isFalse); + expect(updatedScrollDelta, isNull); + expect(didEndPan, isFalse); + expect(didTap, isFalse); - router.handleEvent(move1, null); - expect(didStartScroll, isTrue); - didStartScroll = false; - expect(updateOffset, new sky.Offset(10.0, -10.0)); - updateOffset = null; - expect(didEndScroll, isFalse); + router.route(down); + expect(didStartPan, isFalse); + expect(updatedScrollDelta, isNull); + expect(didEndPan, isFalse); + expect(didTap, isFalse); - router.handleEvent(move2, null); - expect(didStartScroll, isFalse); - expect(updateOffset, new sky.Offset(0.0, -5.0)); - updateOffset = null; - expect(didEndScroll, isFalse); + router.route(pointer.move(new Point(20.0, 20.0))); + expect(didStartPan, isTrue); + didStartPan = false; + expect(updatedScrollDelta, new sky.Offset(10.0, -10.0)); + updatedScrollDelta = null; + expect(didEndPan, isFalse); + expect(didTap, isFalse); - router.handleEvent(up, null); - expect(didStartScroll, isFalse); - expect(updateOffset, isNull); - expect(didEndScroll, isTrue); - didEndScroll = false; + router.route(pointer.move(new Point(20.0, 25.0))); + expect(didStartPan, isFalse); + expect(updatedScrollDelta, new sky.Offset(0.0, -5.0)); + updatedScrollDelta = null; + expect(didEndPan, isFalse); + expect(didTap, isFalse); - scroll.dispose(); + router.route(pointer.up()); + expect(didStartPan, isFalse); + expect(updatedScrollDelta, isNull); + expect(didEndPan, isTrue); + didEndPan = false; + expect(didTap, isFalse); + + pan.dispose(); + tap.dispose(); }); } diff --git a/packages/unit/test/gestures/show_press_test.dart b/packages/unit/test/gestures/show_press_test.dart index 8e6053624b..8bf2eadc5b 100644 --- a/packages/unit/test/gestures/show_press_test.dart +++ b/packages/unit/test/gestures/show_press_test.dart @@ -1,5 +1,6 @@ import 'package:quiver/testing/async.dart'; import 'package:sky/base/pointer_router.dart'; +import 'package:sky/gestures/arena.dart'; import 'package:sky/gestures/show_press.dart'; import 'package:test/test.dart'; @@ -31,8 +32,9 @@ void main() { new FakeAsync().run((async) { showPress.addPointer(down); + GestureArena.instance.close(5); expect(showPressRecognized, isFalse); - router.handleEvent(down, null); + router.route(down); expect(showPressRecognized, isFalse); async.elapse(new Duration(milliseconds: 300)); expect(showPressRecognized, isTrue); @@ -52,12 +54,13 @@ void main() { new FakeAsync().run((async) { showPress.addPointer(down); + GestureArena.instance.close(5); expect(showPressRecognized, isFalse); - router.handleEvent(down, null); + router.route(down); expect(showPressRecognized, isFalse); async.elapse(new Duration(milliseconds: 50)); expect(showPressRecognized, isFalse); - router.handleEvent(up, null); + router.route(up); expect(showPressRecognized, isFalse); async.elapse(new Duration(seconds: 1)); expect(showPressRecognized, isFalse); diff --git a/packages/unit/test/gestures/tap_test.dart b/packages/unit/test/gestures/tap_test.dart index 8935055b24..9d2dad201b 100644 --- a/packages/unit/test/gestures/tap_test.dart +++ b/packages/unit/test/gestures/tap_test.dart @@ -1,4 +1,5 @@ import 'package:sky/base/pointer_router.dart'; +import 'package:sky/gestures/arena.dart'; import 'package:sky/gestures/tap.dart'; import 'package:test/test.dart'; @@ -22,8 +23,9 @@ void main() { ); tap.addPointer(down); + GestureArena.instance.close(5); expect(tapRecognized, isFalse); - router.handleEvent(down, null); + router.route(down); expect(tapRecognized, isFalse); TestPointerEvent up = new TestPointerEvent( @@ -33,7 +35,7 @@ void main() { y: 9.0 ); - router.handleEvent(up, null); + router.route(up); expect(tapRecognized, isTrue); tap.dispose(); diff --git a/packages/unit/test/widget/gesture_detector_test.dart b/packages/unit/test/widget/gesture_detector_test.dart new file mode 100644 index 0000000000..084c1d8d27 --- /dev/null +++ b/packages/unit/test/widget/gesture_detector_test.dart @@ -0,0 +1,58 @@ +import 'package:sky/widgets.dart'; +import 'package:test/test.dart'; + +import '../engine/mock_events.dart'; +import 'widget_tester.dart'; + +void main() { + test('Uncontested scrolls start immediately', () { + WidgetTester tester = new WidgetTester(); + TestPointer pointer = new TestPointer(7); + + bool didStartScroll = false; + double updatedScrollDelta; + bool didEndScroll = false; + + Widget builder() { + return new GestureDetector( + onVerticalScrollStart: () { + didStartScroll = true; + }, + onVerticalScrollUpdate: (double scrollDelta) { + updatedScrollDelta = scrollDelta; + }, + onVerticalScrollEnd: () { + didEndScroll = true; + }, + child: new Container() + ); + } + + tester.pumpFrame(builder); + expect(didStartScroll, isFalse); + expect(updatedScrollDelta, isNull); + expect(didEndScroll, isFalse); + + Point firstLocation = new Point(10.0, 10.0); + tester.dispatchEvent(pointer.down(firstLocation), firstLocation); + expect(didStartScroll, isTrue); + didStartScroll = false; + expect(updatedScrollDelta, isNull); + expect(didEndScroll, isFalse); + + Point secondLocation = new Point(10.0, 9.0); + tester.dispatchEvent(pointer.move(secondLocation), secondLocation); + expect(didStartScroll, isFalse); + expect(updatedScrollDelta, 1.0); + updatedScrollDelta = null; + expect(didEndScroll, isFalse); + + tester.dispatchEvent(pointer.up(), secondLocation); + expect(didStartScroll, isFalse); + expect(updatedScrollDelta, isNull); + expect(didEndScroll, isTrue); + didEndScroll = false; + + tester.pumpFrame(() => new Container()); + }); +} diff --git a/packages/unit/test/widget/widget_tester.dart b/packages/unit/test/widget/widget_tester.dart index 3d610c3c88..187a360d32 100644 --- a/packages/unit/test/widget/widget_tester.dart +++ b/packages/unit/test/widget/widget_tester.dart @@ -89,29 +89,22 @@ class WidgetTester { return SkyBinding.instance.dispatchEvent(event, result); } - void tap(Widget widget) { + void tap(Widget widget, { int pointer: 1 }) { Point location = getCenter(widget); HitTestResult result = _hitTest(location); - _dispatchEvent(new TestPointerEvent(type: 'pointerdown', x: location.x, y: location.y), result); - _dispatchEvent(new TestPointerEvent(type: 'pointerup', x: location.x, y: location.y), result); + TestPointer p = new TestPointer(pointer); + _dispatchEvent(p.down(location), result); + _dispatchEvent(p.up(), result); } - void scroll(Widget widget, Offset offset) { + void scroll(Widget widget, Offset offset, { int pointer: 1 }) { Point startLocation = getCenter(widget); - HitTestResult result = _hitTest(startLocation); - _dispatchEvent(new TestPointerEvent(type: 'pointerdown', x: startLocation.x, y: startLocation.y), result); Point endLocation = startLocation + offset; - _dispatchEvent( - new TestPointerEvent( - type: 'pointermove', - x: endLocation.x, - y: endLocation.y, - dx: offset.dx, - dy: offset.dy - ), - result - ); - _dispatchEvent(new TestPointerEvent(type: 'pointerup', x: endLocation.x, y: endLocation.y), result); + HitTestResult result = _hitTest(startLocation); + TestPointer p = new TestPointer(pointer); + _dispatchEvent(p.down(startLocation), result); + _dispatchEvent(p.move(endLocation), result); + _dispatchEvent(p.up(), result); } void dispatchEvent(sky.Event event, Point location) {