📦 YMFE / yGesture

📄 yGesture.js · 399 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399var TOUCHKEYS = [
        'screenX', 'screenY', 'clientX', 'clientY', 'pageX', 'pageY'
    ], // 需要复制的属性
    TOUCH_NUM = 2, // 最大支持触点数 1 或 2
    TAP_TIMEOUT = 200, // 判断 tap 的延时
    FLICK_TIMEOUT = 300, // 判断 flick 的延时
    PAN_DISTANCE = 10, // 判定 pan 的位移偏移量
    DIRECTION_DEG = 15, // 判断方向的角度
    DOUBLETAP_GAP = 500, // double 判定延时
    PINCH_DIS = 10; // 判定 pinch 的位移偏移量

var curElement = null,
    curVetor = null,
    gestures = {},
    lastTapTime = 0,
    initialAngle = 0,
    rotation = 0,
    longTap = true,
    enabled = true;

var slice = Array.prototype.slice;

// 绑定事件
function addEvent(node, type, listener, useCapture) {
    node.addEventListener(type, listener, !!useCapture);
}

// Array Make
function makeArray(iterable) {
    var n = iterable.length;
    if (n === (n >>> 0)) {
        try {
            return slice.call(iterable);
        } catch (e) {
        }
    }
    return true;
}

// ready
function ready(callback) {
    if (/complete|loaded|interactive/.test(document.readyState) && document.body) {
        callback();
    } else {
        addEvent(document, 'DOMContentLoaded', function () {
            callback();
        });
    }
}


// 是否支持多指
function supportMulti() {
    return TOUCH_NUM == 2;
}

// 获取 obj 中 key 的数量
function getKeys(obj) {
    return Object.getOwnPropertyNames(obj);
}

// 判断对象是否为空
function isEmpty(obj) {
    return getKeys(obj).length === 0;
}

// fix:safari可能是文本节点
function fixElement(el) {
    return 'tagName' in el ? el : el.parentNode;
}

// 创建事件对象
function createEvent(type) {
    var event = document.createEvent("HTMLEvents");
    event.initEvent(type, true, true);
    return event;
}

// 触发事件
function trigger(curElement, event) {
    if (enabled && curElement && curElement.dispatchEvent) {
        curElement.dispatchEvent(event);
    }
}

// 复制 touch 对象上的有用属性到固定对象上
function mixTouchAttr(target, source) {
    TOUCHKEYS.forEach(function(key) {
        target[key] = source[key];
    });
    return target;
}

// 获取方向
function getDirection(offsetX, offsetY) {
    var ret = [],
        absX = Math.abs(offsetX),
        absY = Math.abs(offsetY),
        proportion = Math.tan(DIRECTION_DEG / 180 * Math.PI),
        transverse = absX > absY;

    if (absX > 0 || absY > 0) {
        ret.push(transverse ? offsetX > 0 ? 'right' : 'left' : offsetY > 0 ? 'down' : 'up');
        if (transverse && absY / absX > proportion) {
            ret.push(offsetY > 0 ? 'down' : 'up');
        } else if (!transverse && absX / absY > proportion) {
            ret.push(offsetX > 0 ? 'right' : 'left');
        }
    }

    return ret;
}

// 计算距离
function computeDistance(offsetX, offsetY) {
    return Math.sqrt(Math.pow(offsetX, 2) + Math.pow(offsetY, 2));
}

// 计算角度
function computeDegree(offsetX, offsetY) {
    var degree = Math.atan2(offsetY, offsetX) / Math.PI * 180;
    return degree < 0 ? degree + 360 : degree;
}

// 计算角度,返回(0-180)
function computeDegree180(offsetX, offsetY) {
    var degree = Math.atan(offsetY * -1 / offsetX) / Math.PI * 180;
    return degree < 0 ? degree + 180 : degree;
}

// 获取偏转角
function getAngleDiff(offsetX, offsetY) {
    var diff = initialAngle - computeDegree180(offsetX, offsetY);

    while (Math.abs(diff - rotation) > 90) {
        if (rotation < 0) {
            diff -= 180;
        } else {
            diff += 180;
        }
    }
    rotation = diff;
    return rotation;
}

