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.)
This commit is contained in:
Adam Barth 2015-08-28 22:53:02 -07:00
parent 4adf70215d
commit bef55951a5
15 changed files with 281 additions and 136 deletions

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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() {

View File

@ -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) {

View File

@ -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);
});
}

View File

@ -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
);
}
}

View File

@ -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);

View File

@ -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));

View File

@ -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();
});
}

View File

@ -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);

View File

@ -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();

View File

@ -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());
});
}

View File

@ -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) {