Commit bef55951 authored by Adam Barth's avatar Adam Barth

Scrolls should start immediately when possible

If there are no other gestures in the arena, we should kick off the scroll
gesture right away. This change pulled a refactoring of how we dispatch events
to Widgets. Now we dispatch events to Widgets interleaved with their associated
RenderObjects. (Previously we dispatched to all of the RenderObjects first.)
parent 4adf7021
......@@ -28,7 +28,11 @@ class HitTestEntry {
}
class HitTestResult {
final List<HitTestEntry> path = new List<HitTestEntry>();
HitTestResult({ List<HitTestEntry> path })
: path = path != null ? path : new List<HitTestEntry>();
final List<HitTestEntry> path;
void add(HitTestEntry data) {
path.add(data);
}
......
......@@ -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<int, List<_Route>> _routeMap = new Map<int, List<_Route>>();
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;
}
}
......@@ -42,37 +42,61 @@ class GestureArenaEntry {
}
}
class _GestureArenaState {
final List<GestureArenaMember> members = new List<GestureArenaMember>();
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<Object, List<GestureArenaMember>> _arenas = new Map<Object, List<GestureArenaMember>>();
final Map<Object, _GestureArenaState> _arenas = new Map<Object, _GestureArenaState>();
static final GestureArena instance = new GestureArena();
GestureArenaEntry add(Object key, GestureArenaMember member) {
List<GestureArenaMember> members = _arenas.putIfAbsent(key, () => new List<GestureArenaMember>());
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<GestureArenaMember> 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);
}
......
......@@ -118,12 +118,14 @@ abstract class PrimaryPointerGestureRecognizer extends GestureRecognizer {
}
void rejectGesture(int pointer) {
if (pointer == primaryPointer) {
_stopTimer();
if (pointer == primaryPointer)
state = GestureRecognizerState.defunct;
}
}
void didStopTrackingLastPointer() {
_stopTimer();
state = GestureRecognizerState.ready;
}
......
......@@ -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() {
......
......@@ -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) {
......
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);
});
}
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
);
}
}
......@@ -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);
......
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));
......
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);
router.handleEvent(down, null);
expect(didStartScroll, isFalse);
expect(updateOffset, isNull);
expect(didEndScroll, 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.handleEvent(move2, null);
expect(didStartScroll, isFalse);
expect(updateOffset, new sky.Offset(0.0, -5.0));
updateOffset = null;
expect(didEndScroll, isFalse);
router.handleEvent(up, null);
expect(didStartScroll, isFalse);
expect(updateOffset, isNull);
expect(didEndScroll, isTrue);
didEndScroll = false;
bool didTap = false;
tap.onTap = () {
didTap = true;
};
scroll.dispose();
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.route(down);
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isFalse);
expect(didTap, 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.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);
router.route(pointer.up());
expect(didStartPan, isFalse);
expect(updatedScrollDelta, isNull);
expect(didEndPan, isTrue);
didEndPan = false;
expect(didTap, isFalse);
pan.dispose();
tap.dispose();
});
}
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);
......
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();
......
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());
});
}
......@@ -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) {
......
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