mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
454 lines
16 KiB
Markdown
454 lines
16 KiB
Markdown
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>
|
|
```
|