├── .gitignore
├── README.md
├── assets
├── bundle.js
├── bundle.js.map
├── index.css
└── index.html
├── client
├── app.js
└── app.js.map
├── index.html
├── package.json
├── server
├── server.js
└── start.js
├── src
├── Gestures.js
├── ImgTest.css
├── ImgTest.js
└── index.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | server/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HTML5手势检测,基于React
2 |
3 | ## 检测类型
4 | 1. tap
5 | 2. double tap
6 | 3. long tap
7 | 4. move
8 | 5. swipe
9 | 6. pinch(zoom)
10 | 7. rotate
11 |
12 | 
13 |
14 | ##[DEMO](http://eeandrew.github.io/demos/gestures/index.html)
15 |
16 |
17 |
--------------------------------------------------------------------------------
/assets/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | margin: 0;
7 | }
8 |
9 | ul {
10 | padding: 0;
11 | margin: 0;
12 | }
--------------------------------------------------------------------------------
/assets/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Test
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react_setup",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "author": "Jerry Mao",
7 | "license": "ISC",
8 | "scripts": {
9 | "start": "nodemon server/server.js",
10 | "test": "mocha test/.setup.js test/**/*-Test.js -R nyan"
11 | },
12 | "babel": {
13 | "presets": [
14 | "airbnb",
15 | "react",
16 | "es2015",
17 | "stage-1"
18 | ],
19 | "plugins": [
20 | "transform-runtime"
21 | ]
22 | },
23 | "dependencies": {
24 | "autoprefixer": "^6.3.6",
25 | "babel-cli": "^6.9.0",
26 | "babel-core": "^6.9.0",
27 | "babel-eslint": "^6.0.4",
28 | "babel-loader": "^6.2.4",
29 | "babel-plugin-transform-runtime": "^6.9.0",
30 | "babel-polyfill": "^6.9.0",
31 | "babel-preset-airbnb": "^2.0.0",
32 | "babel-preset-es2015": "^6.9.0",
33 | "babel-preset-react": "^6.5.0",
34 | "babel-preset-stage-1": "^6.5.0",
35 | "babel-register": "^6.9.0",
36 | "babel-runtime": "^6.9.0",
37 | "classnames": "^2.2.5",
38 | "express": "^4.13.4",
39 | "nodemon": "^1.9.2",
40 | "react": "^15.1.0",
41 | "react-dom": "^15.1.0",
42 | "webpack": "^1.13.0",
43 | "webpack-dev-middleware": "^1.6.1",
44 | "webpack-dev-server": "^1.14.1",
45 | "webpack-hot-middleware": "^2.10.0",
46 | "whatwg-fetch": "^1.0.0"
47 | },
48 | "devDependencies": {
49 | "chai": "^3.5.0",
50 | "cheerio": "^0.20.0",
51 | "css-loader": "^0.23.1",
52 | "enzyme": "^2.3.0",
53 | "extract-text-webpack-plugin": "^0.8.2",
54 | "jsdom": "^9.2.1",
55 | "less-loader": "^2.2.3",
56 | "mocha": "^2.5.3",
57 | "react-addons-test-utils": "^15.1.0",
58 | "react-hot-loader": "^1.3.0",
59 | "style-loader": "^0.13.1"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const express = require('express');
4 | const app = express();
5 | const path = require('path');
6 | const request = require('request');
7 | const server = app.listen(3000, () => {console.log('listening on 3000....');});
8 |
9 |
10 | app.use(express.static(path.join(__dirname, './../')));
11 |
12 |
13 | const webpack = require('webpack');
14 | const WebpackDevServer = require('webpack-dev-server');
15 | const config = require('../webpack.config');
16 |
17 | app.get('/app.js', (req, res) => {
18 | if (process.env.PRODUCTION) {
19 | res.sendFile(__dirname + '/client/app.js');
20 | } else {
21 | res.redirect('//localhost:9090/client/app.js');
22 | }
23 | });
24 |
25 | // Serve aggregate stylesheet depending on environment
26 | app.get('/style.css', (req, res) => {
27 | if (process.env.PRODUCTION) {
28 | res.sendFile(__dirname + '/client/style.css');
29 | } else {
30 | res.redirect('//localhost:9090/client/style.css');
31 | }
32 | });
33 |
34 | // Serve index page
35 | app.get('*', (req, res) => {
36 | res.sendFile(path.join(__dirname, '../', 'index.html'))
37 | // res.sendFile(__dirname + '/index.html');
38 | });
39 |
40 | new WebpackDevServer(webpack(config), {
41 | publicPath: config.output.publicPath,
42 | hot: true,
43 | noInfo: true,
44 | historyApiFallback: true
45 | }).listen(9090, 'localhost', (err, result) => {
46 | if (err) {
47 | console.log(err);
48 | }
49 | console.log('webpack i think');
50 | });
--------------------------------------------------------------------------------
/server/start.js:
--------------------------------------------------------------------------------
1 | // Server-side entrypoint that registers Babel's require() hook
2 | const babelRegister = require('babel-register');
3 | babelRegister();
4 |
5 | require('./server');
--------------------------------------------------------------------------------
/src/Gestures.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | PropTypes,
3 | Component
4 | } from 'react';
5 |
6 | export default class Gestures extends Component {
7 | constructor(props) {
8 | super(props);
9 | this._onTouchStart = this._onTouchStart.bind(this);
10 | this._onTouchMove = this._onTouchMove.bind(this);
11 | this._onTouchCancel = this._onTouchCancel.bind(this);
12 | this._onTouchEnd = this._onTouchEnd.bind(this);
13 | this._emitEvent = this._emitEvent.bind(this);
14 | this.startX = this.startY = this.moveX = this.moveY = null;
15 | this.previousPinchScale = 1;
16 | this.longTapTimeout = null;
17 | }
18 | _emitEvent(eventType,e) {
19 | let eventHandler = this.props[eventType];
20 | if(!eventHandler)return;
21 | eventHandler(e);
22 | }
23 | _getTime() {
24 | return new Date().getTime();
25 | }
26 | _getDistance(xLen,yLen) {
27 | return Math.sqrt(xLen * xLen + yLen * yLen);
28 | }
29 | /**
30 | * 获取向量的旋转方向
31 | */
32 | _getRotateDirection(vector1,vector2) {
33 | return vector1.x * vector2.y - vector2.x * vector1.y;
34 | }
35 | _getRotateAngle(vector1,vector2) {
36 | let direction = this._getRotateDirection(vector1,vector2);
37 | direction = direction > 0 ? -1 : 1;
38 | let len1 = this._getDistance(vector1.x,vector1.y);
39 | let len2 = this._getDistance(vector2.x,vector2.y);
40 | let mr = len1 * len2;
41 | if(mr === 0) return 0;
42 | let dot = vector1.x * vector2.x + vector1.y * vector2.y;
43 | let r = dot / mr;
44 | if(r > 1) r = 1;
45 | if(r < -1) r = -1;
46 | return Math.acos(r) * direction * 180 / Math.PI;
47 | }
48 |
49 | _onTouchStart(e) {
50 | let point = e.touches ? e.touches[0] : e;
51 | this.startX = point.pageX;
52 | this.startY = point.pageY;
53 | window.clearTimeout(this.longTapTimeout);
54 | //两点接触
55 | if(e.touches.length > 1) {
56 | let point2 = e.touches[1];
57 | let xLen = Math.abs(point2.pageX - this.startX);
58 | let yLen = Math.abs(point2.pageY - this.startY);
59 | this.touchDistance = this._getDistance(xLen,yLen);
60 | this.touchVector = {
61 | x: point2.pageX - this.startX,
62 | y: point2.pageY - this.startY
63 | };
64 | }else {
65 | this.startTime = this._getTime();
66 | this.longTapTimeout = setTimeout(()=>{
67 | this._emitEvent('onLongPress');
68 | },800);
69 | if(this.previousTouchPoint) {
70 | if( Math.abs(this.startX -this.previousTouchPoint.startX) < 10 &&
71 | Math.abs(this.startY - this.previousTouchPoint.startY) < 10 &&
72 | Math.abs(this.startTime - this.previousTouchTime) < 300) {
73 | this._emitEvent('onDoubleTap');
74 | }
75 | }
76 | this.previousTouchTime = this.startTime;
77 | this.previousTouchPoint = {
78 | startX : this.startX,
79 | startY : this.startY
80 | };
81 | }
82 | }
83 | _onTouchMove(e) {
84 | let timestamp = this._getTime();
85 | if(e.touches.length > 1) {
86 | let xLen = Math.abs(e.touches[0].pageX - e.touches[1].pageX);
87 | let yLen = Math.abs(e.touches[1].pageY - e.touches[1].pageY);
88 | let touchDistance = this._getDistance(xLen,yLen);
89 | if(this.touchDistance) {
90 | let pinchScale = touchDistance / this.touchDistance;
91 | this._emitEvent('onPinch',{scale:pinchScale - this.previousPinchScale});
92 | this.previousPinchScale = pinchScale;
93 | }
94 | if(this.touchVector) {
95 | let vector = {
96 | x: e.touches[1].pageX - e.touches[0].pageX,
97 | y: e.touches[1].pageY - e.touches[0].pageY
98 | };
99 | let angle = this._getRotateAngle(vector,this.touchVector);
100 | this._emitEvent('onRotate',{
101 | angle
102 | });
103 | this.touchVector.x = vector.x;
104 | this.touchVector.y = vector.y;
105 | }
106 | }else {
107 | window.clearTimeout(this.longTapTimeout);
108 | let point = e.touches ? e.touches[0] :e;
109 | let deltaX = this.moveX === null ? 0 : point.pageX - this.moveX;
110 | let deltaY = this.moveY === null ? 0 : point.pageY - this.moveY;
111 | this._emitEvent('onMove',{
112 | deltaX,
113 | deltaY
114 | });
115 | this.moveX = point.pageX;
116 | this.moveY = point.pageY;
117 | }
118 | e.preventDefault();
119 | }
120 | _onTouchCancel(e) {
121 | this._onTouchEnd();
122 | }
123 | _onTouchEnd(e) {
124 | /**
125 | * 在X轴或Y轴发生过移动
126 | */
127 | window.clearTimeout(this.longTapTimeout);
128 | let timestamp = this._getTime();
129 | if(this.moveX !== null && Math.abs(this.moveX - this.startX) > 10 ||
130 | this.moveY !== null && Math.abs(this.moveY - this.startY) > 10) {
131 | if(timestamp - this.startTime < 500) {
132 | this._emitEvent('onSwipe');
133 | }
134 | }else if(timestamp - this.startTime <2000){
135 | if(timestamp - this.startTime < 500) {
136 | this._emitEvent('onTap');
137 | }
138 | if(timestamp - this.startTime > 500) {
139 | // this._emitEvent('onLongPress');
140 | }
141 | }
142 | this.startX = this.startY = this.moveX = this.moveY = null;
143 | this.previousPinchScale = 1;
144 | }
145 | render() {
146 | return React.cloneElement(React.Children.only(this.props.children), {
147 | onTouchStart: this._onTouchStart.bind(this),
148 | onTouchMove: this._onTouchMove.bind(this),
149 | onTouchCancel: this._onTouchCancel.bind(this),
150 | onTouchEnd: this._onTouchEnd.bind(this)
151 | });
152 | }
153 | }
154 |
155 | Gestures.propTypes = {
156 | onMove: PropTypes.func
157 | };
--------------------------------------------------------------------------------
/src/ImgTest.css:
--------------------------------------------------------------------------------
1 | .wrapper {
2 | height: 300px;
3 | background: #ccc;
4 | text-align: center;
5 | display: flex;
6 | display: -webkit-flex;
7 | align-items: center;
8 | -webkit-align-items:center;
9 | justify-content: center;
10 | -webkit-justify-content:center;
11 | overflow: hidden;
12 | }
13 |
14 | @-webkit-keyframes flash {
15 | from, 50%, to {
16 | opacity: 1;
17 | }
18 |
19 | 25%, 75% {
20 | opacity: 0;
21 | }
22 | }
23 |
24 | @keyframes flash {
25 | from, 50%, to {
26 | opacity: 1;
27 | }
28 |
29 | 25%, 75% {
30 | opacity: 0;
31 | }
32 | }
33 |
34 | .flash {
35 | -webkit-animation-name: flash;
36 | animation-name: flash;
37 | }
38 |
39 | .animated {
40 | -webkit-animation-duration: 1s;
41 | animation-duration: 1s;
42 | -webkit-animation-fill-mode: both;
43 | animation-fill-mode: both;
44 | }
45 |
46 | .lena {
47 | position: relative;
48 | width: 200px;
49 | height: 200px;
50 | }
51 |
52 | .mask {
53 | width: 200px;
54 | }
55 |
56 | * {
57 | -webkit-user-select: none;
58 | }
--------------------------------------------------------------------------------
/src/ImgTest.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Component,
3 | PropTypes
4 | } from 'react';
5 | import Gestures from './Gestures';
6 | import './ImgTest.css';
7 | import classNames from 'classnames';
8 |
9 | export default class ImgTest extends Component {
10 |
11 | constructor(props) {
12 | super(props);
13 | this.state = {
14 | pinch : 1,
15 | angle :0,
16 | left:0,
17 | top:0,
18 | animating:false
19 | };
20 | this.pinch = 1;
21 | this.left = 0;
22 | this.top = 0;
23 | this.angle = 0;
24 | this.doubleTapped = false;
25 | this.onPinch = this.onPinch.bind(this);
26 | this.onRotate = this.onRotate.bind(this);
27 | this.onMove = this.onMove.bind(this);
28 | this.onDoubleTap = this.onDoubleTap.bind(this);
29 | this.onLongPress = this.onLongPress.bind(this);
30 | }
31 |
32 | onPinch(event) {
33 | this.pinch += event.scale;
34 | this.setState({
35 | pinch: this.pinch
36 | });
37 | }
38 |
39 | onRotate(event) {
40 | this.angle += event.angle
41 | this.setState({
42 | angle:this.angle
43 | });
44 | }
45 |
46 | onMove(event) {
47 | this.left += event.deltaX;
48 | this.top += event.deltaY;
49 | this.setState({
50 | left: this.left,
51 | top: this.top
52 | });
53 | }
54 |
55 | onDoubleTap() {
56 | if(this.doubleTapped) {
57 | this.pinch = 1;
58 | this.setState({
59 | pinch: this.pinch
60 | });
61 | }else {
62 | this.pinch = 2.5;
63 | this.setState({
64 | pinch: this.pinch
65 | });
66 | }
67 | this.doubleTapped = !this.doubleTapped;
68 | }
69 |
70 | onLongPress() {
71 | alert('Long Press');
72 | this.setState({
73 | animating:true
74 | });
75 | setTimeout(()=>{
76 | this.setState({
77 | animating:false
78 | });
79 | },1000)
80 | }
81 |
82 | render() {
83 | let {
84 | pinch,
85 | angle,
86 | left,
87 | top,
88 | animating
89 | } = this.state;
90 | let imgStyle = {
91 | transform: `scale(${pinch}) rotateZ(${angle}deg)`,
92 | WebkitTransform: `scale(${pinch}) rotateZ(${angle}deg)`,
93 | left: `${left}px`,
94 | top: `${top}px`
95 | }
96 | let imgClasses = classNames('lena','flash',{animated:animating})
97 | return (
98 |
99 |
100 |
101 |

102 |
103 |
104 |
105 | {this.state.pinch}
106 | {this.state.angle}
107 |
108 |
109 | );
110 | }
111 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import ReactDom from 'react-dom';
2 | import React from 'react';
3 | import ImgTest from './ImgTest';
4 |
5 | const App = (props) => {
6 | return (
7 |
10 | );
11 | }
12 |
13 | ReactDom.render(,document.getElementById('app'));
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
3 |
4 |
5 |
6 | module.exports = {
7 | devtool: 'source-map',
8 | entry: [
9 | "webpack-dev-server/client?http://localhost:9090",
10 | "webpack/hot/only-dev-server",
11 | "./src/index"
12 | ],
13 |
14 | // This will not actually create a bundle.js file in ./client. It is used
15 | // by the dev server for dynamic hot loading.
16 | output: {
17 | path: __dirname + "/client/",
18 | filename: "app.js",
19 | publicPath: "http://localhost:9090/client/"
20 | },
21 | // entry:'./src/main.jsx',
22 | // output: {
23 | // path: './client',
24 | // filename: 'bundle.js'
25 | // },
26 | resolve: {
27 | extensions: ['', '.js', '.jsx']
28 | },
29 | module: {
30 | loaders: [{
31 | test: /\.jsx?$/,
32 | loaders: ['react-hot','babel-loader'],
33 | exclude: /node_modules/
34 | },{
35 | test:/\.css$/,
36 | loader:"style!css"
37 | }]
38 | },
39 | plugins: [
40 | new webpack.HotModuleReplacementPlugin(),
41 | // new ExtractTextPlugin('style.css', { allChunks: true })
42 | new webpack.NoErrorsPlugin()
43 | ],
44 | }
--------------------------------------------------------------------------------