最近在忙混合開發,因交互相對復雜,所以也踩了很多坑。在此做一下總結。
1.tap事件的實際應用
在使用tap事件時,老生常談的肯定是點透問題,大多情況下,在有滑屏交互的頁面時,我們會在根節點阻止默認行為以解決事件點透的bug。
阻止默認行為有優點,但也會相對帶來一些問題。
優點:
(1)解決事件點透
(2)解決IOS10+ safari 以及部分安卓瀏覽器 不在支持 viewport的最大縮放值和禁止縮放的問題
(3)解決IOS10+ safari下給body加overflow:hidden無效的問題
給元素加了 一個絕對定位,但是元素本身沒有定位父級,元素如果超出了body的寬度,body 上的overflow對這個元素,不起效果
解決辦法:增加一個div作為根節點,並且添加相對定位和overflow:hidden
缺點:
(1)禁止mouse事件執行(也可以說成是優點,具體情況具體分析)
(2)阻止瀏覽器的自帶效果(左右滑動切換頁面,滾動回彈等)
(3)阻止觸發瀏覽器的滾動條
(4)阻止觸發瀏覽器系統菜單
(5)阻止圖片文字被選中
(6)阻止input的輸入(在新開頁面進行輸入 eg.淘寶)
2.touchEvent相關變量
(1)touches 當前屏幕上的手指列表
(2)targetTouches 當前元素上的手指列表
(3)changedTouches 觸發當前事件的手指列表
(4)clientX 和 clientY 手指相對於可視區的坐標
(5)pageX 和 pageY 手指相對於頁面的坐標
3.tap事件封裝要點
(1)touchend時確定最大距離,大於此距離無效
(2)確定從touchstart到touchend的時間間隔,大於此事件間隔無效
(3)嚴格意義上講,touchmove時也應該有最大距離限制(手指在某元素上點擊后繞了一大圈再回到當前元素不應觸發tap事件)
(4)需要考慮tap事件對click的影響
(5)如有需要的話,還要限制為只有單指操作才能觸發
4. 遵循上述要求的完整的tap封裝如下:
當然你可以擴展為面向對象,或者試用與Vue的各種形式,但原理都是如此。最后會附上Vue版的常用指令
function tap(el, fn) { var start = {}; var moveOverLimit; el.addEventListener('touchstart', function(e) { start = { x: e.changedTouches[0].pageX, y: e.changedTouches[0].pageY, startTime: new Date().getTime() } moveOverLimit = false; }); el.addEventListener('touchmove', function (e) { if (Math.abs(e.changedTouches[0].pageX - start.x) > 5 || Math.abs(e.changedTouches[0].pageY - start.y) > 5) { moveOverLimit = true } }) el.addEventListener('touchend', function(e) { var end = { x: e.changedTouches[0].pageX, y: e.changedTouches[0].pageY, endTime: new Date().getTime() } // 此處的限制如第三點說明的一樣 if (Math.abs(end.x - start.x) < 5 && Math.abs(end.y - start.y) < 5 && end.endTime - start.startTime < 300 && !moveOverLimit) { fn && fn.call(el, e); } }); }
5. Vue版的常用指令如下:
import Vue from 'vue'; // 限制為單手操作,可根據需要自行修改 function commonSingleSlideCheck (ev) { if (ev.touches.length > 1) { return false; } return true; } class VueSlide { constructor (el, binding, vnode) { this.el = el; this.binding = binding; this.vnode = vnode; this.evType = ''; this.startPos = {x: 0, y: 0}; this.startTime = null; this.movePos = {}; this.canPullToLeft = true; this.canPullToRight = true; this.canPullToDown = true; this.canPullToUp = true; this.typeCheck = { 'toLeft': commonSingleSlideCheck, 'toRight': commonSingleSlideCheck, 'toUp': commonSingleSlideCheck, 'toDown': commonSingleSlideCheck, 'drag': commonSingleSlideCheck, }; } start(ev, el, binding, vnode) { this.startPos.x = ev.changedTouches[0].clientX; this.startPos.y = ev.changedTouches[0].clientY; switch (this.evType) { default: if (ev.targetTouches.length > 1) { return false; } break; } if (this.evType === 'tap') { this.startTime = new Date().getTime(); if (binding.value.stop === true) { ev.stopPropagation(); } if (binding.value.prevent === true) { ev.preventDefault(); } } } move(ev, el, binding, vnode) { this.movePos.disX = Math.abs(ev.changedTouches[0].clientX - this.startPos.x); this.movePos.disY = Math.abs(ev.changedTouches[0].clientY - this.startPos.y); this.movePos.changeX = ev.changedTouches[0].clientX - this.startPos.x; this.movePos.changeY = ev.changedTouches[0].clientY - this.startPos.y; this.movePos.dir = ''; if (!this.typeCheck[this.evType](ev)) { return false; } if ((Math.atan(this.movePos.disX / this.movePos.disY) > Math.PI / 3)) { ev.preventDefault(); if (this.movePos.changeX > 0 && this.movePos.disX > 30) { this.movePos.dir = 'right'; } if (this.movePos.changeX < 0) { this.movePos.dir = 'left'; } } if ((Math.atan(this.movePos.disX / this.movePos.disY) < Math.PI / 6)) { if (this.movePos.changeY > 0 && this.movePos.disY > 30) { this.movePos.dir = 'down'; } if (this.movePos.changeY < 0) { this.movePos.dir = 'up'; } } if (this.evType === 'drag') { binding.value({ ev: ev, startX: this.startPos.x, startY: this.startPos.y, changeX: this.movePos.changeX, changeY: this.movePos.changeY, dir: this.movePos.dir, }); } } end(ev, el, binding, vnode) { let disX = Math.abs(ev.changedTouches[0].clientX - this.startPos.x); let disY = Math.abs(ev.changedTouches[0].clientY - this.startPos.y); let changeX = ev.changedTouches[0].clientX - this.startPos.x; let changeY = ev.changedTouches[0].clientY - this.startPos.y; if (this.evType !== 'drag' && this.evType !== 'tap') { if ((Math.atan(disX / disY) > Math.PI / 3) && disX > 30) { if (changeX > 0 && this.evType === 'toRight' && this.canPullToRight) { // console.log('向右滑動'); this.canPullToRight = false; binding.value(el); } if (changeX < 0 && this.evType === 'toLeft' && this.canPullToLeft) { // console.log('向左滑動'); this.canPullToLeft = false; binding.value(el); } } if ((Math.atan(disX / disY) < Math.PI / 6) && disY > 30) { if (changeY > 0 && this.evType === 'toDown' && this.canPullToDown) { // console.log('向下滑動'); this.canPullToDown = false; binding.value(el); } if (changeY < 0 && this.evType === 'toUp' && this.canPullToUp) { // console.log('向上滑動'); this.canPullToUp = false; binding.value(el); } } } if (this.evType === 'tap') { let endTime = new Date().getTime(); if (Math.abs(changeX) < 5 && endTime - this.startTime < 300) { binding.value.handler(binding.value.param); } else { return; } } this.canPullToLeft = true; this.canPullToRight = true; this.canPullToUp = true; this.canPullToDown = true; this.el.addEventListener('touchstart', null); this.el.addEventListener('touchmove', null); this.el.addEventListener('touchend', null); } init(type) { let _this = this; this.evType = type; this.el.addEventListener('touchstart', function(ev) { _this.start(ev, _this.el, _this.binding, _this.vnode); }); this.el.addEventListener('touchmove', function(ev) { _this.move(ev, _this.el, _this.binding, _this.vnode); }); this.el.addEventListener('touchend', function(ev) { _this.end(ev, _this.el, _this.binding, _this.vnode); }); } } /* v-tap: vue移動端tap時間 eg: <div v-tap="{handler: fn, param: testParam}"></div> 參數: handler: 監聽函數(function) param: 監聽函數的參數 (object) stop: 是否阻止冒泡 (boolean) prevent: 是否阻止默認行為 (booleam) */ Vue.directive('tap', { bind: function(el, binding, vnode) { new VueSlide(el, binding, vnode).init('tap'); }, }); Vue.directive('to-left', { bind: function(el, binding, vnode) { new VueSlide(el, binding, vnode).init('toLeft'); }, }); Vue.directive('to-right', { bind: function(el, binding, vnode) { new VueSlide(el, binding, vnode).init('toRight'); }, }); Vue.directive('to-up', { bind: function(el, binding, vnode) { new VueSlide(el, binding, vnode).init('toUp'); }, }); Vue.directive('to-down', { bind: function(el, binding, vnode) { new VueSlide(el, binding, vnode).init('toDown'); }, }); // 拖拽指令:執行函數中注入了三個參數 // changeX(Number) : 橫軸的變化量 // changeY(Number) : 縱軸的變化量 // dir(String): 用戶想要滑動的方向,只有在橫縱軸的左右偏移方向在30deg內才會存在方向,其他角度范圍內為空字符。 Vue.directive('drag', { bind: function(el, binding, vnode) { new VueSlide(el, binding, vnode).init('drag'); }, });