javascript 常用手勢, 個人覺得有3個 tap,swipe(swipeLeft,swipeRight,swipeTop,swipeRight),hold
tap 是輕擊 判斷的原則是,在toustart后,移動范圍不超過10px(圓的范圍),就算是 輕擊了
swipe 是輕滑(輕掃) 判斷在toustart后,時間間隔小於300ms,移動范圍大於20,就判斷是輕滑
hold(常按) 按住 移動范圍小於10px,時間大於200ms,就認為他是hold
自定義手勢,網上相關的源碼很多,我也找了一個來研究,叫touch.js,挺不錯的支持pc端,移動端(移動端就是touchstart,pc端就是mousedown),雖然有些小bug,比如事件刪除有問題
touch.js的地址 http://touch.code.baidu.com/
一些要准備的基礎
1.對touch相關的東西要了解
指尖上的js是很好的東西呀
2.自定義事件CustomEvent
dom是添加自定義事件的,也可以觸發它,它還以冒泡
火狐的一個官方說明 官方說明
一篇比較詳細的介紹,還有例子 點點點
自定義事件是可以用chrome看到的,如圖
手勢的基本實現原理
tap,hold,swipe都是js沒有的事件,都是由,touchstart,touchmove,touchend touchcancel這些事件組合而成的
實現原理就是通過綁定document的”touchstart touchmove touchend touchcancel“事件
當touch到元素,查看手勢是否符合tap,swipe的原則
如果符合原則,就觸發元素綁定的相關事件
判斷移動了多少位置
比如我點擊了元素a,我就就得記下點擊時的位置,計算方式如下
touches[0].pageX,touches[0].pageY
這個是手指點擊的位置離頁面頂端的位置(或者是頁面的左邊)
然后再touchmove時記下相關的位置
在touchend或者touchcancel時,用2個數據,算一下移動了多少就行了
ps:touchend和touchcancel是沒有event的所以必須在touchmove里面記錄位置
閹割源碼解析
touch.js 以我的水平來看,並不能很流暢的閱讀源碼...
而且有些手勢也不是很常用,做了些閹割,寫了些注釋,方便理解
<html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> <title>wo ca!~</title> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="format-detection" content="telephone=no"> </head> <style> .xx{width: 200px;background: #ccc; height: 100px;} </style> <body> <div id="vv" class="xx"></div> <br> <div id="ss" class="xx"></div> <br> <div id="ss1" class="xx">1</div> <br> <div id="ss2" class="xx">1</div> <br> <div id="ss3" class="xx"></div> <br> <div id="ss4" class="xx"></div> <script> (function(){ var utils = {}; //獲取元素的點擊位置 utils.getPosOfEvent = function(ev){ var posi = []; var src = null; for (var t = 0, len = ev.touches.length; t < len; t++) { src = ev.touches[t]; posi.push({ x: src.pageX, y: src.pageY }); } return posi; } utils.getType = function(obj) { return Object.prototype.toString.call(obj).match(/\s([a-z|A-Z]+)/)[1].toLowerCase(); }; //獲取點擊的手指數量 utils.getFingers = function(ev) { return ev.touches ? ev.touches.length : 1; }; utils.isTouchMove = function(ev) { return ev.type === 'touchmove'; }; //是否已經結束了手勢 utils.isTouchEnd = function(ev) { return (ev.type === 'touchend' || ev.type === 'touchcancel'); }; //算2點之間的距離 utils.getDistance = function(pos1, pos2) { var x = pos2.x - pos1.x, y = pos2.y - pos1.y; return Math.sqrt((x * x) + (y * y)); }; //算角度 utils.getAngle = function(p1, p2) { return Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI; }; //根據角度 返回up down left right utils.getDirectionFromAngle = function(agl) { var directions = { up: agl < -45 && agl > -135, down: agl >= 45 && agl < 135, left: agl >= 135 || agl <= -135, right: agl >= -45 && agl <= 45 }; for (var key in directions) { if (directions[key]) return key; } return null; }; utils.reset = function() { startEvent = moveEvent = endEvent = null; __tapped = __touchStart = startSwiping = false; pos = {start: null,move: null,end: null}; }; //ua utils.env = (function() { var os = {}, ua = navigator.userAgent, android = ua.match(/(Android)[\s\/]+([\d\.]+)/), ios = ua.match(/(iPad|iPhone|iPod)\s+OS\s([\d_\.]+)/), wp = ua.match(/(Windows\s+Phone)\s([\d\.]+)/), isWebkit = /WebKit\/[\d.]+/i.test(ua), isSafari = ios ? (navigator.standalone ? isWebkit : (/Safari/i.test(ua) && !/CriOS/i.test(ua) && !/MQQBrowser/i.test(ua))) : false; if (android) { os.android = true; os.version = android[2]; } if (ios) { os.ios = true; os.version = ios[2].replace(/_/g, '.'); os.ios7 = /^7/.test(os.version); if (ios[1] === 'iPad') { os.ipad = true; } else if (ios[1] === 'iPhone') { os.iphone = true; os.iphone5 = screen.height == 568; } else if (ios[1] === 'iPod') { os.ipod = true; } } if (isWebkit) { os.webkit = true; } if (isSafari) { os.safari = true; } return os; })(); //已配置 tap hold swipe表示是否開啟手勢 //tapTime tap事件延遲觸發的時間 //holdTime hold事件多少秒后觸發 //tapMaxDistance 觸發tap的時候 最小的移動范圍 //swipeMinDistance 觸發swipe的時候 最小的移動范圍 //swipeTime touchstart 到touchend之前的時間 如果小於swipeTime 才會觸發swipe手勢 var config = { tap: true, tapMaxDistance: 10, hold: true, tapTime: 200, holdTime: 650, swipe: true, swipeTime: 300, swipeMinDistance: 18 }; var smrEventList = { TOUCH_START: 'touchstart', TOUCH_MOVE: 'touchmove', TOUCH_END: 'touchend', TOUCH_CANCEL: 'touchcancel', SWIPE_START: 'swipestart', SWIPING: 'swiping', SWIPE_END: 'swipeend', SWIPE_LEFT: 'swipeleft', SWIPE_RIGHT: 'swiperight', SWIPE_UP: 'swipeup', SWIPE_DOWN: 'swipedown', SWIPE: 'swipe', HOLD: 'hold', TAP: 'tap', }; /** 手勢識別 */ //記錄 開始 移動 結束時候的位置 var pos = { start: null, move: null, end: null }; var __touchStart = true; var __tapped; var __prev_tapped_end_time; var __prev_tapped_pos; var __holdTimer = null; var startTime; var startEvent; var moveEvent; var endEvent; var startSwiping; var gestures = { swipe: function(ev) { var el = ev.target; if (!__touchStart || !pos.move || utils.getFingers(ev) > 1) { return; } //計算 時間 距離 角度 var now = Date.now(); var touchTime = now - startTime; var distance = utils.getDistance(pos.start[0], pos.move[0]); var angle = utils.getAngle(pos.start[0], pos.move[0]); var direction = utils.getDirectionFromAngle(angle); var touchSecond = touchTime / 1000; var eventObj = { type: smrEventList.SWIPE, originEvent: ev, direction: direction, distance: distance, distanceX: pos.move[0].x - pos.start[0].x, distanceY: pos.move[0].y - pos.start[0].y, x: pos.move[0].x - pos.start[0].x, y: pos.move[0].y - pos.start[0].y, angle: angle, duration: touchTime, fingersCount: utils.getFingers(ev) }; if (config.swipe) { var swipeTo = function() { var elt = smrEventList; switch (direction) { case 'up': engine.trigger(el, elt.SWIPE_UP, eventObj); break; case 'down': engine.trigger(el, elt.SWIPE_DOWN, eventObj); break; case 'left': engine.trigger(el, elt.SWIPE_LEFT, eventObj); break; case 'right': engine.trigger(el, elt.SWIPE_RIGHT, eventObj); break; } }; if (!startSwiping) { eventObj.fingerStatus = eventObj.swipe = 'start'; //大於tap的最小距離 才算進入swipe手勢 if(distance>config.tapMaxDistance){ startSwiping = true; } } else if (utils.isTouchMove(ev)) { eventObj.fingerStatus = eventObj.swipe = 'move'; engine.trigger(el, smrEventList.SWIPING, eventObj); } else if (utils.isTouchEnd(ev)) { eventObj.fingerStatus = eventObj.swipe = 'end'; //事件要短 距離要有點遠 if (config.swipeTime > touchTime && distance > config.swipeMinDistance) { swipeTo(); engine.trigger(el, smrEventList.SWIPE, eventObj, false); } } } }, tap : function(ev){ var el = ev.target; //如果設置了tap為true 才會觸發該手勢 if (config.tap) { var now = Date.now(); var touchTime = now - startTime; var distance = utils.getDistance(pos.start[0], pos.move ? pos.move[0] : pos.start[0]); clearTimeout(__holdTimer); //如果移動的距離比設置的距離大(10) 就不算是tap if (config.tapMaxDistance < distance) return; __tapped = true; __prev_tapped_end_time = now; __prev_tapped_pos = pos.start[0]; __tapTimer = setTimeout(function() { engine.trigger(el, smrEventList.TAP, { type: smrEventList.TAP, originEvent: ev }); }, config.tapTime); } }, hold: function(ev) { var el = ev.target; //如果設置了hold為true 才會觸發該手勢 if (config.hold) { clearTimeout(__holdTimer); __holdTimer = setTimeout(function() { if (!pos.start) return; var distance = utils.getDistance(pos.start[0], pos.move ? pos.move[0] : pos.start[0]); //如果移動的距離大於配置的距離(10) 就不觸發hold if (config.tapMaxDistance < distance) return; if (!__tapped) { engine.trigger(el, "hold", { type: 'hold', originEvent: ev, fingersCount: utils.getFingers(ev), position: pos.start[0] }); } }, config.holdTime); } } } /** 底層事件綁定/代理支持 */ var engine = { proxyid: 0, proxies: [], trigger : function(el, evt, detail){ detail = detail || {}; var e, opt = { bubbles: true, cancelable: true, detail: detail }; try { //這里是觸發 自定義事件 if (typeof CustomEvent !== 'undefined') { e = new CustomEvent(evt, opt); if (el) { el.dispatchEvent(e); } } else { e = document.createEvent("CustomEvent"); e.initCustomEvent(evt, true, true, detail); if (el) { el.dispatchEvent(e); } } } catch (ex) { console.warn("Touch.js is not supported by environment."); } }, bind: function(el, evt, handler) { el.listeners = el.listeners || {}; //proxy才是真正元素綁定的事件 var proxy = function(e) { //對ios7的一個兼容 也不知道是什么原理 if (utils.env.ios7) { utils.forceReflow(); } e.originEvent = e; for (var p in e.detail) { if (p !== 'type') { e[p] = e.detail[p]; } } var returnValue = handler.call(e.target, e); if (typeof returnValue !== "undefined" && !returnValue) { e.stopPropagation(); e.preventDefault(); } }; if (!el.listeners[evt]) { el.listeners[evt] = [proxy]; } else { el.listeners[evt].push(proxy); } handler.proxy = handler.proxy || {}; if (!handler.proxy[evt]) { handler.proxy[evt] = [this.proxyid++]; } else { handler.proxy[evt].push(this.proxyid++); } this.proxies.push(proxy); if (el.addEventListener) { el.addEventListener(evt, proxy, false); } }, unbind : function(el, evt){ var handlers = el.listeners[evt]; if (handlers && handlers.length) { handlers.forEach(function(handler) { el.removeEventListener(evt, handler, false); }); } } } var _on = function(el,evt,handler) { //綁定事件 支持多元素 多事件綁定噢 var evts = evt.split(" "); var els = utils.getType(el) === 'string' ? document.querySelectorAll(el) : [el]; evts.forEach(function(evt) { for(var i=0,len=els.length;i<len;i++){ engine.bind(els[i], evt, handler); } }); }; var _off = function(els,evts,handler) { //刪除綁定事件 支持多元素 多事件刪除綁定噢 var els = utils.getType(els) === 'string' ? document.querySelectorAll(els) : els; els = els.length ? Array.prototype.slice.call(els) : [els]; els.forEach(function(el) { evts = evts.split(" "); evts.forEach(function(evt) { engine.unbind(el, evt, handler); }); }); return; }; //這個函數很重要 // doucment的觸屏事件全部在這個里面 var handlerOriginEvent = function(ev) { var el = ev.target; switch (ev.type) { case 'touchstart': //記錄下剛開始點擊的事件和位置 __touchStart = true; if (!pos.start || pos.start.length < 2) { pos.start = utils.getPosOfEvent(ev); } startTime = Date.now(); startEvent = ev; gestures.hold(ev); break; case 'touchmove': if (!__touchStart || !pos.start) return; //記錄滑動過程中的位置 pos.move = utils.getPosOfEvent(ev); gestures.swipe(ev); break; case 'touchend': case 'touchcancel': if (!__touchStart) return; endEvent = ev; //....... if (startSwiping) { gestures.swipe(ev); } else { gestures.tap(ev); } utils.reset(); if (ev.touches && ev.touches.length === 1) { __touchStart = false; } break; } } var init = function(){ //給 document 綁定 下面這些事件 var touchEvents = 'touchstart touchmove touchend touchcancel'; touchEvents.split(" ").forEach(function(evt) { document.addEventListener(evt, handlerOriginEvent, false); }); } init(); window.touch = { on : _on, off : _off }; })(); touch.on("#vv","tap",function(){ ss1.innerHTML = ~~ss1.innerHTML+1; }); touch.on("#vv","swipeleft",function(){ ss2.innerHTML = ~~ss2.innerHTML+1; }); touch.on("#vv","swiperight",function(){ ss2.innerHTML = ~~ss2.innerHTML-1; }); touch.on("#vv","hold",function(){ ss.innerHTML = ~~ss.innerHTML+1; }); </script> </body> </html>
支持移動端 pc端的閹割源碼解析
taobao的頁面在ipad上,圖片的輪詢是支持手勢滑動的,天涯的也一樣,在pc就支持click了,所以手勢封裝也是支持pc和移動端才好
<html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> <title>wo ca!~</title> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="format-detection" content="telephone=no"> </head> <style> .xx{width: 200px;background: #ccc; height: 100px;} </style> <body> <div id="vv" class="xx"></div> <br> <div id="ss" class="xx"></div> <br> <div id="ss1" class="xx">1</div> <br> <div id="ss2" class="xx">1</div> <br> <div id="ss3" class="xx"></div> <br> <div id="ss4" class="xx"></div> <script> (function(){ var utils = {}; //獲取元素的點擊位置 utils.getPosOfEvent = function(ev){ if (this.hasTouch) { var posi = []; var src = null; for (var t = 0, len = ev.touches.length; t < len; t++) { src = ev.touches[t]; posi.push({ x: src.pageX, y: src.pageY }); } return posi; } else { return [{ x: ev.pageX, y: ev.pageY }]; } } utils.hasTouch = ('ontouchstart' in window); utils.PCevts = { 'touchstart': 'mousedown', 'touchmove': 'mousemove', 'touchend': 'mouseup', 'touchcancel': 'mouseout' }; utils.getPCevts = function(evt) { return this.PCevts[evt] || evt; }; utils.getType = function(obj) { return Object.prototype.toString.call(obj).match(/\s([a-z|A-Z]+)/)[1].toLowerCase(); }; //獲取點擊的手指數量 utils.getFingers = function(ev) { return ev.touches ? ev.touches.length : 1; }; utils.isTouchMove = function(ev) { return (ev.type === 'touchmove' || ev.type === 'mousemove'); }; //是否已經結束了手勢 utils.isTouchEnd = function(ev) { return (ev.type === 'touchend' || ev.type === 'mouseup' || ev.type === 'touchcancel'); }; //算2點之間的距離 utils.getDistance = function(pos1, pos2) { var x = pos2.x - pos1.x, y = pos2.y - pos1.y; return Math.sqrt((x * x) + (y * y)); }; //算角度 utils.getAngle = function(p1, p2) { return Math.atan2(p2.y - p1.y, p2.x - p1.x) * 180 / Math.PI; }; //根據角度 返回up down left right utils.getDirectionFromAngle = function(agl) { var directions = { up: agl < -45 && agl > -135, down: agl >= 45 && agl < 135, left: agl >= 135 || agl <= -135, right: agl >= -45 && agl <= 45 }; for (var key in directions) { if (directions[key]) return key; } return null; }; utils.reset = function() { startEvent = moveEvent = endEvent = null; __tapped = __touchStart = startSwiping = false; pos = {start: null,move: null,end: null}; }; //ua utils.env = (function() { var os = {}, ua = navigator.userAgent, android = ua.match(/(Android)[\s\/]+([\d\.]+)/), ios = ua.match(/(iPad|iPhone|iPod)\s+OS\s([\d_\.]+)/), wp = ua.match(/(Windows\s+Phone)\s([\d\.]+)/), isWebkit = /WebKit\/[\d.]+/i.test(ua), isSafari = ios ? (navigator.standalone ? isWebkit : (/Safari/i.test(ua) && !/CriOS/i.test(ua) && !/MQQBrowser/i.test(ua))) : false; if (android) { os.android = true; os.version = android[2]; } if (ios) { os.ios = true; os.version = ios[2].replace(/_/g, '.'); os.ios7 = /^7/.test(os.version); if (ios[1] === 'iPad') { os.ipad = true; } else if (ios[1] === 'iPhone') { os.iphone = true; os.iphone5 = screen.height == 568; } else if (ios[1] === 'iPod') { os.ipod = true; } } if (isWebkit) { os.webkit = true; } if (isSafari) { os.safari = true; } return os; })(); //已配置 tap hold swipe表示是否開啟手勢 //tapTime tap事件延遲觸發的時間 //holdTime hold事件多少秒后觸發 //tapMaxDistance 觸發tap的時候 最小的移動范圍 //swipeMinDistance 觸發swipe的時候 最小的移動范圍 //swipeTime touchstart 到touchend之前的時間 如果小於swipeTime 才會觸發swipe手勢 var config = { tap: true, tapMaxDistance: 10, hold: true, tapTime: 200, holdTime: 650, swipe: true, swipeTime: 300, swipeMinDistance: 18 }; var smrEventList = { TOUCH_START: 'touchstart', TOUCH_MOVE: 'touchmove', TOUCH_END: 'touchend', TOUCH_CANCEL: 'touchcancel', MOUSE_DOWN: 'mousedown', MOUSE_MOVE: 'mousemove', MOUSE_UP: 'mouseup', CLICK: 'click', PINCH_START: 'pinchstart', PINCH_END: 'pinchend', PINCH: 'pinch', PINCH_IN: 'pinchin', PINCH_OUT: 'pinchout', ROTATION_LEFT: 'rotateleft', ROTATION_RIGHT: 'rotateright', ROTATION: 'rotate', SWIPE_START: 'swipestart', SWIPING: 'swiping', SWIPE_END: 'swipeend', SWIPE_LEFT: 'swipeleft', SWIPE_RIGHT: 'swiperight', SWIPE_UP: 'swipeup', SWIPE_DOWN: 'swipedown', SWIPE: 'swipe', DRAG: 'drag', DRAGSTART: 'dragstart', DRAGEND: 'dragend', HOLD: 'hold', TAP: 'tap', DOUBLE_TAP: 'doubletap' }; /** 手勢識別 */ //記錄 開始 移動 結束時候的位置 var pos = { start: null, move: null, end: null }; var __touchStart = false; var __tapped; var __prev_tapped_end_time; var __prev_tapped_pos; var __holdTimer = null; var startTime=0; var startEvent; var moveEvent; var endEvent; var startSwiping; var gestures = { swipe: function(ev) { var el = ev.target; if (!__touchStart || !pos.move || utils.getFingers(ev) > 1) { return; } //計算 時間 距離 角度 var now = Date.now(); var touchTime = now - startTime; var distance = utils.getDistance(pos.start[0], pos.move[0]); var angle = utils.getAngle(pos.start[0], pos.move[0]); var direction = utils.getDirectionFromAngle(angle); var touchSecond = touchTime / 1000; var eventObj = { type: smrEventList.SWIPE, originEvent: ev, direction: direction, distance: distance, distanceX: pos.move[0].x - pos.start[0].x, distanceY: pos.move[0].y - pos.start[0].y, x: pos.move[0].x - pos.start[0].x, y: pos.move[0].y - pos.start[0].y, angle: angle, duration: touchTime, fingersCount: utils.getFingers(ev) }; if (config.swipe) { var swipeTo = function() { var elt = smrEventList; switch (direction) { case 'up': engine.trigger(el, elt.SWIPE_UP, eventObj); break; case 'down': engine.trigger(el, elt.SWIPE_DOWN, eventObj); break; case 'left': engine.trigger(el, elt.SWIPE_LEFT, eventObj); break; case 'right': engine.trigger(el, elt.SWIPE_RIGHT, eventObj); break; } }; if (!startSwiping) { eventObj.fingerStatus = eventObj.swipe = 'start'; //大於tap的最小距離 才算進入swipe手勢 if(distance>config.tapMaxDistance){ startSwiping = true; } } else if (utils.isTouchMove(ev)) { eventObj.fingerStatus = eventObj.swipe = 'move'; engine.trigger(el, smrEventList.SWIPING, eventObj); } else if (utils.isTouchEnd(ev)|| ev.type === 'mouseout') { eventObj.fingerStatus = eventObj.swipe = 'end'; //事件要短 距離要有點遠 if (config.swipeTime > touchTime && distance > config.swipeMinDistance) { swipeTo(); engine.trigger(el, smrEventList.SWIPE, eventObj, false); } } } }, tap : function(ev){ var el = ev.target; //如果設置了tap為true 才會觸發該手勢 if (config.tap) { var now = Date.now(); var touchTime = now - startTime; var distance = utils.getDistance(pos.start[0], pos.move ? pos.move[0] : pos.start[0]); clearTimeout(__holdTimer); //如果移動的距離比設置的距離大(10) 就不算是tap if (config.tapMaxDistance < distance) return; __tapped = true; __prev_tapped_end_time = now; __prev_tapped_pos = pos.start[0]; __tapTimer = setTimeout(function() { engine.trigger(el, smrEventList.TAP, { type: smrEventList.TAP, originEvent: ev }); }, config.tapTime); } }, hold: function(ev) { var el = ev.target; //如果設置了hold為true 才會觸發該手勢 if (config.hold) { clearTimeout(__holdTimer); __holdTimer = setTimeout(function() { if (!pos.start) return; var distance = utils.getDistance(pos.start[0], pos.move ? pos.move[0] : pos.start[0]); //如果移動的距離大於配置的距離(10) 就不觸發hold if (config.tapMaxDistance < distance) return; if (!__tapped) { engine.trigger(el, "hold", { type: 'hold', originEvent: ev, fingersCount: utils.getFingers(ev), position: pos.start[0] }); } }, config.holdTime); } } } /** 底層事件綁定/代理支持 */ var engine = { proxyid: 0, proxies: [], trigger : function(el, evt, detail){ detail = detail || {}; var e, opt = { bubbles: true, cancelable: true, detail: detail }; try { //這里是觸發 自定義事件 if (typeof CustomEvent !== 'undefined') { e = new CustomEvent(evt, opt); if (el) { el.dispatchEvent(e); } } else { e = document.createEvent("CustomEvent"); e.initCustomEvent(evt, true, true, detail); if (el) { el.dispatchEvent(e); } } } catch (ex) { console.warn("Touch.js is not supported by environment."); } }, bind: function(el, evt, handler) { el.listeners = el.listeners || {}; //proxy才是真正元素綁定的事件 var proxy = function(e) { //對ios7的一個兼容 也不知道是什么原理 if (utils.env.ios7) { utils.forceReflow(); } e.originEvent = e; for (var p in e.detail) { if (p !== 'type') { e[p] = e.detail[p]; } } var returnValue = handler.call(e.target, e); if (typeof returnValue !== "undefined" && !returnValue) { e.stopPropagation(); e.preventDefault(); } }; if (!el.listeners[evt]) { el.listeners[evt] = [proxy]; } else { el.listeners[evt].push(proxy); } handler.proxy = handler.proxy || {}; if (!handler.proxy[evt]) { handler.proxy[evt] = [this.proxyid++]; } else { handler.proxy[evt].push(this.proxyid++); } this.proxies.push(proxy); if (el.addEventListener) { el.addEventListener(evt, proxy, false); } }, unbind : function(el, evt){ var handlers = el.listeners[evt]; if (handlers && handlers.length) { handlers.forEach(function(handler) { el.removeEventListener(evt, handler, false); }); } } } var _on = function(el,evt,handler) { //綁定事件 支持多元素 多事件綁定噢 var evts = evt.split(" "); var els = utils.getType(el) === 'string' ? document.querySelectorAll(el) : [el]; evts.forEach(function(evt) { if (!utils.hasTouch) { evt = utils.getPCevts(evt); } for(var i=0,len=els.length;i<len;i++){ engine.bind(els[i], evt, handler); } }); }; var _off = function(els,evts,handler) { //刪除綁定事件 支持多元素 多事件刪除綁定噢 var els = utils.getType(els) === 'string' ? document.querySelectorAll(els) : els; els = els.length ? Array.prototype.slice.call(els) : [els]; els.forEach(function(el) { evts = evts.split(" "); evts.forEach(function(evt) { if (!utils.hasTouch) { evt = utils.getPCevts(evt); } engine.unbind(el, evt, handler); }); }); return; }; //這個函數很重要 // doucment的觸屏事件全部在這個里面 var handlerOriginEvent = function(ev) { var el = ev.target; switch (ev.type) { case 'mousedown': case 'touchstart': //記錄下剛開始點擊的事件和位置 __touchStart = true; if (!pos.start || pos.start.length < 2) { pos.start = utils.getPosOfEvent(ev); } startTime = Date.now(); startEvent = ev; gestures.hold(ev); break; case 'touchmove': case 'mousemove': if (!__touchStart || !pos.start) return; //記錄滑動過程中的位置 pos.move = utils.getPosOfEvent(ev); gestures.swipe(ev); break; case 'touchend': case 'touchcancel': case 'mouseup': case 'moudeout': if (!__touchStart) return; endEvent = ev; //....... if (startSwiping) { gestures.swipe(ev); } else { gestures.tap(ev); } utils.reset(); if (ev.touches && ev.touches.length === 1) { __touchStart = false; } break; } } var init = function(){ //給 document 綁定 下面這些事件 var mouseEvents = 'mouseup mousedown mousemove', touchEvents = 'touchstart touchmove touchend touchcancel'; var bindingEvents = utils.hasTouch ? touchEvents : mouseEvents; bindingEvents.split(" ").forEach(function(evt) { document.addEventListener(evt, handlerOriginEvent, false); }); } init(); window.touch = { on : _on, off : _off }; })(); touch.on("#vv","tap",function(){ ss1.innerHTML = ~~ss1.innerHTML+1; }); touch.on("#vv","swipeleft",function(){ ss2.innerHTML = ~~ss2.innerHTML+1; }); touch.on("#vv","swiperight",function(){ ss2.innerHTML = ~~ss2.innerHTML-1; }); touch.on("#vv","hold",function(){ ss.innerHTML = ~~ss.innerHTML+1; }); </script> </body> </html>
zepto的手勢源碼解析
zepto自己以移動端的jq自喻,然后提供了一套移動端的手勢
touch的下載地址 https://github.com/madrobby/zepto/blob/master/src/touch.js#files
這個touch的好處就是可以支持jq的事件綁定方式,比如$("#xx").bind("tap",fun),容易理解,容易上手
這個touch的實現方式和百度的touch實現基本是一樣的,document去綁定touchstart,touchmove,touchend,touchcancel然后經過一些列的判斷
去掉ms的兼容 , 去掉一些手勢后的代碼 全部代碼下載地址 https://github.com/madrobby/zepto/blob/master/src/touch.js#files
<html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> <title>wo ca!~</title> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="format-detection" content="telephone=no"> </head> <style> .xx{width: 200px;background: #ccc; height: 100px;} </style> <body> <div id="vv" class="xx a"></div> <br> <div id="ss" class="xx a">1</div> <br> <div id="ss1" class="xx">1</div> <br> <div id="ss2" class="xx">1</div> <br> <script src="http://static.paipaiimg.com/paipai_h5/js/ttj/zepto.min.js"></script> <script > // Zepto.js // (c) 2010-2015 Thomas Fuchs // Zepto.js may be freely distributed under the MIT license. ;(function($){ var touch = {}, touchTimeout, tapTimeout, swipeTimeout, longTapTimeout, longTapDelay = 750, gesture function swipeDirection(x1, x2, y1, y2) { return Math.abs(x1 - x2) >= Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down') } function longTap() { longTapTimeout = null if (touch.last) { touch.el.trigger('longTap') touch = {} } } function cancelLongTap() { if (longTapTimeout) clearTimeout(longTapTimeout) longTapTimeout = null } function cancelAll() { if (touchTimeout) clearTimeout(touchTimeout) if (tapTimeout) clearTimeout(tapTimeout) if (swipeTimeout) clearTimeout(swipeTimeout) if (longTapTimeout) clearTimeout(longTapTimeout) touchTimeout = tapTimeout = swipeTimeout = longTapTimeout = null touch = {} } function isPrimaryTouch(event){ return (event.pointerType == 'touch' || event.pointerType == event.MSPOINTER_TYPE_TOUCH) && event.isPrimary } $(document).ready(function(){ var now, delta, deltaX = 0, deltaY = 0, firstTouch, _isPointerType $(document) .on('touchstart', function(e){ //取第一個手指的信息 firstTouch = e.touches[0] if (e.touches && e.touches.length === 1 && touch.x2) { // Clear out touch movement data if we have it sticking around // This can occur if touchcancel doesn't fire due to preventDefault, etc. touch.x2 = undefined touch.y2 = undefined } //記錄點下的時候 now = Date.now() delta = now - (touch.last || now) touch.el = $('tagName' in firstTouch.target ? firstTouch.target : firstTouch.target.parentNode) touchTimeout && clearTimeout(touchTimeout) //記錄點下的位置 touch.x1 = firstTouch.pageX touch.y1 = firstTouch.pageY touch.last = now longTapTimeout = setTimeout(longTap, longTapDelay) }) .on('touchmove', function(e){ firstTouch = e.touches[0]; //記錄移動到的位置 和移動的距離 cancelLongTap(); touch.x2 = firstTouch.pageX touch.y2 = firstTouch.pageY deltaX += Math.abs(touch.x1 - touch.x2) deltaY += Math.abs(touch.y1 - touch.y2) }) .on('touchend', function(e){ cancelLongTap() //判斷移動的范圍來判斷是 tap 還是 swipe // swipe if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) || (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30)) swipeTimeout = setTimeout(function() { touch.el.trigger('swipe') touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2))) touch = {} }, 0) // normal tap else if ('last' in touch) // don't fire tap when delta position changed by more than 30 pixels, // for instance when moving to a point and back to origin if (deltaX < 30 && deltaY < 30) { // delay by one tick so we can cancel the 'tap' event if 'scroll' fires // ('tap' fires before 'scroll') tapTimeout = setTimeout(function() { // trigger universal 'tap' with the option to cancelTouch() // (cancelTouch cancels processing of single vs double taps for faster 'tap' response) var event = $.Event('tap') event.cancelTouch = cancelAll touch.el.trigger(event) }, 0) } else { touch = {} } deltaX = deltaY = 0 }) // when the browser window loses focus, // for example when a modal dialog is shown, // cancel all ongoing events .on('touchcancel', cancelAll) // scrolling the window indicates intention of the user // to scroll, not tap or swipe, so cancel all ongoing events $(window).on('scroll', cancelAll) }) ;['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'tap', 'longTap'].forEach(function(eventName){ $.fn[eventName] = function(callback){ //給元素綁定上面的事件 return this.on(eventName, callback) } }) })(Zepto); </script> <script> $("#vv").bind("tap",function(){ ss.innerHTML = ~~ss.innerHTML+1; }); $("#vv").bind("swipeLeft",function(){ ss1.innerHTML = ~~ss1.innerHTML+1; }); $("#vv").bind("longTap",function(){ ss2.innerHTML = ~~ss2.innerHTML+1; }); </script> </body> </html>
一些我遇到的手勢問題
問題1
在有些android的版本上 touchend不觸發
在Android 4.0.x的版本上我遇到過,很蛋疼,比如小米1的最開始的版本就遇到過
如果在touchmove中加上 阻止默認行為 是可以的(e.preventDefault();),但是會帶來另為一個嚴重的問題,就是無法向下滑動,真實無解的問題,好在這個版本已經離我們遠去
這個問題的一些討論
https://code.google.com/p/android/issues/detail?id=19827
http://stackoverflow.com/questions/7691551/touchend-event-in-ios-webkit-not-firing
問題2
透傳的問題
透傳應該分2中,
一種是上面的div隱藏,觸發到下面的元素的click,都用tap就可解決,不要一個tap一個click 這樣不好
另外一種是上層的元素隱藏,觸發到下面input的聚焦,彈出鍵盤(最常見的場景,就是彈出個這招層,點關閉遮罩層的時候,下面有一個input)
這個問題都找不到好的解決方案,我在項目中的做法是有一個透明的遮罩層,先關閉遮罩層,在等280ms關閉這個透明的遮罩層