Skip to content

Commit

Permalink
feat(gestures): fully working gesture events prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
localvoid committed Jun 5, 2018
1 parent 35834f4 commit b310dcf
Show file tree
Hide file tree
Showing 14 changed files with 658 additions and 278 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -22,6 +22,7 @@ ivi is a javascript (TypeScript) library for building web user interfaces.
- Children reconciliation with [minimum number of DOM operations](https://github.com/localvoid/ivi/blob/master/documentation/misc/children-reconciliation.md)
- Fast performance
- Test utilities
- **EXPERIMENTAL** [gesture events](https://github.com/localvoid/ivi/tree/master/packages/ivi-gestures)

## Library Size

Expand Down
143 changes: 135 additions & 8 deletions packages/ivi-gestures/README.md
@@ -1,14 +1,141 @@
# Gestures

It is an **EXPERIMENTAL** package that provides an abstraction on top of Touch and Mouse events.
It is an **EXPERIMENTAL** package that provides a gesture recognition system with automatic gesture disambiguation.

Instead of using raw native events for different pointers, all touch and mouse interactions should be implemented as
gestures. Native gestures like scrolling are also implemented as gestures, so when native gesture is detected we can
remove all event handlers to make sure that scrolling isn't waiting for responses from UI thread.
It solves many different problems:

All gesture recognizers are initialized lazily, so there is no overhead when elements are rendered to the document.
- Busy UI thread should not cause any jank when native scrolling is recognized.
- Supports use cases like Long Press DnD inside of a container with a native scrolling.
- There is no need to explicitly specify dependencies between gesture recognizers (traditional gesture APIs), automatic
gesture disambiguation algorithm should automatically resolve all conflicts.
- DnD events.
- Unified Touch and Mouse Events (Pointer events spec is useless:
[Issue#178](https://github.com/w3c/pointerevents/issues/178),
[Issue#216](https://github.com/w3c/pointerevents/issues/216))

It also provides extended capabilities to efficiently solve:
## Browser Compatibility

- Drag and Drop with complex use cases like long-pressure DnD inside a container with native scrolling
- Click-outside events
### Android Chrome (Opera, Samsung Internet, UC Browser)

[~78% Market Share](http://gs.statcounter.com/browser-market-share/mobile/worldwide)

Everything works perfectly.

### iOS Safari

[~17% Market Share](http://gs.statcounter.com/browser-market-share/mobile/worldwide)

Use cases that involve conflicting NativePan and Pan gestures are working unreliably when NativePan is recognizing
movement in one direction and Pan is recognizing movement in the opposite direction. When pointer moves really fast,
NativePan will often win because Safari starts sending uncancellable `TouchMove` events (impossible to fix). Haven't
seen any native applications that are using gestures in such way, terrible UX.

Other use cases are working perfectly.

### Mobile Firefox

[~0.3% Market Share](http://gs.statcounter.com/browser-market-share/mobile/worldwide)

All native gestures are permanently blocked when there are conflicting gesture recognizers (impossible to fix).

## Roadmap

- Add global velocity tracking. Recognizers should be able to declare when they need to track pointer velocity, instead
of tracking velocity by themselves.
- Add multitouch gestures: scale, rotate.
- Combine gesture recognizers. There should be a tool to combine several different recognizers so that they could
recognize different gestures concurrently.
- Add specialized event handlers for DnD `onDrop()`, `onDragOver()` etc.
- Add specialized event handlers when touching outside of the element.

## Gesture Disambiguation Algorithm

### Quick high-level overview of the algorithm

When first pointer is down, event dispatcher will instantiate gesture recognizers that should receive pointer event
(lazy initialization), then it will dispatch pointer event to them. Gesture recognizers in response should activate
itself with a `GestureController` function that available on all gesture recognizers at `controller` property.

Then we need to immediately reject conflicting recognizers that can't be recognized. For example, if we have two Pan
recognizers, then first one will never be recognized. This step is important when we completely override native
gestures, because on iOS Safari we always need to invoke `preventDefault()` on the first `TouchMove` event, otherwise
it can start sending non-cancelable events.

Then if there are still conflicting recognizers, we are starting to wait until recognizers will respond that they
recognized a gesture.

Then we need to check that other active recognizers doesn't depend on the time, so we are invoking lifecycle method
`shouldWait()` and if someone needs to wait, disambiguation algorithm will wait until all awaiting recognizers either
resolved or canceled.

Then if there are still several conflicting resolved recognizers, we just use "last recognizer wins" strategy. This
strategy works perfectly in all scenarios. If there are two Tap recognizers, innermost one will win because we are
dispatching pointer events in a capture mode.

### Constraint-based Gesture Disambiguation

This is just an idea and requires more time to research, especially how it will impact user experience, because gesture
recognition systems in native SDKs doesn't work like this.

Current gesture disambiguation algorithm starts by registering all active recognizers after the first touch down event,
and then tries different strategies to resolve conflicts until one gesture recognizer is left. With constraint-based
gesture disambiguation algorithm instead of canceling gesture recognizers, we can dynamically add more recognizers from
different touch down events, set them in a pending state and switch between them as soon as their constraints are
satisfied.

For example, we can register two pan gestures: one pan gesture that require two pointers and another one with one
pointer. With the current gesture disambiguation algorithm when we start panning with one pointer, there is no way we
can start triggering gesture recognizer with two pointers, until we release all pointers and start panning with two
pointers. With constraint-based gesture disambiguation algorithm when we start panning with one pointer and then
continue panning with two pointers, it should automatically switch to pan recognizer with two pointers because
recognizers with more pointers should have a higher priority and if we release one pointer it should go back to the
previous recognizer.

## Quirks

### Chrome 15px slop

Chrome doesn't send touch move events after the first one until 15px slop region is exceeded. But we need this events,
so that we can use them to resolve conflicts between native and custom gestures, and the trick is to invoke
`preventDefault()` on the first `TouchMove` event, then it will start sending all touch move events. The good news is
that invoking `preventDefault()` on the first `TouchMove` doesn't stop native gestures from recognizing, so we can
stop listening touch move events when we were able to recognize native gesture.

https://developers.google.com/web/updates/2014/05/A-More-Compatible-Smoother-Touch

### Mobile Safari is broken

- `preventDefault()` doesn't work when touchmove handler is registered in touchdown handler. https://bugs.webkit.org/show_bug.cgi?id=182521
- Broken scrolling and fixed elements: https://medium.com/@dvoytenko/amp-ios-scrolling-and-position-fixed-b854a5a0d451

### Mobile Safari scale events should be explicity blocked

When native scaling gesture is recognized, touch events will have `scale` property with a value less or greater than
`1`.

### Mobile Safari doesn't support `InputDeviceCapabilities`

https://wicg.github.io/InputDeviceCapabilities/

## Additional Resources

- [Getting touchy - everything you (n)ever wanted to know about touch and pointer events](https://patrickhlauke.github.io/getting-touchy-presentation/)
- [Touch Event behavior details across browsers](https://docs.google.com/document/d/12k_LL_Ot9GjF8zGWP9eI_3IMbSizD72susba0frg44Y)
- [Issues with touch events](https://docs.google.com/document/d/12-HPlSIF7-ISY8TQHtuQ3IqDi-isZVI0Yzv5zwl90VU)

### Gesture Recognition Systems

- [iOS](https://developer.apple.com/documentation/uikit/uigesturerecognizer)
- [Android](https://developer.android.com/training/gestures/)
- [gtk3](https://developer.gnome.org/gtk3/stable/Gestures.html)
- [Qt](http://doc.qt.io/qt-5/gestures-overview.html)
- [Flutter](https://github.com/flutter/flutter/tree/master/packages/flutter/lib/src/gestures) - Automatic gesture
disambiguation algorithm. `ivi-gestures` implementation were inspired by the ideas from this project, but now it is
using completely different heuristics for gesture disambiguation.
- [TouchScript](https://github.com/TouchScript/TouchScript/wiki) - Unity3D
- [Hammer.js](https://github.com/hammerjs/hammer.js) - Most popular gesture recognition lib for the web platform.
Unable to handle complex scenarios, flawed architecture.

### Non-standard recognizers

- [Pinch-to-Zoom plus](https://www.youtube.com/watch?v=x-hFyzdwoL8)
5 changes: 5 additions & 0 deletions packages/ivi-gestures/src/constants.ts
@@ -0,0 +1,5 @@
export const enum GestureConstants {
PanDistance = 8,
LongPressDelay = 500,
TapDelay = 300,
}
6 changes: 4 additions & 2 deletions packages/ivi-gestures/src/gesture_behavior.ts
Expand Up @@ -4,9 +4,11 @@ export const enum GestureBehavior {
PanDown = 1 << 2,
PanLeft = 1 << 3,
Press = 1 << 4,
Tap = 1 << 5,
Scale = 1 << 5,
Rotate = 1 << 7,
Tap = 1 << 6,
/**
* Native browser gestures.
* Native gesture
*/
Native = 1 << 6,

Expand Down
4 changes: 3 additions & 1 deletion packages/ivi-gestures/src/gesture_controller.ts
Expand Up @@ -3,8 +3,10 @@ import { GestureRecognizer } from "./gesture_recognizer";
export const enum GestureConflictResolverAction {
Activate,
Resolve,
Accept,
Reject,
Cancel,
Finish,
End,
}

export type GestureController = (recognizer: GestureRecognizer<any>, action: GestureConflictResolverAction) => void;

0 comments on commit b310dcf

Please sign in to comment.