manson

Just do I.T @_@ — photo by ip4s & MX3

手机浏览器常用手势动作监听封装

现今大多数触屏手机webkit内核提供了touch事件的监听,让开发者可以获取用户触摸屏幕时的一些信息。

其中包括:touchstart,touchmove,touchend,touchcancel 这四个事件

touchstart,touchmove,touchend事件可以类比于mousedown,mouseover ,mouseup的触发

touchcancel许多人不知道它在什么时候会被触发而忽略它,其实当你的手指还没有离开屏幕时,有系统级的操作发生时就会触发touchcancel,例如alert和confirm弹框,又或者是android系统的功能弹窗,例如:


这4个事件的触发顺序为:

touchstart -> touchmove -> …… -> touchmove ->touchend

但是单凭监听上面的单个事件,不足以满足我们去完成监听在触屏手机常见的一些手势操作,如双击、长按、左右滑动、缩放等手势操作。需要组合监听这些事件去封装对这类手势动作。

其实市面上很多框架都针对手机浏览器封装了这些手势,例如jqmobile、zepto、jqtouch,不过悲剧发生了,对于某些android系统(我自己测试到的在android 4.0.x),touchmove和touchend事件不能被很好的触发,举例子说明下:

比如手指在屏幕由上向下拖动页面时,理论上是会触发 一个 touchstart ,很多次 touchmove ,和最终的 touchend ,可是在android 4.0上,touchmove只被触发一次,触发时间和touchstart 差不多,而touchend直接没有被触发。这是一个非常严重的bug,在google Issue已有不少人提出  http://code.google.com/p/android/issues/detail?id=19827

暂时我只发现在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!

 

做了DEMO,可以在手机浏览器打开 http://my.poco.cn/hotbed/touchevent/index.html

在红色区域进行左右滑动,单击,双击,长按等事件

经测试,上述代码兼容 ios 4.1+、android 2.2+

2012.07.12
返回顶部