| 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<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()``: |
| - 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<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(); |
| } |
| } |
| } |
| |
| } |
| /* |
| ``` |
| |
| Gestures defined in the framework |
| --------------------------------- |
| |
| ```dart |
| 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> |
| ``` |