├── package.json ├── LICENSE ├── mina-touch.d.ts ├── README.md └── mina-touch.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mina-touch", 3 | "version": "1.1.0", 4 | "description": "一个轻量、方便的小程序手势事件监听库", 5 | "main": "mina-touch.js", 6 | "types": "mina-touch.d.ts", 7 | "scripts": {}, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/Yrobot/mina-touch.git" 11 | }, 12 | "keywords": [ 13 | "Yrobot", 14 | "mina", 15 | "touch" 16 | ], 17 | "author": "Yrobot", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/Yrobot/mina-touch/issues" 21 | }, 22 | "homepage": "https://github.com/Yrobot/mina-touch#readme" 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Yrobot 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 | -------------------------------------------------------------------------------- /mina-touch.d.ts: -------------------------------------------------------------------------------- 1 | declare interface TouchPoint { 2 | pageX?: number; 3 | pageY?: number; 4 | x?: number; 5 | y?: number; 6 | } 7 | 8 | declare interface MinaTouchEvent { 9 | touches: TouchPoint[]; 10 | changedTouches?: TouchPoint[]; 11 | type?: string; 12 | deltaX?: number; 13 | deltaY?: number; 14 | direction?: "Left" | "Right" | "Up" | "Down"; 15 | singleZoom?: number; 16 | zoom?: number; 17 | angle?: number; 18 | } 19 | 20 | declare interface MinaTouchOptions { 21 | touchStart: (evt: MinaTouchEvent) => void; 22 | touchMove: (evt: MinaTouchEvent) => void; 23 | touchEnd: (evt: MinaTouchEvent) => void; 24 | touchCancel: (evt: MinaTouchEvent) => void; 25 | multipointStart: (evt: MinaTouchEvent) => void; 26 | multipointEnd: (evt: MinaTouchEvent) => void; 27 | tap: (evt: MinaTouchEvent) => void; 28 | doubleTap: (evt: MinaTouchEvent) => void; 29 | longTap: (evt: MinaTouchEvent) => void; 30 | singleTap: (evt: MinaTouchEvent) => void; 31 | rotate: (evt: MinaTouchEvent) => void; 32 | pinch: (evt: MinaTouchEvent) => void; 33 | pressMove: (evt: MinaTouchEvent) => void; 34 | swipe: (evt: MinaTouchEvent) => void; 35 | } 36 | 37 | declare class MinaTouch { 38 | constructor( 39 | _page: Record, 40 | name: string, 41 | option?: Partial 42 | ); 43 | 44 | start(evt: MinaTouchEvent): void; 45 | move(evt: MinaTouchEvent): void; 46 | end(evt: MinaTouchEvent): void; 47 | cancel(evt: MinaTouchEvent): void; 48 | destroy(): void; 49 | } 50 | 51 | export default MinaTouch; 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mina-touch 2 | 3 | ![](https://track.yrobot.top/ga-beacon/UA-190592680-2/mina-touch/readme?flat) 4 | 5 | `mina-touch`,一个方便、轻量的 **小程序** 手势事件监听库 6 | 事件库部分逻辑参考`alloyFinger`,在此做出声明和感谢 7 | 8 | ## change log: 9 | 10 | 1. 2019.03.10 优化监听和绘制逻辑,动画不卡顿 11 | 2. 2019.03.12 修复第二次之后缩放闪烁的 bug,pinch 添加 singleZoom 参数 12 | 3. 2020.12.13 更名 mina-touch 13 | 4. 2020.12.27 上传 npm 库;优化使用方式;优化 README 14 | 4. 2025.4.1[1.1.0] 支持 ts(添加 mina-touch.d.ts) 15 | 16 | ## 支持的事件 17 | 18 | - 支持 pinch 缩放 19 | - 支持 rotate 旋转 20 | - 支持 pressMove 拖拽 21 | - 支持 doubleTap 双击 22 | - 支持 swipe 滑动 23 | - 支持 longTap 长按 24 | - 支持 tap 按 25 | - 支持 singleTap 单击 26 | 27 | ## 扫码体验 28 | 29 | 30 | 31 | ## demo 展示 32 | 33 | 1. demo1:监听 pressMove 拖拽 手势 [查看 demo 代码](https://github.com/Yrobot/mina-tools-client/tree/master/miniprogram/pages/mina-touch/demo1) 34 | | | | 35 | | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | 36 | 37 | 2. demo2: 监听 pinch 缩放 和 rotate 旋转 手势 (已优化动画卡顿 bug) [查看 demo 代码](https://github.com/Yrobot/mina-tools-client/tree/master/miniprogram/pages/mina-touch/demo2) 38 | | | | 39 | | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | 40 | 41 | 3. demo3: 测试监听双击事件 [查看 demo 代码](https://github.com/Yrobot/mina-tools-client/tree/master/miniprogram/pages/mina-touch/demo3) 42 | | | | 43 | | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | 44 | 45 | 4. demo4: 测试监听长按事件 [查看 demo 代码](https://github.com/Yrobot/mina-tools-client/tree/master/miniprogram/pages/mina-touch/demo4) 46 | | | | 47 | | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | 48 | 49 | ## demo 代码 50 | 51 | demo 代码地址 [mina-tools-client/mina-touch](https://github.com/Yrobot/mina-tools-client/tree/master/miniprogram/pages/mina-touch) 52 | 53 | ## 使用方法 54 | 55 | **大致可以分为 4 步:** 56 | 57 | 1. npm 安装 mina-touch,开发工具构建 npm 58 | 2. 引入 mina-touch 59 | 3. onload 实例化 mina-touch 60 | 4. wxml 绑定实例 61 | 62 | ### 命令行 63 | 64 | `npm install mina-touch ` 65 | 安装完成后,开发工具构建 npm 66 | 67 | ### \*.js 68 | 69 | ```javascript 70 | import MinaTouch from 'mina-touch'; // 1. 引入mina-touch 71 | 72 | Page({ 73 | onLoad: function (options) { 74 | new MinaTouch(this, 'touch1', { 75 | // 2. onload实例化mina-touch 76 | //会创建this.touch1指向实例对象 77 | touchStart: function () {}, 78 | touchMove: function () {}, 79 | touchEnd: function () {}, 80 | touchCancel: function () {}, 81 | multipointStart: function () { 82 | console.log('multipointStart'); 83 | }, //一个手指以上触摸屏幕触发 84 | multipointEnd: function () { 85 | console.log('multipointEnd'); 86 | }, //当手指离开,屏幕只剩一个手指或零个手指触发(一开始只有一根手指也会触发) 87 | tap: function () { 88 | console.log('Tap'); 89 | }, //点按触发,覆盖下方3个点击事件,doubleTap时触发2次 90 | doubleTap: function () { 91 | console.log('doubleTap'); 92 | }, //双击屏幕触发 93 | longTap: function () { 94 | console.log('longTap'); 95 | }, //长按屏幕750ms触发 96 | singleTap: function () { 97 | console.log('singleTap'); 98 | }, //单击屏幕触发,包括长按 99 | rotate: function (evt) { 100 | //evt.angle代表两个手指旋转的角度 101 | console.log('rotate:' + evt.angle); 102 | }, 103 | pinch: function (evt) { 104 | //evt.zoom代表两个手指缩放的比例(多次缩放的累计值),evt.singleZoom代表单次回调中两个手指缩放的比例 105 | console.log('pinch:' + evt.zoom); 106 | }, 107 | pressMove: function (evt) { 108 | //evt.deltaX和evt.deltaY代表在屏幕上移动的距离,evt.target可以用来判断点击的对象 109 | console.log(evt.target); 110 | console.log(evt.deltaX); 111 | console.log(evt.deltaY); 112 | }, 113 | swipe: function (evt) { 114 | //在touch结束触发,evt.direction代表滑动的方向 ['Up','Right','Down','Left'] 115 | console.log('swipe:' + evt.direction); 116 | }, 117 | }); 118 | }, 119 | }); 120 | ``` 121 | 122 | NOTE: 123 | 124 | 1. 多类型事件监听触发 setData 时,建议把数据合并,在 touchMove 中一起进行 setData ,以减少短时内多次 setData 引起的动画延迟和卡顿(参考 demo2) 125 | 126 | ### \*.wxml 127 | 128 | 在 view 上绑定事件并对应: 129 | 130 | ```html 131 | 137 | 138 | 144 | ``` 145 | 146 | NOTE: 147 | 148 | 1. 如果不影响业务,建议使用 catch 捕获事件,否则易造成监听动画卡顿(参考 demo2) 149 | 150 | --- 151 | 152 | 以上简单几步即可使用 mina-touch 手势库 153 | -------------------------------------------------------------------------------- /mina-touch.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_OPTIONS = { 2 | touchStart: function () { }, 3 | touchMove: function () { }, 4 | touchEnd: function () { }, 5 | touchCancel: function () { }, 6 | multipointStart: function () { }, 7 | multipointEnd: function () { }, 8 | tap: function () { }, 9 | doubleTap: function () { }, 10 | longTap: function () { }, 11 | singleTap: function () { }, 12 | rotate: function () { }, 13 | pinch: function () { }, 14 | pressMove: function () { }, 15 | swipe: function () { } 16 | } 17 | export default class MinaTouch { 18 | constructor(_page, name, option = {}) { 19 | this.preV = { x: null, y: null }; 20 | this.pinchStartLen = null; 21 | this.scale = 1; 22 | this.isDoubleTap = false; 23 | 24 | this.delta = null; 25 | this.last = null; 26 | this.now = null; 27 | this.tapTimeout = null; 28 | this.singleTapTimeout = null; 29 | this.longTapTimeout = null; 30 | this.swipeTimeout = null; 31 | this.x1 = this.x2 = this.y1 = this.y2 = null; 32 | this.preTapPosition = { x: null, y: null }; 33 | 34 | this.lastZoom = 1; 35 | this.tempZoom = 1; 36 | 37 | try { 38 | if (this._checkBeforeCreate(_page, name)) { 39 | this._name = name 40 | this._option = { ...DEFAULT_OPTIONS, ...option } 41 | _page[name] = this 42 | this._bindFunc(_page) 43 | } 44 | } catch (error) { 45 | console.error(error) 46 | } 47 | } 48 | _checkBeforeCreate(_page, name) { 49 | if (!_page || !name) { 50 | throw new Error('MinaTouch实例化时,必须传入page对象和引用名') 51 | } 52 | if (_page[name]) { 53 | throw new Error('MinaTouch实例化error: ' + name + ' 已经存在page中') 54 | } 55 | return true 56 | } 57 | _bindFunc(_page) { 58 | let funcNames = ['start', 'move', 'end', 'cancel'] 59 | for (let funcName of funcNames) 60 | _page[this._name + '.' + funcName] = this[funcName].bind(this) 61 | } 62 | start(evt) { 63 | if (!evt.touches) return; 64 | this.now = Date.now(); 65 | this.x1 = evt.touches[0].pageX == null ? evt.touches[0].x : evt.touches[0].pageX; 66 | this.y1 = evt.touches[0].pageY == null ? evt.touches[0].y : evt.touches[0].pageY; 67 | this.delta = this.now - (this.last || this.now); 68 | this._option.touchStart(evt); 69 | if (this.preTapPosition.x !== null) { 70 | this.isDoubleTap = (this.delta > 0 && this.delta <= 250 && Math.abs(this.preTapPosition.x - this.x1) < 30 && Math.abs(this.preTapPosition.y - this.y1) < 30); 71 | } 72 | this.preTapPosition.x = this.x1; 73 | this.preTapPosition.y = this.y1; 74 | this.last = this.now; 75 | let preV = this.preV, 76 | len = evt.touches.length; 77 | if (len > 1) { 78 | this._cancelLongTap(); 79 | this._cancelSingleTap(); 80 | let otx = evt.touches[1].pageX == null ? evt.touches[1].x : evt.touches[1].pageX; 81 | let oty = evt.touches[1].pageY == null ? evt.touches[1].y : evt.touches[1].pageY; 82 | let v = { x: otx - this.x1, y: oty - this.y1 }; 83 | preV.x = v.x; 84 | preV.y = v.y; 85 | this.pinchStartLen = getLen(preV); 86 | this._option.multipointStart(evt); 87 | } 88 | this.longTapTimeout = setTimeout(function () { 89 | evt.type = "longTap"; 90 | this._option.longTap(evt); 91 | }.bind(this), 750); 92 | } 93 | move(evt) { 94 | if (!evt.touches) return; 95 | let preV = this.preV, 96 | len = evt.touches.length, 97 | currentX = evt.touches[0].pageX == null ? evt.touches[0].x : evt.touches[0].pageX, 98 | currentY = evt.touches[0].pageY == null ? evt.touches[0].y : evt.touches[0].pageY; 99 | this.isDoubleTap = false; 100 | if (len > 1) { 101 | let otx = evt.touches[1].pageX == null ? evt.touches[1].x : evt.touches[1].pageX; 102 | let oty = evt.touches[1].pageY == null ? evt.touches[1].y : evt.touches[1].pageY; 103 | let v = { x: otx - currentX, y: oty - currentY }; 104 | 105 | if (preV.x !== null) { 106 | if (this.pinchStartLen > 0) { 107 | evt.singleZoom = getLen(v) / this.pinchStartLen; 108 | evt.zoom = evt.singleZoom * this.lastZoom; 109 | this.tempZoom = evt.zoom; 110 | evt.type = "pinch"; 111 | this._option.pinch(evt); 112 | } 113 | 114 | evt.angle = getRotateAngle(v, preV); 115 | evt.type = "rotate"; 116 | this._option.rotate(evt); 117 | } 118 | preV.x = v.x; 119 | preV.y = v.y; 120 | } else { 121 | if (this.x2 !== null) { 122 | evt.deltaX = currentX - this.x2; 123 | evt.deltaY = currentY - this.y2; 124 | 125 | } else { 126 | evt.deltaX = 0; 127 | evt.deltaY = 0; 128 | } 129 | this._option.pressMove(evt); 130 | } 131 | 132 | this._option.touchMove(evt); 133 | 134 | this._cancelLongTap(); 135 | this.x2 = currentX; 136 | this.y2 = currentY; 137 | if (len > 1) { 138 | // evt.preventDefault(); 139 | } 140 | } 141 | end(evt) { 142 | if (!evt.changedTouches) return; 143 | this._cancelLongTap(); 144 | let self = this; 145 | evt.direction = this._swipeDirection(this.x1, this.x2, this.y1, this.y2); //在结束钩子都加入方向判断,但触发swipe瞬时必须位移大于30 146 | if (evt.touches.length < 2) { 147 | this.lastZoom = this.tempZoom; 148 | this._option.multipointEnd(evt); 149 | } 150 | this._option.touchEnd(evt); 151 | //swipe 152 | if ((this.x2 && Math.abs(this.x1 - this.x2) > 30) || 153 | (this.y2 && Math.abs(this.y1 - this.y2) > 30)) { 154 | // evt.direction = this._swipeDirection(this.x1, this.x2, this.y1, this.y2); 155 | this.swipeTimeout = setTimeout(function () { 156 | evt.type = "swipe"; 157 | self._option.swipe(evt); 158 | 159 | }, 0) 160 | } else { 161 | this.tapTimeout = setTimeout(function () { 162 | evt.type = "tap"; 163 | self._option.tap(evt); 164 | // trigger double tap immediately 165 | if (self.isDoubleTap) { 166 | evt.type = "doubleTap"; 167 | self._option.doubleTap(evt); 168 | clearTimeout(self.singleTapTimeout); 169 | self.isDoubleTap = false; 170 | } 171 | }, 0) 172 | 173 | if (!self.isDoubleTap) { 174 | self.singleTapTimeout = setTimeout(function () { 175 | self._option.singleTap(evt); 176 | }, 250); 177 | } 178 | } 179 | 180 | this.preV.x = 0; 181 | this.preV.y = 0; 182 | this.scale = 1; 183 | this.pinchStartLen = null; 184 | this.x1 = this.x2 = this.y1 = this.y2 = null; 185 | } 186 | cancel(evt) { 187 | clearTimeout(this.singleTapTimeout); 188 | clearTimeout(this.tapTimeout); 189 | clearTimeout(this.longTapTimeout); 190 | clearTimeout(this.swipeTimeout); 191 | this._option.touchCancel(evt); 192 | } 193 | _cancelLongTap() { 194 | clearTimeout(this.longTapTimeout); 195 | } 196 | 197 | _cancelSingleTap() { 198 | clearTimeout(this.singleTapTimeout); 199 | } 200 | 201 | _swipeDirection(x1, x2, y1, y2) { 202 | return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down') 203 | } 204 | destroy() { 205 | if (this.singleTapTimeout) clearTimeout(this.singleTapTimeout); 206 | if (this.tapTimeout) clearTimeout(this.tapTimeout); 207 | if (this.longTapTimeout) clearTimeout(this.longTapTimeout); 208 | if (this.swipeTimeout) clearTimeout(this.swipeTimeout); 209 | 210 | this._option.rotate = null; 211 | this._option.touchStart = null; 212 | this._option.multipointStart = null; 213 | this._option.multipointEnd = null; 214 | this._option.pinch = null; 215 | this._option.swipe = null; 216 | this._option.tap = null; 217 | this._option.doubleTap = null; 218 | this._option.longTap = null; 219 | this._option.singleTap = null; 220 | this._option.pressMove = null; 221 | this._option.touchMove = null; 222 | this._option.touchEnd = null; 223 | this._option.touchCancel = null; 224 | 225 | this.preV = this.pinchStartLen = this.scale = this.isDoubleTap = this.delta = this.last = this.now = this.tapTimeout = this.singleTapTimeout = this.longTapTimeout = this.swipeTimeout = this.x1 = this.x2 = this.y1 = this.y2 = this.preTapPosition = this.rotate = this.touchStart = this.multipointStart = this.multipointEnd = this.pinch = this.swipe = this.tap = this.doubleTap = this.longTap = this.singleTap = this.pressMove = this.touchMove = this.touchEnd = this.touchCancel = null; 226 | 227 | return null; 228 | } 229 | } 230 | 231 | function getLen(v) { 232 | return Math.sqrt(v.x * v.x + v.y * v.y); 233 | } 234 | 235 | function dot(v1, v2) { 236 | return v1.x * v2.x + v1.y * v2.y; 237 | } 238 | 239 | function getAngle(v1, v2) { 240 | let mr = getLen(v1) * getLen(v2); 241 | if (mr === 0) return 0; 242 | let r = dot(v1, v2) / mr; 243 | if (r > 1) r = 1; 244 | return Math.acos(r); 245 | } 246 | 247 | function cross(v1, v2) { 248 | return v1.x * v2.y - v2.x * v1.y; 249 | } 250 | 251 | function getRotateAngle(v1, v2) { 252 | let angle = getAngle(v1, v2); 253 | if (cross(v1, v2) > 0) { 254 | angle *= -1; 255 | } 256 | 257 | return angle * 180 / Math.PI; 258 | } --------------------------------------------------------------------------------