点透 & 解决方案

点透 & 解决方案

学习map:

  • 现象:再现现象,总结导致点透出现的情况
  • 分析原因
  • 解决办法

现象

再现点透现象请使用一下方式:

  1. 手机访问传送门
  2. 复制链接到连图生成二维码后扫一扫
  3. 或者打开chrome手机视图并打开touch screen

zepto-隐藏元素-点透

代码:

zepto-最新-没有点透

代码:

原生js-隐藏元素-点透

代码:

原生js-元素出现-点透

代码:

综上,导致点透现象出现的场景:

  1. 元素z轴重叠;
  2. 绑定了touch事件的元素消失或移走;
  3. 绑定有类click事件的元素出现在点击区域;

PS:
a链接的href跳转、input、select等表单元素的聚焦并弹起软键盘,等触发的事件和click一样,由于历史原因在手机端也会表现出300ms延迟,因此也都可以看做是click事件,我叫他类click事件,demo可用爪机狠戳事件触发顺序&click延迟demo或者复制链接到连图生成二维码后扫一扫。类click事件,也是一种浏览器默认行为,可被event.preventDefault()阻止。

代码:

分析

事件触发顺序&click延迟demo这个demo可以看出:

  • iphone系手机:mouse事件在touchend之后才开始,mouse事件覆盖了click事件,屏蔽了input弹出键盘的默认行为
  • 安卓手机:mouse事件会先于touch事件开始,而迟于其结束,mouse事件没有覆盖input的聚焦弹软键盘的浏览器默认行为
  • 在ios设备上的浏览器[UC浏览器除外]能明显感受到click延迟
  • 无论是android还是ios还是PC的touch screen的click(or mouse)事件都是迟于touchend事件被触发的
  • 因此,从手指触摸屏幕到离开屏幕,先后触发了touchstarttouchendclick

由于我们在touchend阶段z轴层级已经发生了变化,当click被触发时候,能够被点击的元素则是当前z轴离用户最近的层,根据click事件的触发规则:

在被触发时,当前有绑定click事件的元素显示,且在面朝用户的最前端时,才触发click事件

因此touchend之后符合条件的绑定了click事件的元素被点透。

总而言之:出现点透是由于移动端click事件迟于touch事件被触发导致的。

解决办法

不优雅的办法:

1. 对于默认绑定了类click事件的元素(如a、input、select等)
  • touchend + preventDefault及时取消touch元素的默认click事件,即if(eve == "touchend") e.preventDefault();

  • 如果牺牲点性能无所谓的话,可以将可能在z轴方向上引起点透现象的元素绑定成click事件,比如遮罩层之类的,不过还可以增加些许有趣的交互抵消用户的焦躁心理,比如:demo-ripple

2. 对于没有默认绑定类click事件的元素

统一使用touch事件。z轴上都绑定touchstarttouchendtap不用阻止默认行为也不会穿透。

使用上述方法有很明显的缺点和不方便:

使用touchend + preventDefault要在同一个元素上绑定2个事件,zepto可以封装成tap事件,我们也可以,自定义tap事件阻止点透

代码:

在本demo中,虽然没有出现点透现象,但是点击出现弹层以后你会发现点击a链接、span、input都没有任何反映了,这是因为在touchend里阻止浏览器默认行为,触发自定义tap事件,不仅会阻止掉了input的软键盘弹出,还会阻止一切非tap事件,解决办法就是使用合成的click事件去覆盖会延迟的click事件。

重写click事件

代码:

event.initEvent('click', bubbles, true);

touch_target.addEventListener("click", handle, false);

这样再次点击,弹层上的a链接和span的click事件都响应地很迅速,然而input和select的弹出软键盘的功能被阉割了,实际上input弹出软键盘的事件是focus()事件

3. fastclick

读fastclick源码


layer.removeEventListener('touchend', this.onTouchEnd, false);

FastClick.prototype.onTouchEnd = function(event) {
    ...
    targetTagName = targetElement.tagName.toLowerCase();
    if (targetTagName === 'label') {
        forElement = this.findControl(targetElement);
        if (forElement) {
            this.focus(targetElement);
            if (deviceIsAndroid) {
                return false;
            }

            targetElement = forElement;
        }
    } else if (this.needsFocus(targetElement)) {

        if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
            this.targetElement = null;
            return false;
        }

        this.focus(targetElement);
        this.sendClick(targetElement, event);

        if (!deviceIsIOS || targetTagName !== 'select') {
            this.targetElement = null;
            event.preventDefault();
        }

        return false;
    }
    
// needsFocus
FastClick.prototype.needsFocus = function(target) {
    switch (target.nodeName.toLowerCase()) {
    case 'textarea':
        return true;
    case 'select':
        return !deviceIsAndroid;
    case 'input':
        switch (target.type) {
        case 'button':
        case 'checkbox':
        case 'file':
        case 'image':
        case 'radio':
        case 'submit':
            return false;
        }

        // No point in attempting to focus disabled inputs
        return !target.disabled && !target.readOnly;
    default:
        return (/\bneedsfocus\b/).test(target.className);
    }
};
// sendClick 
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);
};    

上面代码的意思就是:在目标元素上绑定touchend事件,在事件处理函数里,如果是需要focus的表单元素被点击,则先触发他们的focus事件,再触发自定义的click事件,fastclick之所以大,就是因为对很多表单元素在各个系统的各个版本的不同表现做了兼容,不仅解决了click以及类click的延迟问题,而且当检测到当前页面使用了基于 <meta> 标签或者 touch-action 属性的解决方案时,会静默退出。可以说,这是真正的跨平台方案出来之前一种很好的变通方案。而zepto 只是为普通的点击事件封装了一个更快的tap事件,类click事件的延迟问题并没有得到解决,而且移动端使用的tap事件,如果没做设备判断兼容PC的话,PC端的点击事件将得不到响应,这会很影响网站的可用性和可访问性。不过zepto封装了一系列移动端很需要的功能,比如swipeLeft、swipeRight、swipeUp、等等,二者各有春秋,兼并两者优势的库我目前没遇到,不过可以尝试自己写一个,加个todo吧。

4. tap.js

tap.js源码只有不到200行,大致看了下,并不能解决类click的延迟问题,鸡肋!

posted @ 2015-09-17 10:55  freeyiyi1993  阅读(...)  评论(...编辑  收藏