├── .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 |
--------------------------------------------------------------------------------