移动设备上的touch

移动设备对h5的touch支持不是太完善,大部分的手势都是基于3个事件来模拟实现touchstart,touchmove,touchend,具体的事件详情可以查看MDN文档

手势

click

其实h5中移动设备上本不应该以click作为主要的事件来处理,因为它是属于MouseEvents,从语义上来说tap更适合移动设备上的"点击",但是因为dom无这个事件,因此需要模拟.

移动设备上事件触发的顺序也有意思,touchstart=>touchmove=>touchend=>mousedown=>focus=>mouseup=>click,为什么会这样,因为click事件有300ms延迟,移动设备需要区分你是单机还是双击缩放,那么新的问题来了

消除click的300ms延迟

在这个问题上用的最多的解决方案是fastclick,它的思路是在touchend的时候阻止默认的行为,取消后面事件的触发,然后手动构造一个click事件,并进行分发,用代码触发的click事件是不会有300ms延迟的,同时它有一个选项配置是否不阻止默认的行为,一般来说是输入框等获取焦点的默认行为

/**
     * Send a click event to the specified element.
     *
     * @param {EventTarget|Element} targetElement
     * @param {Event} event
     */
    FastClick.prototype.sendClick = function(targetElement, event) {
        var clickEvent, touch;

        // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
        if (document.activeElement && document.activeElement !== targetElement) {
            document.activeElement.blur();
        }

        touch = event.changedTouches[0];

        // Synthesise a click event, with an extra attribute so it can be tracked
        clickEvent = document.createEvent('MouseEvents');
        clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
        clickEvent.forwardedTouchEvent = true;
        targetElement.dispatchEvent(clickEvent);
    };

点击击穿ghost click问题

很多人说zepto有点击击穿问题(也有人叫tap点透),即在touchend的时候不阻止触发点击的元素的默认事件,300ms后在该位置仍有可能触发这个位置上的元素的点击事件 只有在被触发时,当前有click事件的元素显示,且在面朝用户的最前端时,才触发click事件。

比较常见的现象就是有一个遮罩,点击遮罩的时候遮罩消失,可如果遮罩后面还有个按钮,那么有可能遮罩消失之后,按钮的点击事件会被触发,或者输入框的话会获得焦点,弹出输入键盘,这里有个示例

zepto在事件冒泡到document的时候preventDefault是没有效果的,应该在触发真正的元素时就阻止,而fastclick恰恰是这样做的,在绑定的dom元素上阻止默认行为和冒泡,同时init一个mouseclick事件来触发回调,既消除了300ms延迟,又不会有点透的问题产生

// Prevent the actual click from going though - unless the target node is marked as requiring
        // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
        if (!this.needsClick(targetElement)) {
            event.preventDefault();
            this.sendClick(targetElement, event);
        }

        return false;

模拟tap

说完这2个坑,再来说下如何模拟tap,其实思路和fastclick一致,只要在touchend的时候触发自定义的tap事件就可以了,当然为了阻止后面的click,需要阻止默认行为,至于自定义事件,正好DOM3提供了CustomEvent对象,于是代码如下

function trigger(el, eventType, detail) {  
            detail = detail || {}
            var event, opt = {
                bubbles: true,
                cancelable: true,
                detail: detail
            }

            try {
                if (utils.isFunction(CustomEvent)) {
                    event = new CustomEvent(eventType, opt)
                }
                else {//不支持CustomEvent构造函数的降级处理
                    event = document.createEvent("CustomEvent")
                    event.initCustomEvent(eventType, true, true, detail)
                }
                el && el.dispatchEvent(event)
            }
            catch (ex) {
                console.warn("Touch is not supported by environment.");
            }

        }

document.addEventListener("tap",function(){})

模拟swipe或slide手势

移动设备上常用的滑动无非是上下左右4个方向的判定和识别,至于叫swipe还是slide都无所谓

至于判定方向,一般的实现是判定x和y轴上的移动距离是否满足一个最小值,但其实这样的实现并不科学也不准确,如果是滑动方向呈一个角度的话,判定常常不太准,参阅了一些实现后发现用极坐标的角度来判定比较科学且准确,基本思路是在touchstart的是记录触摸点的pageX和pageY,在touchmove或touchend的时候同样记录pageX,pageY,对这2个值相减,并且用到反正切函数换算成角度值,通过相对于touchstart为坐标原点极坐标系的移动直线的角度判定移动方向

    getAngle: function (pos1, pos2) {
            //因为Y轴是向下为正值,因此计算数学上的角度应该是pos1在前面,类似本来应该是第四象限结果成了第一象限
            return Math.atan2(pos1.y - pos2.y, pos2.x - pos1.x) * 180 / Math.PI;
        },
    getDirectionByAngle: function (angle) {
            var direction
            if (angle <= 45 && angle > -45) {
                direction = "right"
            }
            else if (angle <= 135 && angle > 45) {
                direction = "up"
            }
            else if (angle > 135 || angle <= -135) {
                direction = "left"
            }
            else if (angle <= -45 && angle > -135) {
                direction = "down"
            }

            return direction
        },

这里又不得不说下Android Ice Cream Sandwich上对于touchmove的坑,如果在首次touchstart或者touchmove的时候不阻止默认行为,后续的touchmove或touchend不会触发,这个坑简直令人发指啊。。

如果你阻止了默认行为,那么native的滚动便不能用了,如果是事件绑定在document上的话,原生滚动条不能滚动怎么办?iscroll的实现是计算首次move和start的运算距离和时间算出势能,然后用transition模拟实现滚动效果,目前来看也是比较好的一个实现兼容的方式了

总结

了解这些坑可以帮助我们在实际的应用中作出取舍,选择最合适的方法来解决问题