由弹出层激发对转动道理的议论

媒介

上一篇为了诠释挪动端web的事宜和点击穿透题目,我做了一个弹出框做例子,见demo。如今请把关注点转移到弹出层自身上来,我运用fix定位将它定在屏幕中心,转动屏幕时发明题目没有,底层元素照样在转动,只是弹出层在屏幕正中心而且四周有遮罩。所以我们就“转动”这件事细致说说,可以存在哪些转动需求。

页面转动原理

在PC上网页转动主要靠鼠标滚轮,其次按“上”“下”键也能转动页面,还可以按“空格”“Page Down/Up”以及“HOME”键,或许直接点击或拖动转动条也能转动页面。那末我们来做个试验,看这些事宜的发作递次是怎样的。

document.addEventListener('scroll', function(){
    alert('document scroll');
});

window.addEventListener('scroll', function(){
    alert('window scroll');
});

window.addEventListener('mousewheel', function(){
    alert('window mousewheel');
});

window.addEventListener('keydown', function(e){
    if(37 <= e.keyCode && e.keyCode <= 40 || e.keyCode == 32){
        alert('keydown ' + e.keyCode);
    }
});

可以得知,当经由历程鼠标滚轮时,mousewheel事宜会先触发,然后才是scroll。而事宜的listener默许是遵照冒泡的,所以绑在document上的函数会先触发,然后才是window上的。同理,当经由历程按特定的键去转动页面时,keydown事宜会先触发,然后也是scroll

PC上没啥题目,那来看看手机端的表现。

document.addEventListener('scroll', function(){
    alert('document scroll');
});

document.addEventListener('touchstart', function(){
    alert('document touchstart');
});

document.addEventListener('touchmove', function(){
    alert('document touchmove');
});

document.addEventListener('touchend', function(){
    alert('document touchend');
});

依据PC上相似的逻辑,以及前一篇文章中提到的touch事宜原理,我们很轻易猜出alert递次是:touchstart -> touchmove -> scroll -> touchend 但这是事宜发作的递次,并非alert结果的递次。可以扫二维码看看,这个alert很诡异的。

当逐步滑时,只会 alert touchstart,然后就没有了。而快速滑时,alert touchstart 然后 alert scroll。这是因为alert框会壅塞事宜响应,当touchstart后还没来的及滑动就已弹出alert了,全部事宜线程就被中缀了,所以就不会响应scroll了。而当弹出alert后继承滑动(从最先到如今手指一直不松开),然后再松开手指,我们会发明 alert touchstart 后又 alert scroll。为何alert又没中缀事宜线程呢?

我们晓得PC上的alert框是会中缀全部页面的,即除非你先点“一定”,不然页面上的任何操纵都是无效的,即全部用户界面被“卡住”了。而在手机上,因为触摸事宜的连贯性,我猜想是如许的。当手机上弹出alert时是壅塞其他事宜的,但因为手指一直没松开,所以全部触摸历程还在继承。一边是alert的壅塞性,一边是前一轮的触摸历程还未完毕,因为js单线程的特征,一切事宜在用户界面上的响应都是要进入行列处置惩罚的,然后才会在界面上体现出来。因为触摸历程是先发作的,它仍未完毕,而alert是后发作的,所以alert并不能壅塞当前还未完毕的触摸历程。因而只需不松开手指,继承滑动,末了再松开手指,alert touchstart 后还会 alert scroll。

那末另有个题目,为何不会 alert touchmove 和 alert touchend 呢?我们继承做试验,顺次把 touchstart 和 touchmove 的 alert 语句解释掉,看看表现结果。

document.addEventListener('touchstart', function(){
    // alert('document touchstart');
});

document.addEventListener('touchmove', function(){
    alert('document touchmove');
});

document.addEventListener('touchend', function(){
    alert('document touchend');
});

去掉 alert touchstart 后发明只弹出 alert touchmove,我猜想是因为 touchstart / touchmove / touchend 都是在统一轮触摸历程当中的,因为alert的壅塞性,前面诠释了它许可先发作的触摸(还未松开的手指)继承touch,然则 alert 会壅塞统一轮触摸历程的其他事宜的响应函数。而之所以alert弹出后继承滑动手指(一直不松开),仍能看到页面在转动,这是因为这是浏览器的默许行动,而且touch历程的发作时刻早于alert,所以在行列中alert没法壅塞它。

