寫在前面
讀 Zepto 的 touch 模塊源碼是想知道 tap 的具體實現,進而寫出自己的工具庫 my.js 的 touch 模塊
(my.js 是根據業務需要量身定做的移動頁面工具庫,不支持 win phone)
一.Zepto 與點透/點穿問題
Zepto 以前存在點擊穿透的問題,14 年(還是 12/13 年?)修復了頁內點擊穿透問題,但目前(2015-8-22)仍然存在跨頁面點擊穿透問題
P.S.關於點擊穿透問題的詳細信息,請查看 黯羽輕揚:移動頁面點擊穿透問題解決方案
二.源碼解讀
Zepto 的 touch 模塊源碼如下,附有詳細註釋(ie 相關部分略掉):
// Zepto.js
// (c) 2010-2015 Thomas Fuchs
// Zepto.js may be freely distributed under the MIT license.
;(function($) {
var touch = {},
touchTimeout, tapTimeout, swipeTimeout, longTapTimeout, // Timeout ID
longTapDelay = 750,
gesture
// 判斷滑動方向,返回 Left, Right, Up, Down
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 = {}
}
// IE 的 touch 事件?
function isPrimaryTouch(event) {
return (event.pointerType == 'touch' ||
event.pointerType == event.MSPOINTER_TYPE_TOUCH) && event.isPrimary
}
// (ie) 鼠標事件?
function isPointerEventType(e, type) {
return (e.type == 'pointer' + type ||
e.type.toLowerCase() == 'mspointer' + type)
}
// 入口
$(document).ready(function() {
var now, delta, deltaX = 0,
deltaY = 0,
firstTouch, _isPointerType
// IE 手勢
if ('MSGesture' in window) {
gesture = new MSGesture()
gesture.target = document.body
}
$(document)
// 處理 IE 手勢結束
.bind('MSGestureEnd', function(e) {
var swipeDirectionFromVelocity =
e.velocityX > 1 ? 'Right' : e.velocityX < -1 ? 'Left' : e.velocityY > 1 ? 'Down' : e.velocityY < -1 ? 'Up' : null;
if (swipeDirectionFromVelocity) {
touch.el.trigger('swipe')
touch.el.trigger('swipe' + swipeDirectionFromVelocity)
}
})
// 處理手指接觸
.on('touchstart MSPointerDown pointerdown', function(e) {
// 排除非觸摸設備
if ((_isPointerType = isPointerEventType(e, 'down')) &&
!isPrimaryTouch(e)) return
// 獲取起點位置數據
firstTouch = _isPointerType ? e : 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 事件處理器的 Timeout ID
// 記錄起點坐標
touch.x1 = firstTouch.pageX
touch.y1 = firstTouch.pageY
// 判定雙擊
if (delta > 0 && delta <= 250) touch.isDoubleTap = true
touch.last = now // 更新上次碰觸時間
// 註冊長按事件處理器 ID(750ms 後觸發長按,肯定會在手指離開時判斷取消)
longTapTimeout = setTimeout(longTap, longTapDelay)
// 支持 IE 手勢識別
// adds the current touch contact for IE gesture recognition
if (gesture && _isPointerType) gesture.addPointer(e.pointerId);
})
// 處理手指滑動
.on('touchmove MSPointerMove pointermove', function(e) {
// 排除非觸摸設備
if ((_isPointerType = isPointerEventType(e, 'move')) &&
!isPrimaryTouch(e)) return
// 讀出起點坐標
firstTouch = _isPointerType ? e : 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 MSPointerUp pointerup', function(e) {
// 排除非觸摸設備
if ((_isPointerType = isPointerEventType(e, 'up')) &&
!isPrimaryTouch(e)) return
// 取消長按事件處理器
cancelLongTap()
// swipe
// 判定滑動動作(起點 - 終點的橫向或者縱向距離超過 30px)
if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
(touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))
// 註冊長按事件處理器 ID(立即準備執行長按)
swipeTimeout = setTimeout(function() {
touch.el.trigger('swipe') // 觸發長按
// 觸發向上 | 下 | 左 | 右的長按
touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
// 清空數據,本次 touch 結束
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
// 如果從接觸到抬起,中間滑過的橫向和縱向距離都不超過 30px——是正常輕觸
if (deltaX < 30 && deltaY < 30) {
// delay by one tick so we can cancel the 'tap' event if 'scroll' fires
// ('tap' fires before 'scroll')
// 立即準備執行輕觸,不立即執行是為了 scroll 時能取消執行輕觸
tapTimeout = setTimeout(function() {
// trigger universal 'tap' with the option to cancelTouch()
// (cancelTouch cancels processing of single vs double taps for faster 'tap' response)
// 觸發全局 tap,cancelTouch() 可以取消 singleTap,doubleTap 事件,以求更快響應輕觸
var event = $.Event('tap')
event.cancelTouch = cancelAll
touch.el.trigger(event)
// trigger double tap immediately
// 立即觸發 doubleTap
if (touch.isDoubleTap) {
if (touch.el) touch.el.trigger('doubleTap')
touch = {}
}
// trigger single tap after 250ms of inactivity
// 250ms 後觸發 singleTap
else {
touchTimeout = setTimeout(function() {
touchTimeout = null
if (touch.el) touch.el.trigger('singleTap')
touch = {}
}, 250)
}
}, 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 MSPointerCancel pointercancel', cancelAll)
// scrolling the window indicates intention of the user
// to scroll, not tap or swipe, so cancel all ongoing events
// 觸發 scroll 時取消所有事件處理動作
$(window).on('scroll', cancelAll)
})
// 註冊自定義事件
;['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown',
'doubleTap', 'tap', 'singleTap', 'longTap'
].forEach(function(eventName) {
$.fn[eventName] = function(callback) {
return this.on(eventName, callback)
}
})
})(Zepto)
三.tap 事件的實現原理
tap 事件是通過 touch 事件來模擬的,tap 表示「輕觸」,比 click 要快很多(300ms 的樣子)。具體的實現細節不再贅述(上面的源碼裡註釋得很清楚了),這裡說一點題外話
單單實現 tap 很容易,幾十行代碼就足夠了,但要確保其餘功能正常就需要手動實現一整套的動作事件:
-
tap
-
singleTap
-
doubleTap
-
longTap
-
swipe
-
swipeLeft
-
swipeRight
-
swipeUp
-
swipeDown
如果只是簡單實現了 tap,那麼滑動,雙擊,長按等等各種動作都會失效,Zepto 的 touch 模塊也只實現了 tap 和 swipe 相關的動作,不支持複雜手勢,需要支持複雜手勢的話,建議選用 hammer.js,hammer 提供了完善的一整套手勢支持(注意:hammer 也存在點擊穿透問題,仍然需要手動處理該問題)
暫無評論,快來發表你的看法吧