/**
* myHand.js
* by name:libin
*/
"use strict";
(function(root, factory) {
if(typeof define === "function"&& define. Amd) {//AMD specification define([],function() {
return factory(root);
});
} else{ root.myHand=root.Toucher = factory(root); // Mount it to the window object}}(window,function(root, undefined) {
if(!"ontouchstart" in window) {
return; } var _wrapped; // Get the class name on the objectfunction _typeOf(obj) {
returnObject.prototype.toString.call(obj).toLowerCase().slice(8, -1); } // Get the current timestamp of 1970/1/1function getTimeStr() {
return+(new Date()); } // Get location informationfunction getPosInfo(ev) {
var _touches = ev.touches;
if(! _touches || _touches.length === 0) {return;
}
return{ pageX: ev.touches[0].pageX, pageY: ev.touches[0].pageY, clientX: ev.touches[0].clientX || 0, clientY: ev.touches[0].clientY || 0 }; } // Bind eventsfunction bindEv(el, type, fn) {
if(el.addEventListener) {
el.addEventListener(type, fn, false);
} else {
el["on" + type] = fn; }} // Unbind eventsfunction unBindEv(el, type, fn) {
if(el.removeEventListener) {
el.removeEventListener(type, fn, false);
} else {
el["on" + type] = fn; }} // Get the sliding directionfunction getDirection(startX, startY, endX, endY) {
var xRes = startX - endX;
var xResAbs = Math.abs(startX - endX);
var yRes = startY - endY;
var yResAbs = Math.abs(startY - endY);
var direction = "";
if(xResAbs >= yResAbs && xResAbs > 25) {
direction = (xRes > 0) ? "Right" : "Left";
} else if(yResAbs > xResAbs && yResAbs > 25) {
direction = (yRes > 0) ? "Down" : "Up";
}
returndirection; } // Get the straight line distance between two pointsfunction getDistance(startX, startY, endX, endY) {
return Math.sqrt(Math.pow((startX - endX), 2) + Math.pow((startY - endY), 2));
}
function getLength(pos) {
return Math.sqrt(Math.pow(pos.x, 2) + Math.pow(pos.y, 2));
}
function cross(v1, v2) {
returnv1.x * v2.y - v2.x * v1.y; } // orientationfunction getVector(startX, startY, endX, endY) {
return(startX * endX) + (startY * endY); } / / Angle for a * b = | | | | * b * cos (deg); a*b=x1*x2+y1*y2function getAngle(v1, v2) {
var mr = getLength(v1) * getLength(v2);
if(mr === 0) {
return0}; var r = getVector(v1.x, v1.y, v2.x, v2.y) / mr;if(r > 1) {
r = 1;
}
returnMath.acos(r); } // Get the rotation Angle, not radiansfunction getRotateAngle(v1, v2) {
var angle = getAngle(v1, v2);
if(cross(v1, v2) > 0) {
angle *= -1;
}
returnangle * 180 / Math.PI; } // Wrap a new event objectfunction wrapEvent(ev, obj) {
var res = {
touches: ev.touches,
type: ev.type
};
if(_typeOf(obj) === "object") {
for(var i inobj) { res[i] = obj[i]; }}returnres; } // Convert a pseudo-array to an arrayfunction toArray(list) {
if(list && (typeof list === "object") && isFinite(list.length) && (list.length >= 0) && (list.length === Math.floor(list.length)) && list.length < 4294967296) {
return[].slice.call(list); }} // Check if there are multiple elements in a list of elementsfunction isContain(collection, el) {
if(arguments.length === 2) {
return collection.some(function(elItem) {
return el.isEqualNode(elItem);
});
}
return false; } // Generate a random IDfunction uId() {
returnMath.random().toString(16).slice(2); } var Event = (function() {
var storeEvents = {};
return {
// add an event handle
add: function(type, el, handler) {
var selector = el,
len = arguments.length,
finalObject = {},
_type;
/**
* Event.add("swipe".function() {* //... *}); * /if(_typeOf(el) === "string") {
el = document.querySelectorAll(el);
}
if(len === 2 && _typeOf(el) === "function") {
finalObject = {
handler: el
};
} else if(len === 3 && el instanceof HTMLElement || el instanceof NodeList && _typeOf(handler) === "function") {
/**
* Event.add("swipe"."#div".function(ev) { * // ... *}); */ _type = _typeOf(el); finalObject = {type: _type,
selector: selector,
el: _type === "nodelist" ? toArray(el) : el,
handler: handler
};
}
if(! storeEvents[type]) {
storeEvents[type] = [];
}
storeEvents[type].push(finalObject);
},
// remove an event handle
remove: function(type, selector) {
var len = arguments.length;
if(_typeOf(type) = = ="string" && _typeOf(storeEvents[type= = =])"array" && storeEvents[type].length) {
if(len === 1) {
storeEvents[type] = [];
} else if(len === 2) {
storeEvents[type] = storeEvents[type].filter(function(item) {
return! (item.selector === selector || _typeOf(selector) ! = ="string" && item.selector.isEqualNode(selector));
});
}
}
},
// trigger an event handle
trigger: function(type, el, argument) {
var len = arguments.length;
/**
* Event.trigger("swipe", document.querySelector("#div"), {* //... *}); * /if(len === 3 && _typeOf(storeEvents[type= = =])"array" && storeEvents[type].length) {
storeEvents[type].forEach(function(item) {
if(_typeOf(item.handler) === "function") {
if(item.type && item.el) {
argument.target = el;
if(item.type === "nodelist" && isContain(item.el, el)) {
item.handler(argument);
} else if(item.el.isEqualNode && item.el.isEqualNode(el)) { item.handler(argument); }}else{ item.handler(argument); }}}); }}}; }) (); // constructorfunction Toucher(selector) {
returnnew Toucher.fn.init(selector); } Toucher. Fn = Toucher. Prototype = {constructor: Toucher, //function(selector) {
this.el = selector instanceof HTMLElement ? selector :
_typeOf(selector) === "string" ? document.querySelector(selector) : null;
if(_typeOf(this.el) === "null") {// Throw new Error if no match is found ("You must specify a particular selector or a particular DOM object.");
}
this.scale = 1;
this.pinchStartLen = null;
this.isDoubleTap = false;
this.triggedSwipeStart = false;
this.triggedLongTap = false; this.delta = null; this.last = null; this.now = null; this.tapTimeout = null; this.singleTapTimeout = null; this.longTapTimeout = null; this.swipeTimeout = null; this.startPos = {}; this.endPos = {}; this.preTapPosition = {}; this.cfg = { doubleTapTime: 400, longTapTime: 700 }; // Bind 4 eventsbindEv(this.el, "touchstart", this._start.bind(this));
bindEv(this.el, "touchmove", this._move.bind(this));
bindEv(this.el, "touchcancel", this._cancel.bind(this));
bindEv(this.el, "touchend", this._end.bind(this));
returnthis; }, // provide the config method to configure config:function(option) {
if(_typeOf(option) ! = ="object") {
throw new Error("Option must be a JSON instance object" + option.toString());
}
for(var i in option) {
this.cfg[i] = option[i];
}
returnthis; } /** * var toucher = toucher ({... }); * * toucher.on("swipe".function(ev) { * // ... *}); * * // or * * toucher.on("tap"."#id".function(ev) { * // ... *}); * * support events: singleTap,longTap,swipe,swipeStart,swipeEnd,swipeUp,swipeRight,swipeDown,swipeLeft,pinch,rotate * */ on:function(type, el, callback) {
var len = arguments.length;
if(len === 2) {
Event.add(type, el);
} else {
Event.add(type, el, callback);
}
returnthis; }, /** * var toucher = toucher ({... }); * toucher.off(type);
*
* // or
*
* toucher.off(type, selector);
*/
off: function(type, selector) {
Event.remove(type, selector);
returnthis; }, // just touch the screen _start:function(ev) {
if(! ev.touches || ev.touches.length === 0) {return; } var self = this; var otherToucher, v, preV = this.preV, target = ev.target; // Get the target element self.now = getTimeStr(); // Get the current timestamp 1970/1/1 self.startPos = getPosInfo(ev); / / click on the coordinates of the location information self. The delta = self. Now - (self. The last | | self. Now); // Calculate the interval self.triggedSwipeStart =false;
self.triggedLongTap = false; // Quick double clickif(JSON.stringify(self.preTapPosition).length > 2 && self.delta < self.cfg.doubleTapTime && getDistance(self.preTapPosition.clientX, self.preTapPosition.clientY, self.startPos.clientX, Self.startpos. ClientY) < 25) {self.isdoubletap = self.isdoubletap = self.isdoubletap = self.isdoubletap = self.isdoubletap = self.isdoubletap =true; } // Hold the timer self.longTapTimeout =setTimeout(function() {
_wrapped = {
el: self.el,
type: "longTap",
timeStr: getTimeStr(),
position: self.startPos
};
Event.trigger("longTap", target, _wrapped);
self.triggedLongTap = true; }, self.cfg.longTapTime); // Multiple fingers on the screenif(ev.touches.length > 1) {
self._cancelLongTap();
otherToucher = ev.touches[1];
v = {
x: otherToucher.pageX - self.startPos.pageX,
y: otherToucher.pageY - self.startPos.pageY
};
this.preV = v;
self.pinchStartLen = getLength(v);
self.isDoubleTap = false; } self.last = self.now; self.preTapPosition = self.startPos; Ev.preventdefault (); // Save the last click ev.preventdefault (); }, // move your finger on the screen _move:function(ev) {
if(! ev.touches || ev.touches.length === 0) {return; } var v, otherToucher; var self = this; var len = ev.touches.length; var posNow = getPosInfo(ev); var preV = self.preV; var currentX = posNow.pageX; var currentY = posNow.pageY; var target = ev.target; // Move the finger to cancel the long press event and double-click self._cancellongtap (); self.isDoubleTap =false; // swipeStart is triggered only once after a press liftif(! self.triggedSwipeStart) { _wrapped = { el: self.el,type: "swipeStart",
timeStr: getTimeStr(),
position: posNow
};
Event.trigger("swipeStart", target, _wrapped);
self.triggedSwipeStart = true;
} else {
_wrapped = {
el: self.el,
type: "swipe",
timeStr: getTimeStr(),
position: posNow
};
Event.trigger("swipe", target, _wrapped);
}
if(len > 1) { otherToucher = ev.touches[1]; v = { x: otherToucher.pageX - currentX, y: otherToucher.pageY - currentY }; // _wrapped = wrapEvent(ev, {el: self.el,type: "pinch",
scale: getLength(v) / this.pinchStartLen,
timeStr: getTimeStr(),
position: posNow
});
Event.trigger("pinch", target, _wrapped); // Wrap = wrapEvent(ev, {el: self.el,type: "rotate",
angle: getRotateAngle(v, preV),
timeStr: getTimeStr(),
position: posNow
});
Event.trigger("rotate", target, _wrapped); ev.preventDefault(); } self.endPos = posNow; }, // Touch cancel _cancel:function(ev) { clearTimeout(this.longTapTimeout); clearTimeout(this.tapTimeout); clearTimeout(this.swipeTimeout); clearTimeout(self.singleTapTimeout); }, // Finger off the screen _end:function(ev) {
if(! ev.changedTouches) {return; } // Cancel long press this._cancellongtap (); var self = this; var direction = getDirection(self.endPos.clientX, self.endPos.clientY, self.startPos.clientX, self.startPos.clientY); var callback, target = ev.target;if(direction ! = ="") {
self.swipeTimeout = setTimeout(function() {
_wrapped = wrapEvent(ev, {
el: self.el,
type: "swipe",
timeStr: getTimeStr(),
position: self.endPos
});
Event.trigger("swipe", target, _wrapped); SwipeXyz callback = self["swipe" + direction];
_wrapped = wrapEvent(ev, {
el: self.el,
type: "swipe" + direction,
timeStr: getTimeStr(),
position: self.endPos
});
Event.trigger(("swipe" + direction), target, _wrapped);
_wrapped = wrapEvent(ev, {
el: self.el,
type: "swipeEnd",
timeStr: getTimeStr(),
position: self.endPos
});
Event.trigger("swipeEnd", target, _wrapped);
}, 0);
} else if(! self.triggedLongTap) { self.tapTimeout =setTimeout(function() {
if(self.isDoubleTap) {
_wrapped = wrapEvent(ev, {
el: self.el,
type: "doubleTap",
timeStr: getTimeStr(),
position: self.startPos
});
Event.trigger("doubleTap", target, _wrapped);
clearTimeout(self.singleTapTimeout);
self.isDoubleTap = false;
} else {
self.singleTapTimeout = setTimeout(function() {
_wrapped = wrapEvent(ev, {
el: self.el,
type: "singleTap",
timeStr: getTimeStr(),
position: self.startPos
});
Event.trigger("singleTap", target, _wrapped); }, 100); }}, 0); } this.startPos = {}; this.endPos = {}; }, // Cancel the long press timer _cancelLongTap:function() {
if(_typeOf(this.longTapTimeout) ! = ="null") { clearTimeout(this.longTapTimeout); }}}; Toucher.fn.init.prototype = Toucher.fn; // No new implementationreturn Toucher;
}));
Copy the code
The DEMO:
<! DOCTYPE html> <html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="Width = device - width, target - densitydpi = high - dpi, initial - scale = 1.0, the minimum - scale = 1.0, the maximum - scale = 1.0, user - scalable = no" />
<title></title>
<style type="text/css">
* {
margin: 0;
padding: 0;
}
#toucher {
width: 100%;
height: 400px;
background: yellow;
}
</style>
</head>
<body>
<div id="toucher">
</div>
<div id="result"></div>
<div></div>
<script src=""></script>
<script>
window.onload = function() {
var toucher = Toucher("#toucher");
var result = document.querySelector("#result");
toucher.on("singleTap"."#toucher".function(e) {
result.innerHTML = e.type;
})
.on("doubleTap".function(e) {
result.innerHTML = e.type;
})
.on("longTap".function(e) {
result.innerHTML = e.type;
})
.on("swipe".function(e) {
result.innerHTML = e.type;
})
.on("swipeStart".function(e) {
result.innerHTML = e.type;
})
.on("swipeEnd".function(e) {
result.innerHTML = e.type;
})
.on("swipeUp".function(e) {
result.innerHTML = e.type;
})
.on("swipeRight".function(e) {
result.innerHTML = e.type;
})
.on("swipeDown".function(e) {
result.innerHTML = e.type;
})
.on("swipeLeft".function(e) {
result.innerHTML = e.type;
})
.on("rotate".function(e) {
result.innerHTML = e.type + " angle " + e.angle;
})
.on("pinch".function(e) {
result.innerHTML = e.type + " scale " + e.scale;
});
}
</script>
</body>
</html>
Copy the code
Compatible code, some weird phones do not support E.touches, can be added to the top library file at line 36:
// touches
function fnTouches(e) {
if(!e.touches) {
e.touches = e.originalEvent.touches;
}
}
Copy the code
For the summing-up analysis of gesture library, you can see the author’s own article, ultra-small Web gesture library AlloyFinger principle
What I’ve written is probably more about implementation details, how to implement a specific feature, rather than this overall architectural analysis, where capabilities are limited by the big picture.
Tag
Point-2: How to realize the judgment of rotation operation, how to calculate the included Angle of rotation and the direction of rotation point-3: specific gesture, pseudo-code logic
Code architecture approach
var AlloyFinger = function(el, option) {// Get the el element // change this of start, move, end, cancel to this. Start = this.start.bind(this); Touchmove, touchend, } AlloyFinger. Prototype = {start:function() {... }, move:function() {... }, end:function() {... }, cancel:function() {... }}Copy the code
In general, all the properties and methods are mounted under the AlloyFinger object.
Each of the four phases of the Touch native event is bound to its own response function, in which a series of judgments are made to determine whether to trigger a gesture.
Can I say that at first glance the code feels like a 6? :). Or their ability is too slag, a lot of things are still lacking. For example, how would a novice like me implement these features if I hadn’t seen the source code? The response function can only be bound to one of the library of possible written gestures. Then when the gesture is triggered, call func() directly. Inside the logic of the gesture judgment, but also slag of a…
Anonymous immediate execution of functions preceded by a semicolon
; (function() {... }) ()Copy the code
I didn’t pay attention to it before, but if YOU look at the answers on the Internet, basically, you don’t have a semicolon on the front line when you compress it, you don’t have a semicolon on the back line when you merge it.
Judge the direction and Angle of the rotation gesture
A vector is A (x1, y1), B (x2, y2) vector dot product: a. B = x1 * * x2 + y1 y2 = | | | | * B * cos theta vector difference by: A x B = x1 * * y1 y2 – x2 = | | | | * B * sine theta
How do you calculate the rotation Angle and direction when you rotate the image 🙂
Angle: by the dot product formula, can export equation cosine theta = x1 * * x2 + y1 y2 / (| | | | * b), then use Math. A cosine function can get theta. And for the length of vectors A and B, the Pythagorean theorem solves.
Direction of rotation:
- For the coordinate axes, the counterclockwise direction is the positive direction.
- A, x, B, represents the rotation of vector A to vector B
- If the difference product value is > 0, rotate counterclockwise and take -θ. If the difference product value is < 0, rotate clockwise and take -θ.
- The rotation Angle is converted to radians
Refer to the article
To sum up, both dot product and cross product are used in rotation operation to obtain the size and direction of rotation Angle respectively. GetLen (), dot(), getAngle(), cross(), and getRotateAngle() are basically fine
Bind multiple response functions
It is interesting to see the implementation of HandlerAdmin, similar to the observer pattern. Use an array to store all the response functions of this gesture, and add, del, and Dispatch methods to add, delete, and trigger the response functions.
Use of marker
this.delta = null; This. last = null; // When last clicked, touchStart time this.now = null; This. PreTapPosition = {x: null, y: null}; // This. IsDoubleTap =false; This. tapTimeout = null; this.tapTimeout = null; this.tapTimeout = null; this.singleTapTimeout = null; this.longTapTimeout = null; this.swipeTimeout = null; X1 = this.x2 = this.y1 = this.y2 = null; x1 = this.y2 = this.y2 = null; This.prev = {x: null, y: null}; this.prev = {x: null, y: null}; this.pinchStartLen = null; This. scale = 1; // ScaleCopy the code
Signal analysis
Start: 1. Obtain the current time and location and calculate the time difference. 2. If there is preTapPosition 3.1 To judge the time difference interval, click the area twice 3.2 to set the double click flag 4. Set the current point to preTapPosition 5. Determine whether to multi-touch 5.1 Calculate the vector preV distance connected by two points 5.2 Dispatch multipointStart event 6. CurrentX, currentY Obtains the current move position and sets the temporary variable preV 3. After judging that multi-touch is 3.1.1 move, two points form vector V, Calculate the V distance 3.1.2 Calculate the zoom ratio based on preV and V 3.1.3 Dispatch scale event 3.1.4 Calculate the rotation Angle Dispatch Rotate Event No 3.2.1 Calculate deltaX DeltaY 3.2.3 Dispatch pressMove event 4. Clear longTap timer 5. Set end point x2, y2 end: 1. Length <2, dispatch multipointEnd event 3. Dispatch touchEnd event 4. Through the starting point x1, y1; End point X2, y2, calculated distance greater than 30, Calculate swipe direction by responding to Swipe 4.1.1 4.1.2 Set swipe timer dispatch to swipe event distance less than 30, Setting the TAP timer 4.2.1 Dispatch tap event 4.2.2 Checking isDoubleTap, is 1. Dispatch doubleTap event 2. Clear singleTap timer No 1. Set singleTap timer to dispatch singleTap events 5. this.prev. x = 0; this.preV.y = 0; this.scale = 1; this.pinchStartLen = null; this.x1 = this.x2 = this.y1 = this.y2 = null; Cancel: 1. Clear longTap, singleTap, Swipe, tap timers 2. Dispatch the TouchCancel eventCopy the code
Timeout with 0 event intervals
setTimeout(function () {
console.log('Timeout start runing... ');
}, 0)
for(var i=0; i<5; i++) {
console.log(i);
}Copy the code
Results:
0
1
2
3
4
Timeout start runing...Copy the code
Therefore, if timeout is 0, it does not mean that cancel cannot be cancelled. If you don’t understand, you can read this book, asynchronous programming in javascript