以上只是我的猜想,有谁晓得详细细节的请告诉我~ 手指不松开时,这个alert框的底层转动题目恰好也投合了本文一最先说的弹出框demo,假如有需求说弹出框涌现时必需让外部不能转动,该怎样办?

转动禁用

overflow

我们常常会写overflow: hidden如许的css去让牢固尺寸的元素写死,如许就算它的子元素超出了父容器的尺寸局限,也不会“溢出来”。借这个原理,我们可以在root元素上写死,如许body内里就不会溢出屏幕了,就不会涌现转动条了。

html, body{
    overflow: hidden;
}

但随之又涌现了另一个题目,假如页面本来是有转动条的,在windows下的浏览器中转动条是会占有一定宽度的(chrome下是17px,firefox下多是13px),会让全部viewport的宽度减小一段,看起就像页面里的一切元素团体往左偏移一小段。而mac下浏览器的转动条是悬浮在上面的,所以不会占有页面上的空间。

如许的话,windows就哭了。假定页面底本就是有转动条的,当我们翻开弹出框时,为了制止转动,root元素被加上overflow: hidden,转动条消逝,底层一切元素就向右偏移一小段。封闭弹出框时,要让页面恢复转动,root元素改成overflow: auto,转动条又涌现了,底层一切元素又向左偏移一小段。全部体验很蹩脚!

要领就是在overflow: hidden的同时经由历程padding-right把转动条的空间预留出来。那末怎样晓得差别浏览器中转动条究竟占多宽呢?一般相似推断当前浏览器是不是支撑某个css属性或许某些取值,这类跟浏览器环境相干的题目,要领就是探索。用js动态天生一个元素,把你想测试的属性或值赋在这个元素上,然后把元素append到document中去,末了再经由历程js去取响应的值,看它究竟表现出来是啥。

参考这篇文章,可以晓得

转动条宽度 = 元素的offsetWidth – 元素border占有的2倍宽 – 元素的clientWidth

上面公式的条件是,元素具有y轴转动条。另有种相似要领是

转动条宽度 = 不带转动条的元素的clientWidth – 为该元素加上y轴转动条后的clientWidth

var getScrollbarWidth = function(){
    if(typeof getScrollbarWidth.value === 'undefined'){
        var $test = $('<div></div>');
        $test.css({
            width: '100px',
            height: '1px',
            'overflow-y': 'scroll'
        });

        $('body').append($test);
        getScrollbarWidth.value = $test[0].offsetWidth - $test[0].clientWidth;
        $test.remove();
    }
    return getScrollbarWidth.value;
};

这是依据第一种盘算体式格局写出的要领,有了这个再合营overflow便可以完成页面转动的禁用与恢复了。细致代码见demo

var disableScroll = function(){
    // body上禁用
    $('body, html').css({
        'overflow': 'hidden',
        'padding-right': getScrollbarWidth() + 'px'
    });
};

var enableScroll = function(){
    $('body, html').css({
        'overflow': 'auto',
        'padding-right': '0'
    });
};

我们看看表现结果:PC上很OK,简朴有用;手机上完整没卵用!(我是安卓机,注重是真机上无效,而非chrome手机模拟器)

《由弹出层激发对转动道理的议论》

禁用事宜

依据上面页面转动原理我们做的试验,很明显可以把转动涉及到的事宜干掉,如许固然不会转动了。

// 纪录本来的事宜函数,以便恢复
var oldonwheel, oldonmousewheel, oldonkeydown, oldontouchmove;
var isDisabled;

var disableScroll = function(){
    oldonwheel = window.onwheel;
    window.onwheel = preventDefault;

    oldonmousewheel = window.onmousewheel;
    window.onmousewheel = preventDefault;

    oldonkeydown = document.onkeydown;
    document.onkeydown = preventDefaultForScrollKeys;

    oldontouchmove = window.ontouchmove;
    window.ontouchmove = preventDefault;

    isDisabled = true;
};

