手機web開發,click,touch,tap事件淺析
其中包括:touchstart,touchmove,touchend,touchcancel 這四個事件
touchstart,touchmove,touchend事件可以類比於mousedown,mouseover
,mouseup的觸發。
touchstart : 當手指觸控到螢幕會觸發;
touchmove : 當手指在螢幕上移動時,會觸發;
touchend : 當手指離開螢幕時,會觸發;
而touchcancel許多人不知道它在什麼時候會被觸發而忽略它,其實當你的手指還沒有離開螢幕時,有系統級的操作發生時就會觸發touchcancel,例如alert和confirm彈框,又或者是
這4個事件的觸發順序為:
touchstart -> touchmove -> …… -> touchmove ->touchend
但是單憑監聽上面的單個事件,不足以滿足我們去完成監聽在觸屏手機常見的一些手勢操作,如雙擊、長按、左右滑動、縮放等手勢操作。需要組合監聽這些事件去封裝對這類手勢動作。
其實市面上很多框架都針對手機瀏覽器封裝了這些手勢,例如jqmobile、zepto、jqtouch,不過悲劇發生了,對於某些android系統(我自己測試到的在android 4.0.x),touchmove和touchend事件不能被很好的觸發,舉例子說明下:
比如手指在螢幕由上向下拖動頁面時,理論上是會觸發 一個 touchstart ,很多次 touchmove ,和最終的 touchend ,可是在android 4.0上,touchmove只被觸發一次,觸發時間和
暫時我只發現在android 4.0會有這個bug,據說 ios 3.x的版本也會有。
而顯然jqmobile、zepto等都沒有意識到這個bug對監聽實現帶來的嚴重影響,所以在直接使用這些框架的event時,或多或少會出現相容性問題!(個人親身慘痛經歷)
所以我修改了一下zepto的event模組,並且添加了一些事件觸發引數,加強了一下可用性。
(function($){
$.fn.touchEventBind = function(touch_options)
{
var touchSettings = $.extend({
tapDurationThreshold : 250,//觸屏大於這個時間不當作tap
scrollSupressionThreshold : 10,//觸發touchmove的敏感度
swipeDurationThreshold : 750,//大於這個時間不當作swipe
horizontalDistanceThreshold: 30,//swipe的觸發垂直方向move必須小於這個距離
verticalDistanceThreshold: 75,//swipe的觸發水平方向move必須大於這個距離
tapHoldDurationThreshold: 750,//swipe的觸發水平方向move必須大於這個距離
doubleTapInterval: 250//swipe的觸發水平方向move必須大於這個距離
}, touch_options || {})
var touch = {}, touchTimeout ,delta ,longTapTimeout;
function parentIfText(node){
return 'tagName' in node ? node : node.parentNode
}
function swipeDirection(x1, x2, y1, y2){
var xDelta = Math.abs(x1 - x2), yDelta = Math.abs(y1 - y2)
return xDelta >= yDelta ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')
}
function longTap()
{
longTapTimeout = null
touch.el.trigger('longTap');
touch.longTap = true;
touch = {};
}
function cancelLongTap()
{
if (longTapTimeout) clearTimeout(longTapTimeout)
longTapTimeout = null
}
this.data('touch_event_bind',"true");
this.bind('touchstart', function(e)
{
touchTimeout && clearTimeout(touchTimeout);
touch.el = $(parentIfText(e.touches[0].target));
now = Date.now();
delta = now - (touch.last_touch_time || now);
touch.x1 = e.touches[0].pageX;
touch.y1 = e.touches[0].pageY;
touch.touch_start_time = now;
touch.last_touch_time = now;
if (delta > 0 && delta <= touchSettings.doubleTapInterval) touch.isDoubleTap = true;
longTapTimeout = setTimeout(function(){
longTap();
},touchSettings.tapHoldDurationThreshold);
}).bind('touchmove',function(e)
{
cancelLongTap();
touch.x2 = e.touches[0].pageX;
touch.y2 = e.touches[0].pageY;
// prevent scrolling
if ( Math.abs( touch.x1 - touch.x2 ) > touchSettings.scrollSupressionThreshold )
{
e.preventDefault();
}
touch.touch_have_moved = true;
}).bind('touchend',function(e)
{
cancelLongTap();
now = Date.now();
touch_duration = now - (touch.touch_start_time || now);
if(touch.isDoubleTap)
{
touch.el.trigger('doubleTap');
touch = {};
}
else if(!touch.touch_have_moved && touch_duration < touchSettings.tapDurationThreshold)
{
touch.el.trigger('tap');
touchTimeout = setTimeout(function(){
touchTimeout = null;
touch.el.trigger('singleTap');
touch = {};
}, touchSettings.doubleTapInterval);
}
else if ( touch.x1 && touch.x2 )
{
if ( touch_duration < touchSettings.swipeDurationThreshold && Math.abs( touch.x1 - touch.x2 ) > touchSettings.verticalDistanceThreshold && Math.abs( touch.y1 - touch.y2 ) < touchSettings.horizontalDistanceThreshold )
{
touch.el.trigger('swipe').trigger( touch.x1 > touch.x2 ? "swipeLeft" : "swipeRight" );
touch = {};
}
}
}).bind('touchcancel',function(e){
touchTimeout && clearTimeout(touchTimeout);
cancelLongTap();
touch = {};
})
}
$.fn.touchbind = function(m,callback,touch_options)
{
if(this.data('touch_event_bind')!="true")
{
this.touchEventBind(touch_options);
}
this.bind(m,callback);
}
;['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'singleTap', 'longTap'].forEach(function(m)
{
$.fn[m] = function(touch_options,callback)
{
if(typeof(touch_options)=="object" && typeof(callback)=="function")
{
return this.touchbind(m, callback , touch_options)
}
else if(typeof(touch_options)=="function")
{
var callback = touch_options;
return this.touchbind(m, callback)
}
}
})
})(Zepto)
上面的程式碼基於zepto,替換掉原先zepto的這塊就OK了,不過獨立寫開來也是可以的,我只是用到了zepto的 bind函式來做事件監聽而已,實現的思路其實也很清晰。
相容的解決辦法是在 touchmove 時判斷手勢趨勢大於預設值時(大於預設值證明有 move的動作趨勢),停止預設的操作e.preventDefault(),這樣touchedn就可以被正常觸發了。真心認為google的這個bug是一個極其影響手機web互動的bug!