├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── gesture.js ├── gesture.json ├── gesture.wxml └── gesture.wxs ├── example ├── app.js ├── app.json ├── app.wxss ├── package.json ├── pages │ ├── basic │ │ ├── anim.wxs │ │ ├── basic.js │ │ ├── basic.json │ │ ├── basic.wxml │ │ └── basic.wxss │ ├── index │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── photo │ │ ├── anim.wxs │ │ ├── avatar.jpg │ │ ├── photo.js │ │ ├── photo.json │ │ ├── photo.wxml │ │ └── photo.wxss │ ├── propagation │ │ ├── anim.wxs │ │ ├── propagation.js │ │ ├── propagation.json │ │ ├── propagation.wxml │ │ └── propagation.wxss │ └── requireFailure │ │ ├── requireFailure.js │ │ ├── requireFailure.json │ │ ├── requireFailure.wxml │ │ └── requireFailure.wxss ├── project.config.json ├── sitemap.json └── utils │ └── util.js ├── package.json └── src ├── gesture.js ├── gesture.json ├── gesture.wxml └── gesture.wxs /.eslintignore: -------------------------------------------------------------------------------- 1 | */.DS_Store 2 | .DS_Store 3 | */node_modules/* 4 | */miniprogram_npm/* 5 | package-lock.json 6 | node_modules/* -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'extends': [ 3 | 'airbnb-base', 4 | 'plugin:promise/recommended' 5 | ], 6 | 'parserOptions': { 7 | 'ecmaVersion': 9, 8 | 'ecmaFeatures': { 9 | 'jsx': false 10 | }, 11 | 'sourceType': 'module' 12 | }, 13 | 'env': { 14 | 'es6': true, 15 | 'node': true, 16 | 'jest': true 17 | }, 18 | 'plugins': [ 19 | 'import', 20 | 'node', 21 | 'promise' 22 | ], 23 | 'rules': { 24 | 'arrow-parens': 'off', 25 | 'comma-dangle': [ 26 | 'error', 27 | 'only-multiline' 28 | ], 29 | 'complexity': ['error', 10], 30 | 'func-names': 'off', 31 | 'global-require': 'off', 32 | 'handle-callback-err': [ 33 | 'error', 34 | '^(err|error)$' 35 | ], 36 | 'import/no-unresolved': [ 37 | 'error', 38 | { 39 | 'caseSensitive': true, 40 | 'commonjs': true, 41 | 'ignore': ['^[^.]'] 42 | } 43 | ], 44 | 'import/prefer-default-export': 'off', 45 | 'linebreak-style': 'off', 46 | 'no-catch-shadow': 'error', 47 | 'no-continue': 'off', 48 | 'no-div-regex': 'warn', 49 | 'no-else-return': 'off', 50 | 'no-param-reassign': 'off', 51 | 'no-plusplus': 'off', 52 | 'no-shadow': 'off', 53 | // enable console for this project 54 | 'no-console': 'off', 55 | 'no-multi-assign': 'off', 56 | 'no-underscore-dangle': 'off', 57 | 'node/no-deprecated-api': 'error', 58 | 'node/process-exit-as-throw': 'error', 59 | 'object-curly-spacing': [ 60 | 'error', 61 | 'never' 62 | ], 63 | 'operator-linebreak': [ 64 | 'error', 65 | 'after', 66 | { 67 | 'overrides': { 68 | ':': 'before', 69 | '?': 'before' 70 | } 71 | } 72 | ], 73 | 'prefer-arrow-callback': 'off', 74 | 'prefer-destructuring': 'off', 75 | 'prefer-template': 'off', 76 | 'quote-props': [ 77 | 1, 78 | 'as-needed', 79 | { 80 | 'unnecessary': true 81 | } 82 | ], 83 | 'semi': [ 84 | 'error', 85 | 'never' 86 | ] 87 | }, 88 | 'globals': { 89 | 'window': true, 90 | 'document': true, 91 | 'App': true, 92 | 'Page': true, 93 | 'Component': true, 94 | 'Behavior': true, 95 | 'wx': true, 96 | 'worker': true, 97 | 'getApp': true 98 | } 99 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */.DS_Store 2 | .DS_Store 3 | */node_modules/* 4 | */miniprogram_npm/* 5 | package-lock.json 6 | node_modules/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rabbit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wechat MiniProgram Gesture Library (微信小程序手势库) 2 | 3 | 这个手势库可以使微信小程序拥有识别手势的能力。本代码部分参考自 [AlloyFinger](https://github.com/AlloyTeam/AlloyFinger)。 4 | 5 | ## 使用方法 6 | 7 | 1. 在小程序的目录下依次执行 `npm init -y`, `npm i miniprogram-gesture` 8 | 2. 小程序开启 `使用 npm 模块` 开关 9 | 3. 在开发者工具上,点击 `工具`, `构建 npm` 10 | 4. 即可使用,使用方法参考 demo 11 | 12 | ## 注意事项 13 | 14 | 1. 本事件可以利用 `WXS` 在 `渲染层` 触发,如果回调函数,只是修改 `WebView` 的 `CSS` 属性、 `DOM` 属性,建议采取此种触发方式,性能较高;也可以在 Service 层 (逻辑层) 触发。 15 | 2. 下面说明所描述的时间可能不准确,因为计时是 setTimeout 实现的 16 | 17 | ## demo 18 | 19 | `/example` 文件夹下有 Demo ,敬请体验 20 | 21 | ## 事件解释 22 | 23 | - `touchStart` 触摸开始 (手指数不限) 24 | - `touchMove` 触摸移动 (手指数不限) 25 | - `touchEnd` 触摸结束 (手指数不限) 26 | - `touchCancel` 触摸取消 (手指数不限) 27 | 28 | - `multipointStart` 多指点按开始 29 | - `multipointEnd` 多指点按结束 30 | 31 | - `longTap` 长按 750ms 以上 32 | - `pinch` 双指捏合 33 | - `rotate` 双指旋转 34 | - `twoFingerPressMove` 双指移动 35 | - `pressMove` 单指点按移动 36 | - `swipe` 滑动 37 | - `tap` 点击 38 | - `doubleTap` 250 ms 内连续敲击两次 39 | - `singleTap` 敲击一次 40 | 41 | 42 | ## 使用方法 43 | 44 | 使用 `` 包裹要识别的组件,然后 `bind***` 即可 45 | 46 | ## 属性 47 | 48 | - propagation:`touchstart`, `touchmove`, `touchend` 是否事件向上冒泡到父节点,`Boolean` 类型,默认为 `true` 49 | 50 | - requireFailure:同时绑定 `singleTap`, `doubleTap` 的时候,当用户触发 `doubleTap`事件,是否会同时触发 `singleTap`,这个概念和 iOS 设备的 `require(toFail:)` 概念一致,`Boolean` 类型,默认为 `true` -------------------------------------------------------------------------------- /dist/gesture.js: -------------------------------------------------------------------------------- 1 | Component({ 2 | options: { 3 | addGlobalClass: true, 4 | multipleSlots: true, 5 | }, 6 | properties: { 7 | propagation: { 8 | type: Boolean, 9 | value: true, 10 | }, 11 | requireFailure: { 12 | type: Boolean, 13 | value: true, 14 | }, 15 | }, 16 | methods: {} 17 | }) 18 | -------------------------------------------------------------------------------- /dist/gesture.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "component": true, 4 | "usingComponents": {} 5 | } -------------------------------------------------------------------------------- /dist/gesture.wxml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | -------------------------------------------------------------------------------- /dist/gesture.wxs: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable */ 3 | // @ts-ignore 4 | function getLen(v) { 5 | return Math.sqrt(v.x * v.x + v.y * v.y) 6 | } 7 | function dot(v1, v2) { 8 | return v1.x * v2.x + v1.y * v2.y 9 | } 10 | function getAngle(v1, v2) { 11 | var mr = getLen(v1) * getLen(v2) 12 | if (mr === 0) return 0 13 | var r = dot(v1, v2) / mr 14 | if (r > 1) r = 1 15 | return Math.acos(r) 16 | } 17 | function cross(v1, v2) { 18 | return v1.x * v2.y - v2.x * v1.y 19 | } 20 | function getRotateAngle(v1, v2) { 21 | var angle = getAngle(v1, v2) 22 | if (cross(v1, v2) > 0) { 23 | angle *= -1 24 | } 25 | return angle * 180 / Math.PI 26 | } 27 | function _swipeDirection(x1, x2, y1, y2) { 28 | if (Math.abs(x1 - x2) >= Math.abs(y1 - y2)) { 29 | return x1 - x2 > 0 ? 'Left' : 'Right' 30 | } else { 31 | return y1 - y2 > 0 ? 'Up' : 'Down' 32 | } 33 | } 34 | // 实现setTimeout功能 35 | var setTimeout = function(callback, interval, instance) { 36 | var now = Date.now 37 | var stime = now() 38 | var loop = function() { 39 | if (now() - stime >= interval) { 40 | callback() 41 | } else { 42 | instance.requestAnimationFrame(loop) 43 | } 44 | } 45 | instance.requestAnimationFrame(loop) 46 | } 47 | var start = function(event, ownerInstance) { 48 | var instance = event.instance; 49 | var State = instance.getState() 50 | if(!State._init) { 51 | State.preV = {x: null, y: null} 52 | State.pinchStartLen = null 53 | State.zoom = 1 54 | State.isDoubleTap = false 55 | State.delta = null 56 | State.last = null 57 | State.now = null 58 | State.x1 = State.x2 = State.y1 = State.y2 = null 59 | State.preTapPosition = {x: null, y: null} 60 | // 控制定时器 61 | State._cancelLongTap = function() { 62 | State.longTapTimeout = false 63 | } 64 | State._cancelSingleTap = function() { 65 | State.singleTapTimeout = false 66 | } 67 | State._tapTimeout = function() { 68 | State.tapTimeout = false 69 | } 70 | State._swipeTimeout = function() { 71 | State.swipeTimeout = false 72 | } 73 | State._init = true // 表示已经初始化完成 74 | } 75 | State.tapTimeout = true 76 | State.singleTapTimeout = true 77 | State.longTapTimeout = true 78 | State.swipeTimeout = true 79 | State.now = Date.now() 80 | State.x1 = event.touches[0].pageX 81 | State.y1 = event.touches[0].pageY 82 | State.delta = State.now - (State.last || State.now) 83 | // 触发 touchStart 事件 84 | ownerInstance.triggerEvent('touchStart', event) 85 | if (State.preTapPosition.x !== null) { 86 | State.isDoubleTap = (State.delta > 0 && 87 | State.delta <= 250 && 88 | Math.abs(State.preTapPosition.x - State.x1) < 30 && 89 | Math.abs(State.preTapPosition.y - State.y1) < 30) 90 | if (State.isDoubleTap) { 91 | State._cancelSingleTap() 92 | } 93 | } 94 | State.preTapPosition.x = State.x1 95 | State.preTapPosition.y = State.y1 96 | State.last = State.now 97 | var preV = State.preV 98 | var len = event.touches.length 99 | if (len > 1) { 100 | State._cancelLongTap() 101 | State._cancelSingleTap() 102 | var v = {x: event.touches[1].pageX - State.x1, y: event.touches[1].pageY - State.y1} 103 | preV.x = v.x 104 | preV.y = v.y 105 | State.pinchStartLen = getLen(preV) 106 | // 触发 multipointStart 多指点按 事件 107 | ownerInstance.triggerEvent('multipointStart', event) 108 | } 109 | State._preventTap = false 110 | setTimeout(function () { 111 | // 触发 longTap(长按) 事件 112 | if(State.longTapTimeout) { 113 | ownerInstance.triggerEvent('longTap', event) 114 | State._preventTap = true 115 | State.longTapTimeout = true 116 | } 117 | }, 750, instance) 118 | 119 | if (!instance.getDataset()['propagation']) return false 120 | } 121 | var move = function(event, ownerInstance) { 122 | var instance = event.instance; 123 | var State = instance.getState() 124 | var preV = State.preV 125 | var len = event.touches.length 126 | var currentX = event.touches[0].pageX 127 | var currentY = event.touches[0].pageY 128 | State.isDoubleTap = false 129 | if (len > 1) { 130 | var sCurrentX = event.touches[1].pageX 131 | var sCurrentY = event.touches[1].pageY 132 | var v = {x: event.touches[1].pageX - currentX, y: event.touches[1].pageY - currentY} 133 | if (preV.x !== null) { 134 | if (State.pinchStartLen > 0) { 135 | event.zoom = getLen(v) / State.pinchStartLen 136 | // 触发 pinch 事件 137 | ownerInstance.triggerEvent('pinch', event) 138 | } 139 | event.angle = getRotateAngle(v, preV) 140 | // 触发 rotate 事件 141 | ownerInstance.triggerEvent('rotate', event) 142 | } 143 | preV.x = v.x 144 | preV.y = v.y 145 | if (State.x2 !== null && State.sx2 !== null) { 146 | event.deltaX = (currentX - State.x2 + sCurrentX - State.sx2) / 2 147 | event.deltaY = (currentY - State.y2 + sCurrentY - State.sy2) / 2 148 | } else { 149 | event.deltaX = 0 150 | event.deltaY = 0 151 | } 152 | // 触发 twoFingerPressMove 事件 153 | ownerInstance.triggerEvent('twoFingerPressMove', event) 154 | State.sx2 = sCurrentX 155 | State.sy2 = sCurrentY 156 | } else { 157 | if (State.x2 !== null) { 158 | event.deltaX = currentX - State.x2 159 | event.deltaY = currentY - State.y2 160 | // move事件中添加对当前触摸点到初始触摸点的判断, 161 | // 如果曾经大于过某个距离(比如10),就认为是移动到某个地方又移回来,应该不再触发tap事件才对。 162 | var movedX = Math.abs(State.x1 - State.x2) 163 | var movedY = Math.abs(State.y1 - State.y2) 164 | if (movedX > 10 || movedY > 10) { 165 | State._preventTap = true 166 | } 167 | } else { 168 | event.deltaX = 0 169 | event.deltaY = 0 170 | } 171 | // 触发 pressMove 单指点按移动 事件 172 | ownerInstance.triggerEvent('pressMove', event) 173 | } 174 | // 触发 touchMove 移动事件 175 | ownerInstance.triggerEvent('touchMove', event) 176 | State._cancelLongTap() 177 | State.x2 = currentX 178 | State.y2 = currentY 179 | // if (len > 1) { 180 | // // event.preventDefault() 181 | // } 182 | if (!instance.getDataset()['propagation']) return false 183 | } 184 | var end = function(event, ownerInstance) { 185 | var instance = event.instance; 186 | var State = instance.getState() 187 | State._cancelLongTap() 188 | if (event.touches.length < 2) { 189 | // 触发 multipointEnd 多指点按结束 事件 190 | ownerInstance.triggerEvent('multipointEnd', event) 191 | State.sx2 = State.sy2 = null 192 | } 193 | // swipe 194 | if ((State.x2 && Math.abs(State.x1 - State.x2) > 30) || 195 | (State.y2 && Math.abs(State.y1 - State.y2) > 30)) { 196 | event.direction = _swipeDirection(State.x1, State.x2, State.y1, State.y2) 197 | setTimeout(function () { 198 | if(State.swipeTimeout) { 199 | // 触发 swipe 滑动 上下左右 事件 200 | ownerInstance.triggerEvent('swipe', event) 201 | State.swipeTimeout = true 202 | } 203 | }, 0, instance) 204 | } else { 205 | setTimeout(function () { 206 | if(State.tapTimeout) { 207 | if (!State._preventTap) { 208 | // 触发 tap 事件 209 | ownerInstance.triggerEvent('tap', event) 210 | } 211 | // trigger double tap immediately 212 | if (State.isDoubleTap) { 213 | // 触发 doubleTap 事件 214 | ownerInstance.triggerEvent('doubleTap', event) 215 | State.isDoubleTap = false 216 | } 217 | State.tapTimeout = true 218 | } 219 | }, 0, instance) 220 | if (!State.isDoubleTap) { 221 | if (instance.getDataset()['requirefailure']) { // requireFailure 222 | setTimeout(function () { 223 | if(State.singleTapTimeout) { 224 | // 触发 singleTap 事件 225 | ownerInstance.triggerEvent('singleTap', event) 226 | State.singleTapTimeout = true 227 | } 228 | }, 250, instance) 229 | } else { 230 | ownerInstance.triggerEvent('singleTap', event) 231 | State.singleTapTimeout = true 232 | } 233 | } 234 | } 235 | // 触发 touchEnd 事件 236 | ownerInstance.triggerEvent('touchEnd', event) 237 | State.preV.x = 0 238 | State.preV.y = 0 239 | State.zoom = 1 240 | State.pinchStartLen = null 241 | State.x1 = State.x2 = null 242 | State.y1 = State.y2 = null 243 | 244 | if (!instance.getDataset()['propagation']) return false 245 | } 246 | var cancel = function(event, ownerInstance) { 247 | var instance = event.instance; 248 | var State = instance.getState() 249 | State._cancelLongTap() 250 | State._cancelSingleTap() 251 | State._tapTimeout() 252 | State._swipeTimeout() 253 | // 触发 touchCancel 事件 254 | ownerInstance.triggerEvent('touchCancel', event) 255 | 256 | if (!instance.getDataset()['propagation']) return false 257 | } 258 | module.exports = { 259 | start: start, 260 | move: move, 261 | end: end, 262 | cancel: cancel 263 | } 264 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | // app.js 2 | App({ 3 | onLaunch() { 4 | // 展示本地存储能力 5 | const logs = wx.getStorageSync('logs') || [] 6 | logs.unshift(Date.now()) 7 | wx.setStorageSync('logs', logs) 8 | 9 | // 登录 10 | wx.login({ 11 | success: () => { 12 | // 发送 res.code 到后台换取 openId, sessionKey, unionId 13 | } 14 | }) 15 | // 获取用户信息 16 | wx.getSetting({ 17 | success: res => { 18 | if (res.authSetting['scope.userInfo']) { 19 | // 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框 20 | wx.getUserInfo({ 21 | success: res => { 22 | // 可以将 res 发送给后台解码出 unionId 23 | this.globalData.userInfo = res.userInfo 24 | 25 | // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 26 | // 所以此处加入 callback 以防止这种情况 27 | if (this.userInfoReadyCallback) { 28 | this.userInfoReadyCallback(res) 29 | } 30 | } 31 | }) 32 | } 33 | } 34 | }) 35 | }, 36 | globalData: { 37 | userInfo: null 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/index/index", 4 | "pages/basic/basic", 5 | "pages/propagation/propagation", 6 | "pages/requireFailure/requireFailure", 7 | "pages/photo/photo" 8 | ], 9 | "window": { 10 | "backgroundTextStyle": "light", 11 | "navigationBarBackgroundColor": "#fff", 12 | "navigationBarTitleText": "WeChat", 13 | "navigationBarTextStyle": "black" 14 | }, 15 | "style": "v2", 16 | "sitemapLocation": "sitemap.json" 17 | } -------------------------------------------------------------------------------- /example/app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | .container { 3 | height: 100%; 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: space-between; 8 | padding: 200rpx 0; 9 | box-sizing: border-box; 10 | } 11 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "app.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "author": "", 9 | "license": "ISC", 10 | "dependencies": { 11 | "miniprogram-gesture": "^1.0.3" 12 | }, 13 | "devDependencies": {}, 14 | "keywords": [], 15 | "description": "" 16 | } 17 | -------------------------------------------------------------------------------- /example/pages/basic/anim.wxs: -------------------------------------------------------------------------------- 1 | var rotateZ = 0 2 | var initScale = 1, 3 | scale = 1 4 | var translateX = 0, 5 | translateY = 0 6 | 7 | function setStyle(ins) { 8 | ins.selectComponent('#test').setStyle({ 9 | left: translateX + 'px', 10 | top: translateY + 'px', 11 | transform: 'rotateZ(' + rotateZ + 'deg) scale(' + scale + ')' 12 | }) 13 | } 14 | 15 | function rotate(e, ins) { 16 | var angle = e.detail.angle 17 | rotateZ += angle 18 | setStyle(ins) 19 | } 20 | 21 | function multitouchstart(e, ins) { 22 | initScale = scale 23 | } 24 | 25 | function pinch(e, ins) { 26 | scale = initScale * e.detail.zoom 27 | setStyle(ins) 28 | } 29 | 30 | function pressmove(e, ins) { 31 | translateX += e.detail.deltaX 32 | translateY += e.detail.deltaY 33 | 34 | setStyle(ins) 35 | } 36 | 37 | module.exports = { 38 | rotate: rotate, 39 | multitouchstart: multitouchstart, 40 | pinch: pinch, 41 | pressmove: pressmove 42 | } -------------------------------------------------------------------------------- /example/pages/basic/basic.js: -------------------------------------------------------------------------------- 1 | Page({ 2 | data: {} 3 | }) 4 | -------------------------------------------------------------------------------- /example/pages/basic/basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "gesture": "/miniprogram_npm/miniprogram-gesture/gesture" 4 | }, 5 | "disableScroll": true 6 | } -------------------------------------------------------------------------------- /example/pages/basic/basic.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/pages/basic/basic.wxss: -------------------------------------------------------------------------------- 1 | .test { 2 | width: 200px; 3 | height: 200px; 4 | background-color: dodgerblue; 5 | position: absolute; 6 | } 7 | -------------------------------------------------------------------------------- /example/pages/index/index.js: -------------------------------------------------------------------------------- 1 | Page({ 2 | data: {}, 3 | basic() { 4 | wx.navigateTo({ 5 | url: '/pages/basic/basic', 6 | }) 7 | }, 8 | propagation() { 9 | wx.navigateTo({ 10 | url: '/pages/propagation/propagation', 11 | }) 12 | }, 13 | requireFailure() { 14 | wx.navigateTo({ 15 | url: '/pages/requireFailure/requireFailure', 16 | }) 17 | }, 18 | photo() { 19 | wx.navigateTo({ 20 | url: '/pages/photo/photo', 21 | }) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /example/pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {}, 3 | "disableScroll": true 4 | } -------------------------------------------------------------------------------- /example/pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /example/pages/index/index.wxss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/miniprogram-gesture/02738f584db7c21337edd3ea171109f26f45e038/example/pages/index/index.wxss -------------------------------------------------------------------------------- /example/pages/photo/anim.wxs: -------------------------------------------------------------------------------- 1 | var rotateZ = 0 2 | var initScale = 1, 3 | scale = 1 4 | var translateX = 0, 5 | translateY = 0 6 | 7 | function setStyle(ins) { 8 | ins.selectComponent('#avatar').setStyle({ 9 | transform: 'rotateZ(' + rotateZ + 'deg) scale(' + scale + ') translateX(' + translateX + 'px) translateY(' + translateY + 'px)' 10 | }) 11 | } 12 | 13 | function multitouchstart(e, ins) { 14 | initScale = scale 15 | } 16 | 17 | function pinch(e, ins) { 18 | scale = initScale * e.detail.zoom 19 | setStyle(ins) 20 | } 21 | 22 | function pressmove2(e, ins) { 23 | console.log('press move2') 24 | translateX += e.detail.deltaX 25 | translateY += e.detail.deltaY 26 | 27 | setStyle(ins) 28 | } 29 | 30 | function pressmove(e, ins) { 31 | console.log('press move') 32 | translateX += e.detail.deltaX 33 | translateY += e.detail.deltaY 34 | 35 | setStyle(ins) 36 | } 37 | 38 | function touchstart(e, ins) { 39 | ins.selectComponent('#avatar').removeClass('avatar-animation') 40 | } 41 | 42 | function touchend(e, ins) { 43 | if (e.detail.touches.length > 0) return 44 | ins.selectComponent('#avatar').addClass('avatar-animation') 45 | if (scale < 0.7 || scale > 3.5) { 46 | ins.callMethod('vibrate') 47 | } 48 | if (scale < 0.7) { 49 | scale = 0.7 50 | } 51 | if (scale > 3.5) { 52 | scale = 3.5 53 | } 54 | translateY = translateX = 0 55 | setStyle(ins) 56 | } 57 | 58 | module.exports = { 59 | multitouchstart: multitouchstart, 60 | pinch: pinch, 61 | pressmove: pressmove, 62 | pressmove2: pressmove2, 63 | touchstart: touchstart, 64 | touchend: touchend 65 | } -------------------------------------------------------------------------------- /example/pages/photo/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wechat-miniprogram/miniprogram-gesture/02738f584db7c21337edd3ea171109f26f45e038/example/pages/photo/avatar.jpg -------------------------------------------------------------------------------- /example/pages/photo/photo.js: -------------------------------------------------------------------------------- 1 | Page({ 2 | data: {}, 3 | vibrate() { 4 | wx.vibrateShort() 5 | } 6 | }) 7 | -------------------------------------------------------------------------------- /example/pages/photo/photo.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": { 3 | "gesture": "/miniprogram_npm/miniprogram-gesture/gesture" 4 | }, 5 | "disableScroll": true 6 | } -------------------------------------------------------------------------------- /example/pages/photo/photo.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /example/pages/photo/photo.wxss: -------------------------------------------------------------------------------- 1 | .avatar { 2 | position: absolute; 3 | height: 100vw; 4 | width: 100vw; 5 | top: calc((100vh - 100vw)/2); 6 | 7 | will-change: transform; 8 | } 9 | 10 | .root { 11 | background-color: black; 12 | width: 100vw; 13 | height: 100vh; 14 | } 15 | 16 | .avatar-animation { 17 | transition: all .2s cubic-bezier(.33, .63, .65, .99); 18 | } -------------------------------------------------------------------------------- /example/pages/propagation/anim.wxs: -------------------------------------------------------------------------------- 1 | var wrapperX = 0, 2 | wrapperY = 0 3 | 4 | function pressmovewrapper(e, ins) { 5 | wrapperX += e.detail.deltaX 6 | wrapperY += e.detail.deltaY 7 | 8 | ins.selectComponent('#wrapper').setStyle({ 9 | left: wrapperX + 'px', 10 | top: wrapperY + 'px' 11 | }) 12 | } 13 | 14 | var innerX = 0, 15 | innerY = 0 16 | 17 | function pressmoveinner(e, ins) { 18 | innerX += e.detail.deltaX 19 | innerY += e.detail.deltaY 20 | 21 | ins.selectComponent('#inner').setStyle({ 22 | left: innerX + 'px', 23 | top: innerY + 'px' 24 | }) 25 | } 26 | 27 | module.exports = { 28 | pressmovewrapper: pressmovewrapper, 29 | pressmoveinner: pressmoveinner 30 | } -------------------------------------------------------------------------------- /example/pages/propagation/propagation.js: -------------------------------------------------------------------------------- 1 | Component({ 2 | properties: {}, 3 | data: { 4 | propagation: false 5 | }, 6 | methods: { 7 | handlePropagation() { 8 | const { 9 | propagation 10 | } = this.data 11 | this.setData({ 12 | propagation: !propagation 13 | }) 14 | } 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /example/pages/propagation/propagation.json: -------------------------------------------------------------------------------- 1 | { 2 | "disableScroll": true, 3 | "usingComponents": { 4 | "gesture": "/miniprogram_npm/miniprogram-gesture/gesture" 5 | } 6 | } -------------------------------------------------------------------------------- /example/pages/propagation/propagation.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /example/pages/propagation/propagation.wxss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 200px; 3 | height: 200px; 4 | background-color: dodgerblue; 5 | position: fixed; 6 | } 7 | 8 | .inner { 9 | width: 100px; 10 | height: 100px; 11 | background-color: red; 12 | position: fixed; 13 | } -------------------------------------------------------------------------------- /example/pages/requireFailure/requireFailure.js: -------------------------------------------------------------------------------- 1 | Component({ 2 | data: { 3 | log: [], 4 | requireFailure: true 5 | }, 6 | methods: { 7 | singleTap(e) { 8 | console.warn('single tap', e) 9 | 10 | const { 11 | log 12 | } = this.data 13 | log.push('single tap triggered') 14 | this.setData({ 15 | log 16 | }) 17 | }, 18 | doubleTap(e) { 19 | console.warn('double tap', e) 20 | 21 | const { 22 | log 23 | } = this.data 24 | log.push('double tap triggered') 25 | this.setData({ 26 | log 27 | }) 28 | }, 29 | handletap() { 30 | const { 31 | requireFailure 32 | } = this.data 33 | this.setData({ 34 | requireFailure: !requireFailure 35 | }) 36 | } 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /example/pages/requireFailure/requireFailure.json: -------------------------------------------------------------------------------- 1 | { 2 | "component": true, 3 | "usingComponents": { 4 | "gesture": "/miniprogram_npm/miniprogram-gesture/gesture" 5 | } 6 | } -------------------------------------------------------------------------------- /example/pages/requireFailure/requireFailure.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Single / Double Tap 5 | 6 | 7 | 8 | {{item}} 9 | 10 | -------------------------------------------------------------------------------- /example/pages/requireFailure/requireFailure.wxss: -------------------------------------------------------------------------------- 1 | .test { 2 | width: 200px; 3 | height: 200px; 4 | background-color: dodgerblue; 5 | color: white; 6 | margin: 20px; 7 | } 8 | 9 | .log { 10 | margin: 0 20px; 11 | } -------------------------------------------------------------------------------- /example/project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件", 3 | "packOptions": { 4 | "ignore": [] 5 | }, 6 | "setting": { 7 | "urlCheck": true, 8 | "es6": true, 9 | "enhance": false, 10 | "postcss": true, 11 | "preloadBackgroundData": false, 12 | "minified": true, 13 | "newFeature": false, 14 | "coverView": true, 15 | "nodeModules": true, 16 | "autoAudits": false, 17 | "showShadowRootInWxmlPanel": true, 18 | "scopeDataCheck": false, 19 | "uglifyFileName": false, 20 | "checkInvalidKey": true, 21 | "checkSiteMap": true, 22 | "uploadWithSourceMap": true, 23 | "compileHotReLoad": false, 24 | "babelSetting": { 25 | "ignore": [], 26 | "disablePlugins": [], 27 | "outputPath": "" 28 | }, 29 | "useIsolateContext": true, 30 | "useCompilerModule": false, 31 | "userConfirmedUseCompilerModuleSwitch": false 32 | }, 33 | "compileType": "miniprogram", 34 | "libVersion": "2.11.2", 35 | "appid": "wx1dbf2281ce5d62ac", 36 | "projectname": "gesture-demo99", 37 | "debugOptions": { 38 | "hidedInDevtools": [] 39 | }, 40 | "scripts": {}, 41 | "isGameTourist": false, 42 | "simulatorType": "wechat", 43 | "simulatorPluginLibVersion": {}, 44 | "condition": { 45 | "search": { 46 | "current": -1, 47 | "list": [] 48 | }, 49 | "conversation": { 50 | "current": -1, 51 | "list": [] 52 | }, 53 | "game": { 54 | "current": -1, 55 | "list": [] 56 | }, 57 | "plugin": { 58 | "current": -1, 59 | "list": [] 60 | }, 61 | "gamePlugin": { 62 | "current": -1, 63 | "list": [] 64 | }, 65 | "miniprogram": { 66 | "current": -1, 67 | "list": [] 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /example/sitemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", 3 | "rules": [{ 4 | "action": "allow", 5 | "page": "*" 6 | }] 7 | } -------------------------------------------------------------------------------- /example/utils/util.js: -------------------------------------------------------------------------------- 1 | const formatNumber = n => { 2 | n = n.toString() 3 | return n[1] ? n : '0' + n 4 | } 5 | 6 | const formatTime = date => { 7 | const year = date.getFullYear() 8 | const month = date.getMonth() + 1 9 | const day = date.getDate() 10 | const hour = date.getHours() 11 | const minute = date.getMinutes() 12 | const second = date.getSeconds() 13 | 14 | return [year, month, day].map(formatNumber).join('/') + ' ' + [hour, minute, second].map(formatNumber).join(':') 15 | } 16 | 17 | module.exports = { 18 | formatTime 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "miniprogram-gesture", 3 | "version": "1.0.6", 4 | "description": "", 5 | "main": "index.js", 6 | "miniprogram": "dist", 7 | "scripts": { 8 | "lint": "eslint ." 9 | }, 10 | "author": "yangziyue80@outlook.com", 11 | "license": "MIT", 12 | "repository": { 13 | "github": "https://github.com/ttzztztz/miniprogram-gesture" 14 | }, 15 | "devDependencies": { 16 | "eslint": "^5.5.0", 17 | "eslint-config-airbnb-base": "13.1.0", 18 | "eslint-plugin-import": "^2.14.0", 19 | "eslint-plugin-node": "^7.0.1", 20 | "eslint-plugin-promise": "^4.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/gesture.js: -------------------------------------------------------------------------------- 1 | Component({ 2 | options: { 3 | addGlobalClass: true, 4 | multipleSlots: true, 5 | }, 6 | properties: { 7 | propagation: { 8 | type: Boolean, 9 | value: true, 10 | }, 11 | requireFailure: { 12 | type: Boolean, 13 | value: true, 14 | }, 15 | }, 16 | methods: {} 17 | }) 18 | -------------------------------------------------------------------------------- /src/gesture.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "component": true, 4 | "usingComponents": {} 5 | } -------------------------------------------------------------------------------- /src/gesture.wxml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | -------------------------------------------------------------------------------- /src/gesture.wxs: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable */ 3 | // @ts-ignore 4 | function getLen(v) { 5 | return Math.sqrt(v.x * v.x + v.y * v.y) 6 | } 7 | function dot(v1, v2) { 8 | return v1.x * v2.x + v1.y * v2.y 9 | } 10 | function getAngle(v1, v2) { 11 | var mr = getLen(v1) * getLen(v2) 12 | if (mr === 0) return 0 13 | var r = dot(v1, v2) / mr 14 | if (r > 1) r = 1 15 | return Math.acos(r) 16 | } 17 | function cross(v1, v2) { 18 | return v1.x * v2.y - v2.x * v1.y 19 | } 20 | function getRotateAngle(v1, v2) { 21 | var angle = getAngle(v1, v2) 22 | if (cross(v1, v2) > 0) { 23 | angle *= -1 24 | } 25 | return angle * 180 / Math.PI 26 | } 27 | function _swipeDirection(x1, x2, y1, y2) { 28 | if (Math.abs(x1 - x2) >= Math.abs(y1 - y2)) { 29 | return x1 - x2 > 0 ? 'Left' : 'Right' 30 | } else { 31 | return y1 - y2 > 0 ? 'Up' : 'Down' 32 | } 33 | } 34 | // 实现setTimeout功能 35 | var setTimeout = function(callback, interval, instance) { 36 | var now = Date.now 37 | var stime = now() 38 | var loop = function() { 39 | if (now() - stime >= interval) { 40 | callback() 41 | } else { 42 | instance.requestAnimationFrame(loop) 43 | } 44 | } 45 | instance.requestAnimationFrame(loop) 46 | } 47 | var start = function(event, ownerInstance) { 48 | var instance = event.instance; 49 | var State = instance.getState() 50 | if(!State._init) { 51 | State.preV = {x: null, y: null} 52 | State.pinchStartLen = null 53 | State.zoom = 1 54 | State.isDoubleTap = false 55 | State.delta = null 56 | State.last = null 57 | State.now = null 58 | State.x1 = State.x2 = State.y1 = State.y2 = null 59 | State.preTapPosition = {x: null, y: null} 60 | // 控制定时器 61 | State._cancelLongTap = function() { 62 | State.longTapTimeout = false 63 | } 64 | State._cancelSingleTap = function() { 65 | State.singleTapTimeout = false 66 | } 67 | State._tapTimeout = function() { 68 | State.tapTimeout = false 69 | } 70 | State._swipeTimeout = function() { 71 | State.swipeTimeout = false 72 | } 73 | State._init = true // 表示已经初始化完成 74 | } 75 | State.tapTimeout = true 76 | State.singleTapTimeout = true 77 | State.longTapTimeout = true 78 | State.swipeTimeout = true 79 | State.now = Date.now() 80 | State.x1 = event.touches[0].pageX 81 | State.y1 = event.touches[0].pageY 82 | State.delta = State.now - (State.last || State.now) 83 | // 触发 touchStart 事件 84 | ownerInstance.triggerEvent('touchStart', event) 85 | if (State.preTapPosition.x !== null) { 86 | State.isDoubleTap = (State.delta > 0 && 87 | State.delta <= 250 && 88 | Math.abs(State.preTapPosition.x - State.x1) < 30 && 89 | Math.abs(State.preTapPosition.y - State.y1) < 30) 90 | if (State.isDoubleTap) { 91 | State._cancelSingleTap() 92 | } 93 | } 94 | State.preTapPosition.x = State.x1 95 | State.preTapPosition.y = State.y1 96 | State.last = State.now 97 | var preV = State.preV 98 | var len = event.touches.length 99 | if (len > 1) { 100 | State._cancelLongTap() 101 | State._cancelSingleTap() 102 | var v = {x: event.touches[1].pageX - State.x1, y: event.touches[1].pageY - State.y1} 103 | preV.x = v.x 104 | preV.y = v.y 105 | State.pinchStartLen = getLen(preV) 106 | // 触发 multipointStart 多指点按 事件 107 | ownerInstance.triggerEvent('multipointStart', event) 108 | } 109 | State._preventTap = false 110 | setTimeout(function () { 111 | // 触发 longTap(长按) 事件 112 | if(State.longTapTimeout) { 113 | ownerInstance.triggerEvent('longTap', event) 114 | State._preventTap = true 115 | State.longTapTimeout = true 116 | } 117 | }, 750, instance) 118 | 119 | if (!instance.getDataset()['propagation']) return false 120 | } 121 | var move = function(event, ownerInstance) { 122 | var instance = event.instance; 123 | var State = instance.getState() 124 | var preV = State.preV 125 | var len = event.touches.length 126 | var currentX = event.touches[0].pageX 127 | var currentY = event.touches[0].pageY 128 | State.isDoubleTap = false 129 | if (len > 1) { 130 | var sCurrentX = event.touches[1].pageX 131 | var sCurrentY = event.touches[1].pageY 132 | var v = {x: event.touches[1].pageX - currentX, y: event.touches[1].pageY - currentY} 133 | if (preV.x !== null) { 134 | if (State.pinchStartLen > 0) { 135 | event.zoom = getLen(v) / State.pinchStartLen 136 | // 触发 pinch 事件 137 | ownerInstance.triggerEvent('pinch', event) 138 | } 139 | event.angle = getRotateAngle(v, preV) 140 | // 触发 rotate 事件 141 | ownerInstance.triggerEvent('rotate', event) 142 | } 143 | preV.x = v.x 144 | preV.y = v.y 145 | if (State.x2 !== null && State.sx2 !== null) { 146 | event.deltaX = (currentX - State.x2 + sCurrentX - State.sx2) / 2 147 | event.deltaY = (currentY - State.y2 + sCurrentY - State.sy2) / 2 148 | } else { 149 | event.deltaX = 0 150 | event.deltaY = 0 151 | } 152 | // 触发 twoFingerPressMove 事件 153 | ownerInstance.triggerEvent('twoFingerPressMove', event) 154 | State.sx2 = sCurrentX 155 | State.sy2 = sCurrentY 156 | } else { 157 | if (State.x2 !== null) { 158 | event.deltaX = currentX - State.x2 159 | event.deltaY = currentY - State.y2 160 | // move事件中添加对当前触摸点到初始触摸点的判断, 161 | // 如果曾经大于过某个距离(比如10),就认为是移动到某个地方又移回来,应该不再触发tap事件才对。 162 | var movedX = Math.abs(State.x1 - State.x2) 163 | var movedY = Math.abs(State.y1 - State.y2) 164 | if (movedX > 10 || movedY > 10) { 165 | State._preventTap = true 166 | } 167 | } else { 168 | event.deltaX = 0 169 | event.deltaY = 0 170 | } 171 | // 触发 pressMove 单指点按移动 事件 172 | ownerInstance.triggerEvent('pressMove', event) 173 | } 174 | // 触发 touchMove 移动事件 175 | ownerInstance.triggerEvent('touchMove', event) 176 | State._cancelLongTap() 177 | State.x2 = currentX 178 | State.y2 = currentY 179 | // if (len > 1) { 180 | // // event.preventDefault() 181 | // } 182 | if (!instance.getDataset()['propagation']) return false 183 | } 184 | var end = function(event, ownerInstance) { 185 | var instance = event.instance; 186 | var State = instance.getState() 187 | State._cancelLongTap() 188 | if (event.touches.length < 2) { 189 | // 触发 multipointEnd 多指点按结束 事件 190 | ownerInstance.triggerEvent('multipointEnd', event) 191 | State.sx2 = State.sy2 = null 192 | } 193 | // swipe 194 | if ((State.x2 && Math.abs(State.x1 - State.x2) > 30) || 195 | (State.y2 && Math.abs(State.y1 - State.y2) > 30)) { 196 | event.direction = _swipeDirection(State.x1, State.x2, State.y1, State.y2) 197 | setTimeout(function () { 198 | if(State.swipeTimeout) { 199 | // 触发 swipe 滑动 上下左右 事件 200 | ownerInstance.triggerEvent('swipe', event) 201 | State.swipeTimeout = true 202 | } 203 | }, 0, instance) 204 | } else { 205 | setTimeout(function () { 206 | if(State.tapTimeout) { 207 | if (!State._preventTap) { 208 | // 触发 tap 事件 209 | ownerInstance.triggerEvent('tap', event) 210 | } 211 | // trigger double tap immediately 212 | if (State.isDoubleTap) { 213 | // 触发 doubleTap 事件 214 | ownerInstance.triggerEvent('doubleTap', event) 215 | State.isDoubleTap = false 216 | } 217 | State.tapTimeout = true 218 | } 219 | }, 0, instance) 220 | if (!State.isDoubleTap) { 221 | if (instance.getDataset()['requirefailure']) { // requireFailure 222 | setTimeout(function () { 223 | if(State.singleTapTimeout) { 224 | // 触发 singleTap 事件 225 | ownerInstance.triggerEvent('singleTap', event) 226 | State.singleTapTimeout = true 227 | } 228 | }, 250, instance) 229 | } else { 230 | ownerInstance.triggerEvent('singleTap', event) 231 | State.singleTapTimeout = true 232 | } 233 | } 234 | } 235 | // 触发 touchEnd 事件 236 | ownerInstance.triggerEvent('touchEnd', event) 237 | State.preV.x = 0 238 | State.preV.y = 0 239 | State.zoom = 1 240 | State.pinchStartLen = null 241 | State.x1 = State.x2 = null 242 | State.y1 = State.y2 = null 243 | 244 | if (!instance.getDataset()['propagation']) return false 245 | } 246 | var cancel = function(event, ownerInstance) { 247 | var instance = event.instance; 248 | var State = instance.getState() 249 | State._cancelLongTap() 250 | State._cancelSingleTap() 251 | State._tapTimeout() 252 | State._swipeTimeout() 253 | // 触发 touchCancel 事件 254 | ownerInstance.triggerEvent('touchCancel', event) 255 | 256 | if (!instance.getDataset()['propagation']) return false 257 | } 258 | module.exports = { 259 | start: start, 260 | move: move, 261 | end: end, 262 | cancel: cancel 263 | } 264 | --------------------------------------------------------------------------------