var enableScroll = function(){
    if(!isDisabled){
        return;
    }

    window.onwheel = oldonwheel;
    window.onmousewheel = oldonmousewheel;
    document.onkeydown = oldonkeydown;

    window.ontouchmove = oldontouchmove;
    isDisabled = false;
};

这里要注重的是,差别浏览器上事宜究竟在window照样document上,PC上会有一些浏览器兼容处置惩罚。细致代码见demo

一样看看表现结果:PC上很粗犷的处理了;手机上也OK

弹出层转动需求

至此我们看到,运用overflow可以处理PC上的转动禁用题目,而禁用与转动相干的事宜可以彻底处理PC和手机的题目。那末有弹出层的话,就应当禁用全部页面的转动吗,假如弹出层内部须要转动怎样办?即我们有可以面对如许的需求:弹出框的内部是可以转动的,而弹出层外部和底层元素是不能转动的。

先看overflow

前面说到给root元素写上overflow: hidden便可以够禁用转动,那末我们对弹出层这个容器从新写个overflow: scroll便可以够了。

#popupLayer{
    overflow: scroll;
}

PC上简朴有用,然则一样手机上不鸟这些。见demo

事宜禁用与恢复

我们把document上的mousewheel事宜禁用了,即给它绑上了一个事宜函数,只不过事宜函数里将事宜发作后的浏览器默许行动阻挠了。

function preventDefault(e) {
    e = e || window.event;
    e.preventDefault && e.preventDefault();
    e.returnValue = false;
}

var disableScroll = function(){
    $(document).on('mousewheel', preventDefault);
    $(document).on('touchmove', preventDefault);
};

因而思绪就来了,我们晓得浏览器里的事宜是遵照冒泡机制的(正确来说是先从root节点由外向内“捕捉”,然后抵达目的元素后,事宜再由内向外逐层冒泡,关于这个机制请看这篇文章的第一部份,这不是本文的重点)。所以我们便可以够为弹出层的元素再绑个一样的事宜,阻挠事宜冒泡到document上,如许就不会调用到e.preventDefault()就不会阻挠浏览器默许的转动行动了。

function preventDefault(e) {
    e = e || window.event;
    e.preventDefault && e.preventDefault();
    e.returnValue = false;
}

// 内部可滚
$('#popupLayer').on('mousewheel', stopPropagation);
$('#popupLayer').on('touchmove', stopPropagation);

来看下demo,手机上请看

背景层是不能转动的,而弹出层妥妥的可以转动了!然则发明题目了不,弹出层内部转动究竟部再继承滚时,会将背景底层的元素一同滚下去了,这尼玛FUCK

革新的内部转动

处理题目的思绪很清楚,就是推断转动边境,当转动抵达bottom和top时,就阻挠转动就好啦。

function innerScroll(e){
    // 阻挠冒泡到document
    // document上已preventDefault
    stopPropagation(e);

    var delta = e.wheelDelta || e.detail || 0;
    var box = $(this).get(0);

    if($(box).height() + box.scrollTop >= box.scrollHeight){
        if(delta < 0) {
            preventDefault(e);
            return false;
        }
    }
    if(box.scrollTop === 0){
        if(delta > 0) {
            preventDefault(e);
            return false;
        }
    }
    // 会阻挠原生转动
    // return false;
}

$('#popupLayer').on('mousewheel', innerScroll);

代码很简朴,关于scrollTop scrollHeight等诠释请看这篇文章。这里唯一要注重的是对鼠标转动值wheelDelta的猎取可以要做兼容性处置惩罚,着实有题目的话可以运用jquery-mousewheel去猎取鼠标的转动量。

上面这段代码是PC上的推断转动边境的处置惩罚,那手机上又该怎样做的,手机上没有鼠标,怎样猎取到转动量delta?

IScroll的启示

我想起“部分转动”界的大佬——IScroll,可以去看下源码,细节很庞杂然则大致构造是很清楚的。

_start: function (e) {
    
    this.startX    = this.x;
    this.startY    = this.y;
    this.absStartX = this.x;
    this.absStartY = this.y;
    this.pointX    = point.pageX;
    this.pointY    = point.pageY;

    this._execEvent('beforeScrollStart');
},

