abstract class GestureEvent extends Event { Gesture _gesture; Gesture get gesture => _gesture; } class GestureState { bool cancel = true; // if true, then cancel the gesture at this point bool capture = false; // (for PointerDownEvent) if true, then this pointer is relevant bool choose = false; // if true, the gesture thinks that other gestures should give up bool finished = true; // if true, we're ready for the next gesture to start // choose and cancel are mutually exclusive } class BufferedEvent { const BufferedEvent(this.event, this.coallesceGroup); final GestureEvent event; final int coallesceGroup; } abstract class Gesture extends EventTarget { Gesture(this.target) : super() { target.events.where((event) => event is PointerDownEvent || event is PointerMovedEvent || event is PointerUpEvent).listen(_handler); } final EventTarget target; bool _ready = true; // last event, we were finished bool get ready => _ready; bool _active = false; // we have not yet been canceled since we last started listening to a pointer bool get active => _active; bool _chosen = false; // we're the only possible gesture at this point bool get chosen => _chosen; // (!ready && !active) means we're discarding events until the user // gets to a state where a new gesture can begin // (active && !chosen) means we're collecting events until no other // gesture is valid, or until we take command GestureState processEvent(PointerEvent event); List<BufferedEvent> _eventBuffer; void choose() { // called by GestureManager // if you override this, make sure to call superclass choose() first assert(_active == true); assert(_chosen == false); _chosen = true; // if there are any buffered events, dispatch them on this if ((_eventBuffer != null) && (_eventBuffer.length > 0)) { // we make a copy of the event buffer first so that the array isn't mutated out from under us // while we are doing this var events = _eventBuffer; _eventBuffer = null; for (var item in events) dispatchEvent(item.event); } } void cancel() { // called by GestureManager // if you override this, make sure to call superclass cancel() last _active = false; _chosen = false; _eventBuffer = null; } // for use by subclasses only void sendEvent(GestureEvent event, { int coallesceGroup, // when queuing events, only the last event with each group is kept bool prechoose: false // if true, event should just be sent right away, not queued }) { assert(_active == true); assert(coallesceGroup == null || prechoose == false); event._gesture = this; if (_chosen || prechoose) { dispatchEvent(event); } else { if (_eventBuffer == null) _eventBuffer = new List<BufferedEvent>(); if (coallesceGroup != null) _eventBuffer.removeWhere((candidate) => candidate.coallesceGroup == coallesceGroup); _eventBuffer.add(new BufferedEvent(event, coallesceGroup)); } } void _handler(Event event) { bool wasActive = _active; if (_ready) { // reset the state to start a new gesture if (_active) module.application.gestureManager.cancelGesture(this); _active = true; _ready = false; } GestureState returnValue = processEvent(event); if (returnValue.capture) { assert(event is PointerDownEvent); if (event is PointerDownEvent) event.result.add(this); } if (returnValue.cancel) { assert(returnValue.choose == false); if (wasActive) module.application.gestureManager.cancelGesture(this); // if we never became active, then we never called addGesture() below _active = false; } else if (active == true) { if (wasActive == false || event is PointerDownEvent) module.application.gestureManager.addGesture(event, this); if (returnValue.choose == true) module.application.gestureManager.chooseGesture(this); } _ready = returnValue.finished; } } /*
Subclasses should override processEvent()
:
*/ class PointerState { PointerState({this.gestures, this.chosen}) { if (gestures == null) gestures = new List<Gesture>(); } factory PointerState.clone(PointerState source) { return new PointerState(gestures: source.gestures, chosen: source.chosen); } List<Gesture> gestures; bool chosen = false; } class GestureManager { GestureManager(this.target) { target.events.where((event) => event is PointerDownEvent).listen(_handler); } final EventTarget target; // usually the ApplicationRoot object Map<int, PointerState> _pointers = new SplayTreeMap<int, PointerState>(); void addGesture(PointerEvent event, Gesture gesture) { assert(gesture.active); var pointer = event.pointer; if (_pointers.containsKey(pointer)) { assert(!_pointers[pointer].gestures.contains(gesture)); if (_pointers[pointer].chosen) cancelGesture(gesture); else _pointers[pointer].gestures.add(gesture); } else { PointerState pointerState = new PointerState(); pointerState.gestures.add(gesture); _pointers[pointer] = pointerState; } } void cancelGesture(Gesture gesture) { _pointers.forEach((index, pointerState) => pointerState.gestures.remove(gesture)); gesture.cancel(); // get a static copy of the _pointers keys, so we can remove them safely var activePointers = new List<int>.from(_pointers.keys); // now walk our lists, removing pointers that are obsolete, and choosing // gestures from pointers that have only one outstanding gesture for (var pointer in activePointers) { var pointerState = _pointers[pointer]; if (pointerState.gestures.length == 0) { _pointers.remove(pointer); } else { if (pointerState.gestures.length == 1 && pointerState.chosen) { pointerState.chosen = true; pointerState.gestures[0].choose(); } } } } void chooseGesture(Gesture gesture) { if (!gesture.active) // this could happen e.g. if two gestures simultaneously add // themselves and chose themselves for the same PointerDownEvent return; List<Gesture> losers = new List<Gesture>(); _pointers.values .where((pointerState) => pointerState.gestures.contains(gesture)) .forEach((pointerState) { losers.addAll(pointerState.gestures.where((candidateLoser) => candidateLoser != gesture)); pointerState.gestures.clear(); pointerState.gestures.add(gesture); pointerState.chosen = true; }); assert(losers.every((loser) => loser.active)); losers.forEach((loser) { // we check loser.active because losers could contain duplicates // and we should only cancel each gesture once if (loser.active) loser.cancel(); assert(!loser.active); }); gesture.choose(); } PointerState getActiveGestures(int pointer) { if (_pointers.containsKey(pointer) && _pointers[pointer].gestures.length > 0) return new PointerState.clone(_pointers[pointer]); return new PointerState(); } void _handler(PointerDownEvent event) { var pointer = event.pointer; if (_pointers.containsKey(pointer)) { var pointerState = _pointers[pointer]; if ((!pointerState.chosen) && (pointerState.gestures.length == 1)) { pointerState.chosen = true; pointerState.gestures[0].choose(); } } } } /*
SKY MODULE <!-- not in dart:sky --> <!-- note: this hasn't been dartified yet --> <script> */ class TapGesture extends Gesture { TapGesture = Gesture; // internal state: // Integer numButtons = 0; // Boolean primaryDown = false; GestureState processEvent(Event event); // - let returnValue = { finished = false } // - if the event is a pointer-down: // - increment this.numButtons // - set returnValue.capture = true // - otherwise if it is a pointer-up: // - assert: this.numButtons > 0 // - decrement this.numButtons // - if numButtons == 0: // - set returnValue.finished = true // - if this.ready == false and this.active == false: // - return returnValue // - if EventTarget isn't an Element: // - assert: event is a pointer-down // - return returnValue // - if the event is pointer-down: // - assert: this.numButtons > 0 // - if it's primary: // - assert: this.ready==true // this is the first press // - this.primaryDown = true // - sendEvent() a tap-down event, with prechoose=true // - set returnValue.cancel = false // - return returnValue // - otherwise: // - if this.primaryDown == true and this.active == true: // - // this is some bogus secondary press that we should have prevent // // taps from starting until it's finished, but it doesn't invalidate // // the existing primary press // - set returnValue.cancel = false // - return returnValue // - otherwise: // - // this is some secondary press but we don't have a first press // // (maybe this is all in the context of a right-click or something) // // we have to wait til it's done before we can start a tap gesture again // - return returnValue // - if the event is pointer-move: // - assert: this.numButtons > 0 // - if it's primary: // - if it hit tests within target's bounding box: // - sendEvent() a tap-move event, with prechoose=true // - set returnValue.cancel = false // - return returnValue // - otherwise: // - sendEvent() a tap-cancel event, with prechoose=true // - return returnValue // - otherwise: // - // this is the move of some bogus secondary press // // ignore it, but continue listening if we have a primary button down // - if this.primaryDown == true and this.active == true: // - set returnValue.cancel = false // - return returnValue // - if the event is pointer-up: // - if it's primary: // - sendEvent() a tap event // - set this.primaryDown = false // - set returnValue.cancel = false // - return returnValue // - otherwise: // - // this is the 'up' of some bogus secondary press // // ignore it, but continue listening for our primary up if necessary // - if this.primaryDown == true and this.active == true: // - set returnValue.cancel = false // - return returnValue } class LongPressGesture extends Gesture { LongPressGesture = Gesture; GestureState processEvent(PointerEvent event); // long-tap-start: sent when the primary pointer goes down // long-tap-cancel: sent when cancel()ed or finger goes out of bounding box // long-tap: sent when the primary pointer is released } class DoubleTapGesture extends Gesture { DoubleTapGesture = Gesture; GestureState processEvent(PointerEvent event); // double-tap-start: sent when the primary pointer goes down the first time // double-tap-cancel: sent when cancel()ed or finger goes out of bounding box, or it times out // double-tap: sent when the primary pointer is released the second time within the timeout } abstract class ScrollGesture extends Gesture { ScrollGesture = Gesture; GestureState processEvent(PointerEvent event); // this fires the following events (inertia is a boolean, delta is a float): // scroll-start, with field inertia=false, delta=0; prechoose=true // scroll, with fields inertia (is this a simulated scroll from inertia or a real scroll?), delta (number of pixels to scroll); prechoose=true // scroll-end, with field inertia (same), delta=0; prechoose=true // scroll-start is fired right away // scroll is sent whenever the primary pointer moves while down // scroll is also sent after the pointer goes back up, based on inertia // scroll-end is sent after the pointer goes back up once the scroll reaches delta=0 // scroll-end is also sent when the gesture is canceled or reset // processEvent() returns: // - cancel=false pretty much always so long as there's a primary touch (e.g. not for a right-click) // - chose=true when you travel a certain distance // - finished=true when the primary pointer goes up } class HorizontalScrollGesture extends ScrollGesture { // a ScrollGesture giving x-axis scrolling HorizontalScrollGesture = ScrollGesture; } class VerticalScrollGesture extends ScrollGesture { // a ScrollGesture giving y-axis scrolling VerticalScrollGesture = ScrollGesture; } class PanGesture extends Gesture { PanGesture = Gesture; // similar to ScrollGesture, but with two axes // pan-start, pan, pan-end // events have inertia (boolean), dx (float), dy (float) } abstract class ZoomGesture extends Gesture { ZoomGesture = Gesture; GestureState processEvent(PointerEvent event); // zoom-start: sent when we could start zooming (e.g. for pinch-zoom, when two fingers hit the glass) (prechoose) // zoom-end: sent when cancel()ed after zoom-start, or when the fingers are lifted (prechoose) // zoom, with a 'scale' attribute, whose value is a multiple of the scale factor at zoom-start // e.g. if the user zooms to 2x, you'd get a bunch of 'zoom' events like scale=1.0, scale=1.17, ... scale=1.91, scale=2.0 } class PinchZoomGesture extends ZoomGesture { PinchZoomGesture = ZoomGesture; // a ZoomGesture for two-finger-pinch gesture // zoom is prechoose } class DoubleTapZoomGesture extends ZoomGesture { DoubleTapZoomGesture = ZoomGesture; // a ZoomGesture for the double-tap-slide gesture // when the slide starts, forceChoose } class PanAndZoomGesture extends Gesture { PanAndZoomGesture = Gesture; GestureState processEvent(PointerEvent event); // manipulate-start (prechoose) // manipulate: (prechoose) // panX, panY: pixels // scaleX, scaleY: a multiplier of the scale at manipulate-start // rotation: turns // manipulate-end (prechoose) } abstract class FlingGesture extends Gesture { FlingGesture = Gesture; GestureState processEvent(PointerEvent event); // fling-start: when the gesture begins (prechoose) // fling-move: while the user is directly dragging the element (has delta attribute with the distance from fling-start) (prechoose) // fling: the user has released the pointer and the decision is it was in fact flung // fling-cancel: cancel(), or the user has released the pointer and the decision is it was not flung (prechoose) // fling-end: cancel(), or after fling or fling-cancel (prechoose) } class FlingLeftGesture extends FlingGesture { FlingLeftGesture = FlingGesture; } class FlingRightGesture extends FlingGesture { FlingRightGesture = FlingGesture; } class FlingUpGesture extends FlingGesture { FlingUpGesture = FlingGesture; } class FlingDownGesture extends FlingGesture { FlingDownGesture = FlingGesture; } </script>