Gestures ======== ```dart 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 _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(); 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()``: - as the events are received, they get examined to see if they fit the pattern for the gesture; if they do, then return an object with valid=true; if more events for this gesture could still come in, return finished=false. - if you returned valid=false finished=false, then the next call to this must not return valid=true - doing anything with the event or target other than reading state is a contract violation - you are allowed to call sendEvent() at any time during a processEvent() call, or after a call to processEvent(), assuming that the last such call returned valid=true, until the next call to processEvent() or cancel(). - set forceChoose=true on the return value if you are confident that this is the gesture the user meant, even if it's possible that another gesture is still claiming it's valid (e.g. a long press might forceChoose to override a scroll, if the user hasn't moved for a while) - if you send events, you can set prechoose=true to send the event even before the gesture has been chosen - if you send prechoose events, make sure to send corresponding "cancel" events if cancel() is called ```dart */ class PointerState { PointerState({this.gestures, this.chosen}) { if (gestures == null) gestures = new List(); } factory PointerState.clone(PointerState source) { return new PointerState(gestures: source.gestures, chosen: source.chosen); } List 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 _pointers = new SplayTreeMap(); 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.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 losers = new List(); _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(); } } } } /* ``` Gestures defined in the framework --------------------------------- ```dart SKY MODULE ```