├── .editorconfig
├── .gitignore
├── .travis.yml
├── HISTORY.md
├── LICENSE
├── README.md
├── examples
├── simple.html
└── simple.tsx
├── index.js
├── package.json
├── src
├── config.tsx
├── index.tsx
└── util.tsx
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | # Unix-style newlines with a newline ending every file
5 | [*.{js,css}]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 2
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | *.log
3 | *.log.*
4 | .idea/
5 | .ipr
6 | .iws
7 | *~
8 | ~*
9 | *.diff
10 | *.patch
11 | *.bak
12 | .DS_Store
13 | Thumbs.db
14 | .project
15 | .*proj
16 | .svn/
17 | *.swp
18 | *.swo
19 | *.pyc
20 | *.pyo
21 | .build
22 | node_modules
23 | .cache
24 | dist
25 | assets/**/*.css
26 | build
27 | lib
28 | es
29 | /coverage
30 | yarn.lock
31 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | sudo: false
4 |
5 | notifications:
6 | email:
7 | - hust2012jiangkai@gmail.com
8 |
9 | node_js:
10 | - 6.0.0
11 |
12 | before_install:
13 | - |
14 | if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(\.md$)|(^(docs|examples))/'
15 | then
16 | echo "Only docs were updated, stopping build process."
17 | exit
18 | fi
19 | phantomjs --version
20 |
21 | script:
22 | - |
23 | if [ "$TEST_TYPE" = test ]; then
24 | npm test
25 | else
26 | npm run $TEST_TYPE
27 | fi
28 |
29 | env:
30 | matrix:
31 | - TEST_TYPE=lint
32 | - TEST_TYPE=test
33 | - TEST_TYPE=coverage
34 | - TEST_TYPE=saucelabs
35 | global:
36 | - secure: S1VwbaPzLnSH/IUT/wlJulxAX5VHRIDmSt53h/ycHcZsszUpWcLCJRQAe0fTVB2dAx5MdBbSZ+o+tr3tRwVB5TRAYm0oTCsYAkOZaWOB28RuUQtdGt3wf9xxTG1UiPiaLLUW3waX9zAaf3yqKBcJGf1op0RD8dksxbCFw/7xVbU=
37 | - secure: EBEDg8k///IlEsnx0AE8X3mbFl0QE5+xGKbG4AxXlGZda12uTIPUSMKJzdZQ2hVbZXduTzf1cQl9rcu9nGoSnkL/DWnIax9cvHi+1orx5+YPlxPHNWAwWByTnHosBn2MJhfy1s5paJfHC9cUzmmEL6x4fYthWxjsPUo+irEZH6E=
38 |
39 |
40 | matrix:
41 | allow_failures:
42 | - env: "TEST_TYPE=saucelabs"
43 |
--------------------------------------------------------------------------------
/HISTORY.md:
--------------------------------------------------------------------------------
1 |
2 | 0.0.20 / 2018-06-04
3 | ==================
4 |
5 | * fix and refine:
6 | 1. Add the event as a gesture property, and can be exposed to consumer. E.g.: rc-swipeout need using event to prevent scroll when touch moving.
7 | 2. Fixed a bug: panMove event could be invoked by unavailable touches. Look at the image for intuitional understanding: https://gw.alipayobjects.com/zos/rmsportal/nJviUPgzjtrUGCKrvUCz.gif.
8 | * change version to 0.0.20
9 |
10 | 0.0.17 / 2018-05-24
11 | ==================
12 |
13 | * change version to 0.0.17
14 | * fix: prevent view scroll when touch moving
15 |
16 | 0.0.7 / 2017-08-30
17 | ==================
18 |
19 | * Support direction lock.
20 |
21 | 0.0.6 / 2017-08-29
22 | ==================
23 |
24 | * Use css touch-action;
25 |
26 | 0.0.4 / 2017-08-28
27 | ==================
28 |
29 | * Support pan.
30 |
31 | 0.0.3 / 2017-07-24
32 | ==================
33 |
34 | * Support onPinchIn, onPinchOut.
35 |
36 | 0.0.2 / 2017-07-21
37 | ==================
38 |
39 | * Add umd dist output.
40 |
41 | 0.0.1 / 2017-07-21
42 | ==================
43 |
44 | * Support Tap, Press, Swipe, Pinch, Rotate.
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is
9 | furnished to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in
12 | all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # rc-gesture
2 | ---
3 |
4 | Support gesture for react component, inspired by [hammer.js](https://github.com/hammerjs/hammer.js) and [AlloyFinger](https://github.com/AlloyTeam/AlloyFinger).
5 |
6 | [![NPM version][npm-image]][npm-url]
7 | [![build status][travis-image]][travis-url]
8 | [![Test coverage][coveralls-image]][coveralls-url]
9 | [![gemnasium deps][gemnasium-image]][gemnasium-url]
10 | [![node version][node-image]][node-url]
11 | [![npm download][download-image]][download-url]
12 |
13 | [npm-image]: http://img.shields.io/npm/v/rc-gesture.svg?style=flat-square
14 | [npm-url]: http://npmjs.org/package/rc-gesture
15 | [travis-image]: https://img.shields.io/travis/react-component/gesture.svg?style=flat-square
16 | [travis-url]: https://travis-ci.org/react-component/gesture
17 | [coveralls-image]: https://img.shields.io/coveralls/react-component/gesture.svg?style=flat-square
18 | [coveralls-url]: https://coveralls.io/r/react-component/gesture?branch=master
19 | [gemnasium-image]: http://img.shields.io/gemnasium/react-component/gesture.svg?style=flat-square
20 | [gemnasium-url]: https://gemnasium.com/react-component/gesture
21 | [node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square
22 | [node-url]: http://nodejs.org/download/
23 | [download-image]: https://img.shields.io/npm/dm/rc-gesture.svg?style=flat-square
24 | [download-url]: https://npmjs.org/package/rc-gesture
25 |
26 | ## Screenshots
27 |
28 |
29 | ## Features
30 |
31 |
32 |
33 | ## Install
34 |
35 | ```bash
36 | npm install --save rc-gesture
37 | ```
38 |
39 | [](https://npmjs.org/package/rc-gesture)
40 |
41 | ## Usage
42 |
43 | ```tsx
44 | import Gesture from 'rc-gesture';
45 |
46 | ReactDOM.render(
47 | { console.log(gestureStatus); }}
49 | >
50 | container
51 | ,
52 | container);
53 | ```
54 |
55 |
56 | ## API
57 |
58 | all callback funtion will have one parammeter: `type GestureHandler = (s: IGestureStatus) => void;`
59 |
60 | - gesture: the rc-gesture state object, which contain all information you may need, see [gesture](#gesture)
61 |
62 | ### props:
63 |
64 | #### common props
65 |
66 |
67 |
68 | name |
69 | type |
70 | default |
71 | description |
72 |
73 |
74 |
75 |
76 | direction |
77 | string |
78 | `all` |
79 | control the allowed gesture direction, could be `['all', 'vertical', 'horizontal']` |
80 |
81 |
82 |
83 | #### Tap & Press
84 |
85 |
86 |
87 | name |
88 | type |
89 | default |
90 | description |
91 |
92 |
93 |
94 |
95 | onTap |
96 | function |
97 | |
98 | single tap callback |
99 |
100 |
101 | onPress |
102 | function |
103 | |
104 | long tap callback |
105 |
106 |
107 | onPressOut |
108 | function |
109 | |
110 | long tap end callback |
111 |
112 |
113 |
114 | #### Swipe
115 |
116 |
117 |
118 | name |
119 | type |
120 | default |
121 | description |
122 |
123 |
124 |
125 |
126 | onSwipe |
127 | function |
128 | |
129 | swipe callback, will triggered at the same time of all of below callback |
130 |
131 |
132 | onSwipeLeft |
133 | function |
134 | |
135 | swipe left callback |
136 |
137 |
138 | onSwipeRight |
139 | function |
140 | |
141 | swipe right callback |
142 |
143 |
144 | onSwipeTop |
145 | function |
146 | |
147 | swipe top callback |
148 |
149 |
150 | onSwipeBottom |
151 | function |
152 | |
153 | swipe bottom callback |
154 |
155 |
156 |
157 |
158 | #### Pan
159 |
160 |
161 |
162 | name |
163 | type |
164 | default |
165 | description |
166 |
167 |
168 |
169 |
170 | onPan |
171 | function |
172 | |
173 | pan callback, will triggered at the same time of all of below callback |
174 |
175 |
176 | onPanStart |
177 | function |
178 | |
179 | drag start callback |
180 |
181 |
182 | onPanMove |
183 | function |
184 | |
185 | drag move callback |
186 |
187 |
188 | onPanEnd |
189 | function |
190 | |
191 | drag end callback |
192 |
193 |
194 | onPanCancel |
195 | function |
196 | |
197 | drag cancel callback |
198 |
199 |
200 | onPanLeft |
201 | function |
202 | |
203 | pan left callback |
204 |
205 |
206 | onPanRight |
207 | function |
208 | |
209 | pan right callback |
210 |
211 |
212 | onPanTop |
213 | function |
214 | |
215 | pan top callback |
216 |
217 |
218 | onPanBottom |
219 | function |
220 | |
221 | pan bottom callback |
222 |
223 |
224 |
225 |
226 | #### Pinch
227 |
228 | pinch gesture is not enabled by default, you must set `props.enablePinch = true` at first;
229 |
230 |
231 |
232 |
233 | name |
234 | type |
235 | default |
236 | description |
237 |
238 |
239 |
240 |
241 | onPinch |
242 | function |
243 | |
244 | pinch callback, will triggered at the same time of all of below callback |
245 |
246 |
247 | onPinchStart |
248 | function |
249 | |
250 | pinch start callback |
251 |
252 |
253 | onPinchMove |
254 | function |
255 | |
256 | pinch move callback |
257 |
258 |
259 | onPinchEnd |
260 | function |
261 | |
262 | pinch end callback |
263 |
264 |
265 | onPanCancel |
266 | function |
267 | |
268 | pinch cancel callback |
269 |
270 |
271 | onPinchIn |
272 | function |
273 | |
274 | pinch in callback |
275 |
276 |
277 | onPinchOut |
278 | function |
279 | |
280 | pinch out callback |
281 |
282 |
283 |
284 |
285 |
286 | #### Rotate
287 |
288 | pinch gesture is not enabled by default, you must set `props.enableRotate = true` at first;
289 |
290 |
291 |
292 |
293 | name |
294 | type |
295 | default |
296 | description |
297 |
298 |
299 |
300 |
301 | onRotate |
302 | function |
303 | |
304 | rotate callback, will triggered at the same time of all of below callback |
305 |
306 |
307 | onRotateStart |
308 | function |
309 | |
310 | rotate start callback |
311 |
312 |
313 | onRotateMove |
314 | function |
315 | |
316 | rotate move callback |
317 |
318 |
319 | onRotateEnd |
320 | function |
321 | |
322 | rotate end callback |
323 |
324 |
325 | onRotateCancel |
326 | function |
327 | |
328 | rotate cancel callback |
329 |
330 |
331 |
332 |
333 | ## gesture
334 |
335 | ```tsx
336 | // http://hammerjs.github.io/api/#event-object
337 | export interface IGestureStauts {
338 | /* start status snapshot */
339 | startTime: number;
340 | startTouches: Finger[];
341 |
342 | startMutliFingerStatus?: MultiFingerStatus[];
343 |
344 | /* now status snapshot */
345 | time: number;
346 | touches: Finger[];
347 |
348 | mutliFingerStatus?: MultiFingerStatus[];
349 |
350 | /* delta status from touchstart to now, just for singe finger */
351 | moveStatus?: SingeFingerMoveStatus;
352 |
353 | /* whether is a long tap */
354 | press?: boolean;
355 |
356 | /* whether is a swipe*/
357 | swipe?: boolean;
358 | direction?: number;
359 |
360 | /* whether is in pinch process */
361 | pinch?: boolean;
362 | scale?: number;
363 |
364 | /* whether is in rotate process */
365 | rotate?: boolean;
366 | rotation?: number; // Rotation (in deg) that has been done when multi-touch. 0 on a single touch.
367 | };
368 | ```
369 |
370 | ## Development
371 |
372 | ```
373 | npm install
374 | npm start
375 | ```
376 |
377 | ## Example
378 |
379 | `npm start` and then go to `http://localhost:8005/examples/`
380 |
381 | Online examples: [http://react-component.github.io/gesture/](http://react-component.github.io/gesture/)
382 |
383 | ## Test Case
384 |
385 | `http://localhost:8005/tests/runner.html?coverage`
386 |
387 | ## Coverage
388 |
389 | `http://localhost:8005/node_modules/rc-server/node_modules/node-jscover/lib/front-end/jscoverage.html?w=http://localhost:8088/tests/runner.html?coverage`
390 |
391 | ## License
392 |
393 | `rc-gesture` is released under the MIT license.
394 |
--------------------------------------------------------------------------------
/examples/simple.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/react-component/gesture/b47e78d8270291b5a140ed56482985c3cf152888/examples/simple.html
--------------------------------------------------------------------------------
/examples/simple.tsx:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-console no-unused-expression */
2 |
3 | import Gesture from 'rc-gesture';
4 | import React, { Component } from 'react';
5 | import ReactDOM from 'react-dom';
6 | const style = `
7 | .outter {
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 | margin: 20px auto;
12 | width: 80%;
13 | height: 40px;
14 | border-width: 1px;
15 | border-color: red;
16 | border-style: solid;
17 | overflow: hidden;
18 | }
19 | .inner {
20 | width: 80%;
21 | height: 80%;
22 | background-color: black;
23 | }
24 | .swiper-container{
25 | margin: 20px 0;
26 | }
27 | .swiper{
28 | display: flex;
29 | align-items: center;
30 | text-align: center;
31 | background-color: #CCC;
32 | width: 100%;
33 | height: 100%;
34 | }
35 | `;
36 |
37 | class Demo extends Component {
38 | private root: any;
39 | private rootNode: any;
40 | private _scale: number;
41 | private _rotation: number;
42 | private _x: number;
43 | private _y: number;
44 |
45 | constructor(props) {
46 | super(props);
47 | }
48 | log = (type: string, keys?: string[]) => (...args) => {
49 | window.requestAnimationFrame(() => {
50 | this.doLog(type, keys, ...args);
51 | this.doTransform(type, ...args);
52 | });
53 | }
54 | doLog = (type, keys, ...args) => {
55 | const extInfo = keys ? keys.map(key => `${key} = ${args[0][key]}`).join(', ') : '';
56 | const logEl = this.refs.log as any;
57 | logEl.innerHTML += `${type} ${extInfo}
`;
58 | logEl.scrollTop = logEl.scrollHeight;
59 | }
60 | doTransform = (type, ...args) => {
61 | if (type === 'onPinch') {
62 | const { scale } = args[0];
63 | this._scale = scale;
64 | }
65 | if (type === 'onRotate') {
66 | const { rotation } = args[0];
67 | this._rotation = rotation;
68 | }
69 | if (type === 'onPan') {
70 | const { x, y } = args[0].moveStatus;
71 | this._x = x;
72 | this._y = y;
73 | }
74 | if (type === 'onPanEnd' || type === 'onPanCancel') {
75 | const { x, y } = args[0].moveStatus;
76 | this._x = 0;
77 | this._y = 0;
78 | }
79 | let transform: any = [];
80 | this._scale && transform.push(`scale(${this._scale})`);
81 | this._rotation && transform.push(`rotate(${this._rotation}deg)`);
82 | typeof this._x === 'number' && transform.push(`translateX(${this._x}px)`);
83 | typeof this._y === 'number' && transform.push(`translateY(${this._y}px)`);
84 | transform = transform.join(' ');
85 | this.rootNode = ReactDOM.findDOMNode(this.root);
86 | this.rootNode.style.transform = transform;
87 | }
88 | moveSwiper(e) {
89 | const {srcEvent, moveStatus} = e;
90 | const {x, y} = e.moveStatus;
91 |
92 | this.swiperNode = ReactDOM.findDOMNode(this.refSwiper);
93 | this.swiperNode.style.transform = [`translateX(${x}px)`];
94 |
95 | // preventDefault, avoid trigger scroll event when touch moving.
96 | srcEvent.preventDefault();
97 | }
98 |
99 | resetSwiper() {
100 | this.swiperNode = ReactDOM.findDOMNode(this.refSwiper);
101 | this.swiperNode.style.transform = [`translateX(0px)`];
102 | }
103 |
104 | render() {
105 | return (
106 |
107 |
108 |
109 |
110 |
144 | { this.root = el; }}>
145 |
146 |
147 |
148 |
149 |
{ this.moveSwiper(e, args); } }
152 | onPanEnd={ () => { this.resetSwiper(); } }
153 | onTouchMove={ (e) => { console.log('still run touch move'); } }
154 | >
155 |
156 |
{ this.refSwiper = e; } }>
157 | This is simple swiper demo. Only allow horizontal direction and height=200px to test scroll event.
158 |
159 |
160 |
161 |
162 |
163 | );
164 | }
165 | }
166 |
167 | ReactDOM.render(, document.getElementById('__react-content'));
168 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './src/index.tsx';
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rc-gesture",
3 | "version": "0.0.22",
4 | "description": "Support gesture for react component",
5 | "keywords": [
6 | "react",
7 | "react-component",
8 | "react-gesture",
9 | "gesture",
10 | "touch"
11 | ],
12 | "homepage": "http://github.com/react-component/gesture/",
13 | "repository": {
14 | "type": "git",
15 | "url": "git@github.com:react-component/gesture.git"
16 | },
17 | "bugs": {
18 | "url": "http://github.com/react-component/gesture/issues"
19 | },
20 | "files": [
21 | "lib",
22 | "es",
23 | "dist"
24 | ],
25 | "license": "MIT",
26 | "main": "./lib/index",
27 | "module": "./es/index",
28 | "config": {
29 | "port": 8005,
30 | "entry": {
31 | "rc-gesture": [
32 | "./index.js"
33 | ]
34 | }
35 | },
36 | "scripts": {
37 | "build": "rc-tools run build",
38 | "dist": "rc-tools run dist --babel-runtime",
39 | "gh-pages": "rc-tools run gh-pages",
40 | "start": "rc-tools run server",
41 | "compile": "rc-tools run compile --babel-runtime",
42 | "watch": "rc-tools run watch",
43 | "prepublish": "rc-tools run guard",
44 | "prepare": "rc-tools run guard",
45 | "prepublishOnly": "rc-tools run guard",
46 | "pub": "rc-tools run pub --babel-runtime",
47 | "lint": "rc-tools run lint",
48 | "test": "jest",
49 | "coverage": "jest --coverage"
50 | },
51 | "jest": {
52 | "collectCoverageFrom": [
53 | "src/*"
54 | ],
55 | "transform": {
56 | "\\.tsx?$": "./node_modules/rc-tools/scripts/jestPreprocessor.js",
57 | "\\.jsx?$": "./node_modules/rc-tools/scripts/jestPreprocessor.js"
58 | }
59 | },
60 | "devDependencies": {
61 | "@types/react": "~16.0.36",
62 | "@types/react-dom": "16.0.3",
63 | "coveralls": "^2.11.15",
64 | "enzyme": "^2.8.0",
65 | "enzyme-to-json": "^1.5.1",
66 | "jest": "^20.0.4",
67 | "pre-commit": "1.x",
68 | "rc-tools": "^6.3.6",
69 | "react": "^15.2.1",
70 | "react-dom": "^15.2.1",
71 | "react-test-renderer": "^15.5.4"
72 | },
73 | "pre-commit": [
74 | "lint"
75 | ],
76 | "typings": "./lib/index.d.ts",
77 | "dependencies": {
78 | "babel-runtime": "6.x"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/config.tsx:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-bitwise */
2 |
3 | // http://hammerjs.github.io/api/#directions
4 | export const DIRECTION_NONE = 1; // 00001
5 | export const DIRECTION_LEFT = 2; // 00010
6 | export const DIRECTION_RIGHT = 4; // 00100
7 | export const DIRECTION_UP = 8; // 01000
8 | export const DIRECTION_DOWN = 16; // 10000
9 |
10 | export const DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; // 00110 6
11 | export const DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN; // 11000 24
12 | export const DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; // 11110 30
13 |
14 | // http://hammerjs.github.io/recognizer-press/
15 | export const PRESS = {
16 | time: 251, // Minimal press time in ms.
17 | };
18 |
19 | // http://hammerjs.github.io/recognizer-swipe/
20 | export const SWIPE = {
21 | threshold: 10,
22 | velocity: 0.3,
23 | };
24 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-console */
2 | import React, { Component, TouchEventHandler } from 'react';
3 | import {
4 | calcRotation,
5 | getEventName, now,
6 | calcMutliFingerStatus, calcMoveStatus,
7 | shouldTriggerSwipe, shouldTriggerDirection,
8 | getMovingDirection, getDirectionEventName,
9 | } from './util';
10 | import { PRESS, DIRECTION_ALL, DIRECTION_VERTICAL, DIRECTION_HORIZONTAL } from './config';
11 |
12 | export declare type GestureHandler = (s: IGestureStatus) => void;
13 |
14 | export declare type Finger = {
15 | x: number; // e.touches[i].pageX
16 | y: number; // e.touches[i].pageY
17 | };
18 |
19 | export declare type MultiFingerStatus = {
20 | x: number;
21 | y: number;
22 | z: number;
23 | angle: number;
24 | };
25 |
26 | export declare type SingeFingerMoveStatus = {
27 | x: number;
28 | y: number;
29 | z: number;
30 | time: number;
31 | velocity: number;
32 | angle: number;
33 | };
34 |
35 | export interface IGesture {
36 | // config options
37 | enableRotate?: boolean;
38 | enablePinch?: boolean;
39 |
40 | // control allowed direction
41 | direction?: 'all' | 'vertical' | 'horizontal';
42 |
43 | // pinch: s.zoom
44 | onPinch?: GestureHandler;
45 | onPinchStart?: GestureHandler;
46 | onPinchMove?: GestureHandler;
47 | onPinchEnd?: GestureHandler;
48 | onPinchCancel?: GestureHandler;
49 | onPinchIn?: GestureHandler;
50 | onPinchOut?: GestureHandler;
51 |
52 | // rotate: s.angle
53 | onRotate?: GestureHandler;
54 | onRotateStart?: GestureHandler;
55 | onRotateMove?: GestureHandler;
56 | onRotateEnd?: GestureHandler;
57 | onRotateCancel?: GestureHandler;
58 |
59 | // pan: s.delta
60 | onPan?: GestureHandler;
61 | onPanStart?: GestureHandler;
62 | onPanMove?: GestureHandler;
63 | onPanEnd?: GestureHandler;
64 | onPanCancel?: GestureHandler;
65 | onPanLeft?: GestureHandler;
66 | onPanRight?: GestureHandler;
67 | onPanUp?: GestureHandler;
68 | onPanDown?: GestureHandler;
69 |
70 | // tap
71 | onTap?: GestureHandler;
72 |
73 | // long tap
74 | onPress?: GestureHandler;
75 | onPressUp?: GestureHandler;
76 |
77 | // swipe
78 | onSwipe?: GestureHandler;
79 | onSwipeLeft?: GestureHandler;
80 | onSwipeRight?: GestureHandler;
81 | onSwipeUp?: GestureHandler;
82 | onSwipeDown?: GestureHandler;
83 |
84 | // original dom element event handler
85 | onTouchStart?: TouchEventHandler;
86 | onTouchMove?: TouchEventHandler;
87 | onTouchEnd?: TouchEventHandler;
88 | onTouchCancel?: TouchEventHandler;
89 | };
90 |
91 | // http://hammerjs.github.io/api/#event-object
92 | export interface IGestureStatus {
93 | /* start status snapshot */
94 | startTime: number;
95 | startTouches: Finger[];
96 |
97 | startMutliFingerStatus?: MultiFingerStatus[];
98 |
99 | /* now status snapshot */
100 | time: number;
101 | touches: Finger[];
102 | preTouches: Finger[];
103 |
104 | mutliFingerStatus?: MultiFingerStatus[];
105 |
106 | /* delta status from touchstart to now, just for singe finger */
107 | moveStatus?: SingeFingerMoveStatus;
108 |
109 | /* whether is a long tap */
110 | press?: boolean;
111 |
112 | /* whether is a pan */
113 | pan?: boolean;
114 | /* whether is an available pan */
115 | availablePan?: boolean;
116 |
117 | /* whether is a swipe*/
118 | swipe?: boolean;
119 | direction?: number;
120 |
121 | /* whether is in pinch process */
122 | pinch?: boolean;
123 | scale?: number;
124 |
125 | /* whether is in rotate process */
126 | rotate?: boolean;
127 | rotation?: number; // Rotation (in deg) that has been done when multi-touch. 0 on a single touch.
128 |
129 | /* event, such as TouchEvent, MouseEvent, PointerEvent */
130 | srcEvent: any;
131 | };
132 |
133 | const directionMap = {
134 | all: DIRECTION_ALL,
135 | vertical: DIRECTION_VERTICAL,
136 | horizontal: DIRECTION_HORIZONTAL,
137 | };
138 |
139 | export default class Gesture extends Component {
140 | static defaultProps = {
141 | enableRotate: false,
142 | enablePinch: false,
143 | direction: 'all',
144 | };
145 |
146 | state = {
147 | };
148 |
149 | protected gesture: IGestureStatus;
150 |
151 | protected event: any;
152 |
153 | private pressTimer: NodeJS.Timer;
154 |
155 | private directionSetting: number;
156 |
157 | constructor(props) {
158 | super(props);
159 | this.directionSetting = directionMap[props.direction];
160 | }
161 |
162 | triggerEvent = (name, ...args) => {
163 | const cb = this.props[name];
164 | if (typeof cb === 'function') {
165 | // always give user gesture object as first params first
166 | cb(this.getGestureState(), ...args);
167 | }
168 | }
169 | triggerCombineEvent = (mainEventName, eventStatus, ...args) => {
170 | this.triggerEvent(mainEventName, ...args);
171 | this.triggerSubEvent(mainEventName, eventStatus, ...args);
172 |
173 | }
174 | triggerSubEvent = (mainEventName, eventStatus, ...args) => {
175 | if (eventStatus) {
176 | const subEventName = getEventName(mainEventName, eventStatus);
177 | this.triggerEvent(subEventName, ...args);
178 | }
179 | }
180 | triggerPinchEvent = (mainEventName, eventStatus, ...args) => {
181 | const { scale } = this.gesture;
182 | if (eventStatus === 'move' && typeof scale === 'number') {
183 | if (scale > 1) {
184 | this.triggerEvent('onPinchOut');
185 | }
186 | if (scale < 1) {
187 | this.triggerEvent('onPinchIn');
188 | }
189 | }
190 | this.triggerCombineEvent(mainEventName, eventStatus, ...args);
191 | }
192 | initPressTimer = () => {
193 | this.cleanPressTimer();
194 | this.pressTimer = setTimeout(() => {
195 | this.setGestureState({
196 | press: true,
197 | });
198 | this.triggerEvent('onPress');
199 | }, PRESS.time);
200 | }
201 | cleanPressTimer = () => {
202 | /* tslint:disable:no-unused-expression */
203 | this.pressTimer && clearTimeout(this.pressTimer);
204 | }
205 | setGestureState = (params) => {
206 | if (!this.gesture) {
207 | this.gesture = {} as any;
208 | }
209 |
210 | // cache the previous touches
211 | if (this.gesture.touches) {
212 | this.gesture.preTouches = this.gesture.touches;
213 | }
214 | this.gesture = {
215 | ...this.gesture,
216 | ...params,
217 | };
218 | }
219 | getGestureState = () => {
220 | if (!this.gesture) {
221 | return this.gesture;
222 | } else {
223 | // shallow copy
224 | return {
225 | ...this.gesture,
226 | };
227 | }
228 | }
229 | cleanGestureState = () => {
230 | delete this.gesture;
231 | }
232 | getTouches = (e) => {
233 | return Array.prototype.slice.call(e.touches).map(item => ({
234 | x: item.screenX,
235 | y: item.screenY,
236 | }));
237 | }
238 | triggerUserCb = (status, e) => {
239 | const cbName = getEventName('onTouch', status);
240 | if (cbName in this.props) {
241 | this.props[cbName](e);
242 | }
243 | }
244 | _handleTouchStart = (e) => {
245 | this.triggerUserCb('start', e);
246 | this.event = e;
247 | if (e.touches.length > 1) {
248 | e.preventDefault();
249 | }
250 | this.initGestureStatus(e);
251 | this.initPressTimer();
252 | this.checkIfMultiTouchStart();
253 | }
254 | initGestureStatus = (e) => {
255 | this.cleanGestureState();
256 | // store the gesture start state
257 | const startTouches = this.getTouches(e);
258 | const startTime = now();
259 | const startMutliFingerStatus = calcMutliFingerStatus(startTouches);
260 | this.setGestureState({
261 | startTime,
262 | startTouches,
263 | startMutliFingerStatus,
264 | /* copy for next time touch move cala convenient*/
265 | time: startTime,
266 | touches: startTouches,
267 | mutliFingerStatus: startMutliFingerStatus,
268 | srcEvent: this.event,
269 | });
270 | }
271 |
272 | checkIfMultiTouchStart = () => {
273 | const { enablePinch, enableRotate } = this.props;
274 | const { touches } = this.gesture;
275 | if (touches.length > 1 && (enablePinch || enableRotate)) {
276 | if (enablePinch) {
277 | const startMutliFingerStatus = calcMutliFingerStatus(touches);
278 | this.setGestureState({
279 | startMutliFingerStatus,
280 |
281 | /* init pinch status */
282 | pinch: true,
283 | scale: 1,
284 | });
285 | this.triggerCombineEvent('onPinch', 'start');
286 | }
287 | if (enableRotate) {
288 | this.setGestureState({
289 | /* init rotate status */
290 | rotate: true,
291 | rotation: 0,
292 | });
293 | this.triggerCombineEvent('onRotate', 'start');
294 | }
295 | }
296 | }
297 | _handleTouchMove = (e) => {
298 | this.triggerUserCb('move', e);
299 | this.event = e;
300 | if (!this.gesture) {
301 | // sometimes weird happen: touchstart -> touchmove..touchmove.. --> touchend --> touchmove --> touchend
302 | // so we need to skip the unnormal event cycle after touchend
303 | return;
304 | }
305 |
306 | // not a long press
307 | this.cleanPressTimer();
308 |
309 | this.updateGestureStatus(e);
310 | this.checkIfSingleTouchMove();
311 | this.checkIfMultiTouchMove();
312 | }
313 | checkIfMultiTouchMove = () => {
314 | const { pinch, rotate, touches, startMutliFingerStatus, mutliFingerStatus } = this.gesture as any;
315 | if (!pinch && !rotate) {
316 | return;
317 | }
318 | if (touches.length < 2) {
319 | this.setGestureState({
320 | pinch: false,
321 | rotate: false,
322 | });
323 | // Todo: 2 finger -> 1 finger, wait to test this situation
324 | pinch && this.triggerCombineEvent('onPinch', 'cancel');
325 | rotate && this.triggerCombineEvent('onRotate', 'cancel');
326 | return;
327 | }
328 |
329 | if (pinch) {
330 | const scale = mutliFingerStatus.z / startMutliFingerStatus.z;
331 | this.setGestureState({
332 | scale,
333 | });
334 | this.triggerPinchEvent('onPinch', 'move');
335 | }
336 | if (rotate) {
337 | const rotation = calcRotation(startMutliFingerStatus, mutliFingerStatus);
338 | this.setGestureState({
339 | rotation,
340 | });
341 | this.triggerCombineEvent('onRotate', 'move');
342 | }
343 | }
344 | allowGesture = () => {
345 | return shouldTriggerDirection(this.gesture.direction, this.directionSetting);
346 | }
347 | checkIfSingleTouchMove = () => {
348 | const { pan, touches, moveStatus, preTouches, availablePan = true } = this.gesture;
349 | if (touches.length > 1) {
350 | this.setGestureState({
351 | pan: false,
352 | });
353 | // Todo: 1 finger -> 2 finger, wait to test this situation
354 | pan && this.triggerCombineEvent('onPan', 'cancel');
355 | return;
356 | }
357 |
358 | // add avilablePan condition to fix the case in scrolling, which will cause unavailable pan move.
359 | if (moveStatus && availablePan) {
360 | const direction = getMovingDirection(preTouches[0], touches[0]);
361 | this.setGestureState({direction});
362 |
363 | const eventName = getDirectionEventName(direction);
364 | if (!this.allowGesture()) {
365 | // if the first move is unavailable, then judge all of remaining touch movings are also invalid.
366 | if (!pan) {
367 | this.setGestureState({availablePan: false});
368 | }
369 | return;
370 | }
371 | if (!pan) {
372 | this.triggerCombineEvent('onPan', 'start');
373 | this.setGestureState({
374 | pan: true,
375 | availablePan: true,
376 | });
377 | } else {
378 | this.triggerCombineEvent('onPan', eventName);
379 | this.triggerSubEvent('onPan', 'move');
380 | }
381 | }
382 | }
383 | checkIfMultiTouchEnd = (status) => {
384 | const { pinch, rotate } = this.gesture;
385 | if (pinch) {
386 | this.triggerCombineEvent('onPinch', status);
387 | }
388 | if (rotate) {
389 | this.triggerCombineEvent('onRotate', status);
390 | }
391 | }
392 | updateGestureStatus = (e) => {
393 | const time = now();
394 | this.setGestureState({
395 | time,
396 | });
397 | if (!e.touches || !e.touches.length) {
398 | return;
399 | }
400 | const { startTime, startTouches, pinch, rotate } = this.gesture;
401 | const touches = this.getTouches(e);
402 | const moveStatus = calcMoveStatus(startTouches, touches, time - startTime);
403 | let mutliFingerStatus;
404 | if (pinch || rotate) {
405 | mutliFingerStatus = calcMutliFingerStatus(touches);
406 | }
407 |
408 | this.setGestureState({
409 | /* update status snapshot */
410 | touches,
411 | mutliFingerStatus,
412 | /* update duration status */
413 | moveStatus,
414 |
415 | });
416 | }
417 | _handleTouchEnd = (e) => {
418 | this.triggerUserCb('end', e);
419 | this.event = e;
420 | if (!this.gesture) {
421 | return;
422 | }
423 | this.cleanPressTimer();
424 | this.updateGestureStatus(e);
425 | this.doSingleTouchEnd('end');
426 | this.checkIfMultiTouchEnd('end');
427 | }
428 |
429 | _handleTouchCancel = (e) => {
430 | this.triggerUserCb('cancel', e);
431 | this.event = e;
432 | // Todo: wait to test cancel case
433 | if (!this.gesture) {
434 | return;
435 | }
436 | this.cleanPressTimer();
437 | this.updateGestureStatus(e);
438 | this.doSingleTouchEnd('cancel');
439 | this.checkIfMultiTouchEnd('cancel');
440 | }
441 | triggerAllowEvent = (type, status) => {
442 | if (this.allowGesture()) {
443 | this.triggerCombineEvent(type, status);
444 | } else {
445 | this.triggerSubEvent(type, status);
446 | }
447 | }
448 | doSingleTouchEnd = (status) => {
449 | const { moveStatus, pinch, rotate, press, pan, direction } = this.gesture;
450 |
451 | if (pinch || rotate) {
452 | return;
453 | }
454 | if (moveStatus) {
455 | const { z, velocity } = moveStatus;
456 | const swipe = shouldTriggerSwipe(z, velocity);
457 | this.setGestureState({
458 | swipe,
459 | });
460 | if (pan) {
461 | // pan need end, it's a process
462 | // sometimes, start with pan left, but end with pan right....
463 | this.triggerAllowEvent('onPan', status);
464 | }
465 | if (swipe) {
466 | const directionEvName = getDirectionEventName(direction);
467 | // swipe just need a direction, it's a endpoint
468 | this.triggerAllowEvent('onSwipe', directionEvName);
469 | return;
470 | }
471 | }
472 |
473 | if (press) {
474 | this.triggerEvent('onPressUp');
475 | return;
476 | }
477 | this.triggerEvent('onTap');
478 | }
479 |
480 | componentWillUnmount() {
481 | this.cleanPressTimer();
482 | }
483 | getTouchAction = () => {
484 | const { enablePinch, enableRotate } = this.props;
485 | const { directionSetting } = this;
486 | if (enablePinch || enableRotate || directionSetting === DIRECTION_ALL) {
487 | return 'pan-x pan-y';
488 | }
489 | if (directionSetting === DIRECTION_VERTICAL) {
490 | return 'pan-x';
491 | }
492 | if (directionSetting === DIRECTION_HORIZONTAL) {
493 | return 'pan-y';
494 | }
495 | return 'auto';
496 | }
497 | render() {
498 | const { children } = this.props;
499 |
500 | const child = React.Children.only(children);
501 | const touchAction = this.getTouchAction();
502 |
503 | const events = {
504 | onTouchStart: this._handleTouchStart,
505 | onTouchMove: this._handleTouchMove,
506 | onTouchCancel: this._handleTouchCancel,
507 | onTouchEnd: this._handleTouchEnd,
508 | };
509 |
510 | return React.cloneElement(child, {
511 | ...events,
512 | style: {
513 | touchAction,
514 | ...(child.props.style || {}),
515 | },
516 | });
517 | }
518 | }
519 |
--------------------------------------------------------------------------------
/src/util.tsx:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-bitwise */
2 | import { SWIPE, DIRECTION_NONE, DIRECTION_LEFT, DIRECTION_RIGHT, DIRECTION_UP, DIRECTION_DOWN } from './config';
3 |
4 | function _calcTriangleDistance(x, y) {
5 | return Math.sqrt(x * x + y * y);
6 | }
7 |
8 | function _calcAngle (x, y) {
9 | const radian = Math.atan2(y, x);
10 | return 180 / (Math.PI / radian);
11 | }
12 |
13 | export function now() {
14 | return Date.now();
15 | }
16 |
17 | export function calcMutliFingerStatus(touches) {
18 | if (touches.length < 2) {
19 | return;
20 | }
21 | const { x: x1, y: y1 } = touches[0];
22 | const { x: x2, y: y2 } = touches[1];
23 | const deltaX = x2 - x1;
24 | const deltaY = y2 - y1;
25 | return {
26 | x: deltaX,
27 | y: deltaY,
28 | z: _calcTriangleDistance(deltaX, deltaY),
29 | angle: _calcAngle(deltaX, deltaY),
30 | };
31 | }
32 |
33 | export function calcMoveStatus(startTouches, touches, time) {
34 | const { x: x1, y: y1 } = startTouches[0];
35 | const { x: x2, y: y2 } = touches[0];
36 | const deltaX = x2 - x1;
37 | const deltaY = y2 - y1;
38 | const deltaZ = _calcTriangleDistance(deltaX, deltaY);
39 | return {
40 | x: deltaX,
41 | y: deltaY,
42 | z: deltaZ,
43 | time,
44 | velocity: deltaZ / time,
45 | angle: _calcAngle(deltaX, deltaY),
46 | };
47 | }
48 | export function calcRotation(startMutliFingerStatus, mutliFingerStatus) {
49 | const { angle: startAngle } = startMutliFingerStatus;
50 | const { angle } = mutliFingerStatus;
51 |
52 | return angle - startAngle;
53 | }
54 |
55 | export function getEventName(prefix, status) {
56 | return prefix + status[0].toUpperCase() + status.slice(1);
57 | }
58 |
59 | export function shouldTriggerSwipe(delta, velocity) {
60 | return Math.abs(delta) >= SWIPE.threshold && Math.abs(velocity) > SWIPE.velocity;
61 | }
62 |
63 | export function shouldTriggerDirection(direction, directionSetting) {
64 | if (directionSetting & direction) {
65 | return true;
66 | }
67 | return false;
68 | }
69 |
70 | /**
71 | * @private
72 | * get the direction between two points
73 | * Note: will change next version
74 | * @param {Number} x
75 | * @param {Number} y
76 | * @return {Number} direction
77 | */
78 | export function getDirection(x, y) {
79 | if (x === y) {
80 | return DIRECTION_NONE;
81 | }
82 | if (Math.abs(x) >= Math.abs(y)) {
83 | return x < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
84 | }
85 | return y < 0 ? DIRECTION_UP : DIRECTION_DOWN;
86 | }
87 |
88 | /**
89 | * @private
90 | * get the direction between tow points when touch moving
91 | * Note: will change next version
92 | * @param {Object} point1 coordinate point, include x & y attributes
93 | * @param {Object} point2 coordinate point, include x & y attributes
94 | * @return {Number} direction
95 | */
96 | export function getMovingDirection(point1, point2) {
97 | const {x: x1, y: y1} = point1;
98 | const {x: x2, y: y2} = point2;
99 | const deltaX = x2 - x1;
100 | const deltaY = y2 - y1;
101 | if (deltaX === 0 && deltaY === 0) {
102 | return DIRECTION_NONE;
103 | }
104 | if (Math.abs(deltaX) >= Math.abs(deltaY)) {
105 | return deltaX < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
106 | }
107 | return deltaY < 0 ? DIRECTION_UP : DIRECTION_DOWN;
108 | }
109 |
110 | export function getDirectionEventName(direction) {
111 | let name;
112 | switch (direction) {
113 | case DIRECTION_NONE:
114 | break;
115 | case DIRECTION_LEFT:
116 | name = 'left';
117 | break;
118 | case DIRECTION_RIGHT:
119 | name = 'right';
120 | break;
121 | case DIRECTION_UP:
122 | name = 'up';
123 | break;
124 | case DIRECTION_DOWN:
125 | name = 'down';
126 | break;
127 | default:
128 | }
129 | return name;
130 | }
131 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strictNullChecks": true,
4 | "moduleResolution": "node",
5 | "allowSyntheticDefaultImports": true,
6 | "jsx": "react",
7 | "target": "es6"
8 | }
9 | }
--------------------------------------------------------------------------------