// 构造 pan / flick / panend 事件
function createPanEvent(type, offsetX, offsetY, touch, duration) {
    var ev = createEvent(type);
    ev.offsetX = offsetX;
    ev.offsetY = offsetY;
    ev.degree = computeDegree(offsetX, offsetY);
    ev.directions = getDirection(offsetX, offsetY);
    if (duration) {
        ev.duration = duration;
        ev.speedX = ev.offsetX / duration;
        ev.speedY = ev.offsetY / duration;
    }
    return mixTouchAttr(ev, touch);
}

// 构造 pinch 事件
function createMultiEvent(type, centerX, centerY, scale, deflection, touch1, touch2) {
    var ev = createEvent(type);
    ev.centerX = centerX;
    ev.centerY = centerY;
    if (scale !== void 0) {
        ev.scale = scale;
    }
    if (deflection !== void 0) {
        ev.deflection = deflection;
    }
    ev.touchs = [touch1, touch2];
    return ev;
}

// 判断是否处理完所有触点
function checkEnd() {
    var flag = true;
    for (var key in gestures) {
        if (gestures[key].status != 'end') {
            flag = false;
            break;
        }
    }
    return flag;
}

ready(function() {
    var body = document.body;

    // 处理 touchstart 事件
    function touchStart(event) {

        // 判定现在是否开始手势判定
        if (isEmpty(gestures)) {
            // 获取第一个触点的Element
            curElement = fixElement(event.touches[0].target);
        }

        // 遍历每一个 touch 对象,进行处理
        makeArray(event.changedTouches).forEach(function(touch, index) {
            var keys = getKeys(gestures);
            if (keys.length < TOUCH_NUM) {
                var origin = mixTouchAttr({}, touch),
                    gesture = {
                        startTouch: origin,
                        curTouch: origin,
                        startTime: Date.now(),
                        status: 'tapping',
                        other: null,
                        handler: setTimeout(function() {
                            if (gesture) {
                                if (gesture.status == 'tapping') {
                                    gesture.status = 'pressing';
                                    trigger(curElement, mixTouchAttr(createEvent('press'), origin));
                                }
                                clearTimeout(gesture.handler);
                                gesture.handler = null;
                            }
                        }, TAP_TIMEOUT)
                    };

                trigger(curElement, mixTouchAttr(createEvent('feel'), origin));

                // 每一次手势不同触点的 identifier 是不同的
                gestures[touch.identifier] = gesture;

                if (supportMulti() && keys.length == 1) {
                    var otherTouch = gestures[keys[0]].startTouch,
                        disX = origin.clientX - otherTouch.clientX,
                        disY = origin.clientY - otherTouch.clientY,
                        centerX = (origin.clientX + otherTouch.clientX) / 2,
                        centerY = (origin.clientY + otherTouch.clientY) / 2;
                    gesture.other = gestures[keys[0]];
                    gestures[keys[0]].other = gesture;
                    curVetor = {
                        centerX: centerX,
                        centerY: centerY,
                        pinch: false,
                        deflection: false,
                        distance: computeDistance(disX, disY)
                    };

                    initialAngle = computeDegree180(disX, disY);
                }
            }
        });
    }

    // 处理 touchmove 事件
    function touchMove(event) {
        makeArray(event.changedTouches).forEach(function(touch, index) {
            var gesture = gestures[touch.identifier],
                flag = false;
            if (gesture) {
                var startTouch = gesture.startTouch,
                    offsetX = touch.clientX - startTouch.clientX,
                    offsetY = touch.clientY - startTouch.clientY;

                if (gesture.status == 'tapping' || gesture.status == 'pressing') {
                    if (computeDistance(offsetX, offsetY) > PAN_DISTANCE) {
                        gesture.status = 'panning';
                        // 记录移动开始的时间
                        gesture.startMoveTime = Date.now();
                        trigger(curElement, createPanEvent('pan', offsetX, offsetY, touch));
                    }
                } else if (gesture.status == 'panning') {
                    trigger(curElement, createPanEvent('pan', offsetX, offsetY, touch));
                }

                if (supportMulti() && gesture.other && gesture.other.status != 'end') {
                    var otherTouch = gesture.other.curTouch,
                        disX = touch.clientX - otherTouch.clientX,
                        disY = touch.clientY - otherTouch.clientY,
                        centerX = (touch.clientX + otherTouch.clientX) / 2,
                        centerY = (touch.clientY + otherTouch.clientY) / 2,
                        distance = computeDistance(disX, disY);

                    // 判断 pinch
                    if (!curVetor.pinch) {
                        if (Math.abs(curVetor.distance - distance) > PINCH_DIS) {
                            curVetor.pinch = true;
                            trigger(curElement, createMultiEvent('pinch', centerX, centerY, distance /
                                curVetor.distance, void 0, touch, otherTouch));
                        }
                    } else {
                        trigger(curElement, createMultiEvent('pinch', centerX, centerY, distance /
                            curVetor.distance, void 0, touch, otherTouch));
                    }

                    // 判断 rorate
                    if (!curVetor.deflection) {
                        var rotation = getAngleDiff(disX, disY);
                        if (Math.abs(rotation) > DIRECTION_DEG) {
                            trigger(curElement, createMultiEvent('rotate', centerX, centerY, void 0, rotation, touch, otherTouch));
                            curVetor.deflection = true;
                        }
                    } else {
                        var rotation = getAngleDiff(disX, disY);
                        trigger(curElement, createMultiEvent('rotate', centerX, centerY, void 0, rotation, touch, otherTouch));
                    }

                }

                gesture.curTouch = mixTouchAttr({}, touch);
            }
        });
    }

    // 处理 touchend 事件
    function touchEnd(event) {

        makeArray(event.changedTouches).forEach(function(touch, index) {
            var gesture = gestures[touch.identifier];
            if (gesture) {

                if (gesture.handler) {
                    clearTimeout(gesture.handler);
                    gesture.handler = null;
                }

                if (gesture.status == 'tapping') {
                    trigger(curElement, mixTouchAttr(createEvent('tap'), touch));
                } else if (gesture.status == 'pressing') {
                    if (longTap) {
                        trigger(curElement, mixTouchAttr(createEvent('tap'), touch));
                    }
                    trigger(curElement, mixTouchAttr(createEvent('pressend'), touch));
                } else if (gesture.status == 'panning') {
                    var startTouch = gesture.startTouch,
                        offsetX = touch.clientX - startTouch.clientX,
                        offsetY = touch.clientY - startTouch.clientY,
                        duration = Date.now() - gesture.startMoveTime;
                    trigger(curElement, createPanEvent('panend', offsetX, offsetY, touch, duration));
                    // 判断是否是快速移动
                    if (duration < FLICK_TIMEOUT) {
                        trigger(curElement, createPanEvent('flick', offsetX, offsetY, touch, duration));
                    }
                }

                if (supportMulti() && gesture.other && gesture.other.status != 'end') {
                    var otherTouch = gesture.other.curTouch,
                        disX = touch.clientX - otherTouch.clientX,
                        disY = touch.clientY - otherTouch.clientY,
                        centerX = (touch.clientX + otherTouch.clientX) / 2,
                        centerY = (touch.clientY + otherTouch.clientY) / 2,
                        distance = computeDistance(disX, disY);
                    if (curVetor.pinch) {
                        trigger(curElement, createMultiEvent('pinchend', centerX, centerY, distance /
                            curVetor.distance, void 0, touch, otherTouch));
                    }
                    if (curVetor.deflection) {
                        var rotation = getAngleDiff(disX, disY);
                        trigger(curElement, createMultiEvent('rotatend', centerX, centerY, void 0, rotation, touch, otherTouch));


                    }
                    rotation = 0;
                }

                gesture.status = 'end';
            }

        });

        if (checkEnd()) {
            for (var key in gestures) {
                delete gestures[key];
            }
        }
    }

    addEvent(body, 'touchstart', touchStart);
    addEvent(body, 'touchmove', touchMove);
    addEvent(body, 'touchend', touchEnd);

    addEvent(body, 'tap', function(ev) {
        var now = Date.now();
        if (now - lastTapTime < DOUBLETAP_GAP) {
            trigger(curElement, mixTouchAttr(createEvent('doubletap'), ev));
            lastTapTime = 0;
        } else {
            lastTapTime = now;
        }
    });
});

var yGesture = {
    enable: function() {
        enabled= true;
    },
    disable: function() {
        enabled = false;
    },
    disableLongTap: function() {
        longTap = false;
    }
};