_move: function (e) {
    
    var point        = e.touches ? e.touches[0] : e,
        deltaX        = point.pageX - this.pointX,
        deltaY        = point.pageY - this.pointY;

    this.pointX        = point.pageX;
    this.pointY        = point.pageY;

},

这是iscroll中的一小段代码,这就是猎取touchmove转动量的要领。因而我们便可以写出相似上面innerScroll适用于手机上的推断转动边境的要领了。

// 挪动端touch重写
var startX, startY;

$('#popupLayer').on('touchstart', function(e){
    startX = e.changedTouches[0].pageX;
    startY = e.changedTouches[0].pageY;
});

// 仿innerScroll要领
$('#popupLayer').on('touchmove', function(e){
    e.stopPropagation();

    var deltaX = e.changedTouches[0].pageX - startX;
    var deltaY = e.changedTouches[0].pageY - startY;

    // 只能纵向滚
    if(Math.abs(deltaY) < Math.abs(deltaX)){
        e.preventDefault();
        return false;
    }

    var box = $(this).get(0);

    if($(box).height() + box.scrollTop >= box.scrollHeight){
        if(deltaY < 0) {
            e.preventDefault();
            return false;
        }
    }
    if(box.scrollTop === 0){
        if(deltaY > 0) {
            e.preventDefault();
            return false;
        }
    }
    // 会阻挠原生转动
    // return false;
});

这里要注重的是,我加了一条推断,弹出层内部的转动只能纵向滚,即 deltaY 要大于 deltaX。因为我发明个bug,当没有这条推断时,弹出层内部可以横向滚,滚出的都是空缺,人人可以本身试下。另有这里究竟运用e.changedTouches[0]照样像iscroll里的e.touches[0]猎取当前转动的手指,实在都OK,可以看下这篇文章

末了请看demo,手机请扫二维码,结果棒棒的!

【更新】注:一年前做这个demo时,我手机 ( Meizu Android 4.4.2 ) 上结果是OK的,在 SegmentFault 论坛上不止一个人复兴说上面的计划有题目,有一半机率是不可的,快速滑的时刻一定不可。

来自SF网友的计划【更新】

网友 jiehwa 的提到不须要重写事宜那末贫苦,经由历程几个 css属性 掌握即可。

  • 弹出层父元素设置属性 overflow-y: scroll

  • 弹窗弹出时,用js掌握底层元素的 position 属性置为 fixed

  • 弹窗封闭时,用js掌握底层元素的 position 属性置为 static

  • 在 iOS 端,为了弹窗内里的转动结果看起来顺滑,须要设置弹窗层的包裹元素属性:-webkit-overflow-scrolling: touch

css计划的demo(感谢 SegmentFault 网友)

可以看到有瑕疵,当强即将底层元素置为 fixed 后,因为 fixed 定位会让元素离开一般的DOM文档流,所以底本位于页面底部的元素就一会儿顶上来了。另有当底层元素滑动一段距离后再翻开弹出层,底层元素又被 fixed 定位重置了,看着也很别扭。

仔细阅读后发明我误解了,掌握底层元素的 fixed 定位应当作用在 <body> 的一级子元素,而弹出层的包裹元素也是 <body> 的一级子元素,因而 革新后的 demo 以下

如今“页面底部”这几个字不会顶上来了,然则滑动一段距离后再翻开弹出层时的页面底层照样会发抖,这个临时也想不出很好的处理计划

《由弹出层激发对转动道理的议论》

末了感谢恭弘=叶 恭弘小钗,近来一直在看他关于挪动端事宜原理的博客,有点学会了他那种 代码试验 -> 猜想诠释 -> 考证原理 -> 革新题目 如许的学习要领。本文也花了很大气力写代码试验,疏漏的地方望多多斧正,感谢耐烦的看完

参考资料

本文最早宣布在我的个人博客上,转载请保存出处 http://jsorz.cn/blog/2015/10/popup-scroll-tricks.html

    原文作者:依然宁静
    原文地址: https://segmentfault.com/a/1190000003849952
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