├── .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 | ![image](https://github.com/eeandrew/ReadmeResource/blob/master/img/gestures/gestures.gif) 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 |
8 | 9 |
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 | } --------------------------------------------------------------------------------