关于IOS设备window.onscroll滚动条滚动事件不触发的问题

写在前面:此文很久之前就写好了,一直没发。当时是微信正在统一的更换 WKwebview 内核。正常的 UIwebview 不会出现这个问题,于是定位成 Safari 的通病。

这两天遇到一个奇葩事,在ios设备上,为了节约设备事件开销,window.onscroll 触发频率极低,只在停止滚动或页面回到顶部、底部才触发,有时甚至不触发。。。

后来去google了这方面的文章,几乎都说用 touchmove 代替 scroll,但是这有一个问题,就是 touchmove 的触发频率依旧不如 scroll 的频率高,当然最重要的问题是,当 touchmove 结束的时候,ios 的页面具有一个类似于惯性的滑动特性,此时发生的滚动,既不能被 window.onscroll 捕获,也不不会执行 touchmove 的代码,甚是蛋疼。

再次,我查到了很多文章说,从 ios8.0 之后苹果放开了 scroll 的触发频率,但是事实摆在眼前,人家确实不会触发啊。。。。总不能苹果放开之后觉得不妥,又改回去了?(参考文章很多,我这里提供一篇 http://developer.telerik.com/featured/scroll-event-change-ios-8-big-deal/)

 

经过数次研究,仍然不能解决这个问题,但是后来得到了几个套路:

1.setInterval

我想到的最简单的办法,就是setInterval, 模拟 scroll 的不间断询问过程。

后来发现,这个方法,行不通。因为 iOS 虽然不会暂停JavaScript执行,但它会暂停绘制。因此,应用程序的JavaScript将继续运行,但是在滚动操作完成之前,DOM的任何更改都不会被绘制。所以,即使我们监听到了页面发生了滚动,我们也可以进行运算,但是我们对页面上的元素展现无能为力。无论我们做什么,在滚动停止之前都不会绘制到页面上。

2.使用 touchmove 代替 scroll

touchmove 的触发频率没有 scroll 的频率高,貌似 scroll 每 10px 就会触发一次,但是touchmove 的触发频率似乎与手指移动的距离有关?但是对于我们监听滑动事件,它的触发频率已经足够高了。

但是 touchmove 事件有一个十分致命的缺点,就是当手指滑动的比较快的时候,手指离开屏幕的时候,滑动并未停止,还是会继续惯性的滑动一段距离。这个时候,我们是没有办法对这个惯性进行控制的。当然,好消息是,我们可以关闭这个惯性效果:

-webkit-overflow-scrolling 属性控制元素在移动设备上是否使用滚动回弹效果.

auto  :使用普通滚动, 当手指从触摸屏上移开,滚动会立即停止。

touch :使用具有回弹效果的滚动, 当手指从触摸屏上移开,内容会继续保持一段时间的滚动效果。继续滚动的速度和持续的时间和滚动手势的强烈程度成正比。同时也会创建一个新的堆栈上下文。

-webkit-overflow-scrolling: touch; /* 当手指从触摸屏上移开,会保持一段时间的滚动 */
-webkit-overflow-scrolling: auto; /* 当手指从触摸屏上移开,滚动会立即停止 */

但是,上面这么做,虽然可以解决问题,但是会使滑动没有之前的那么灵动。总体来说,可行但是不太完美。所以我也没有使用这个方法。

3.touchmove+scroll(我正在使用的)

$("#page1").on('touchmove', function () {
    // do something
});
//ps:我真的已经尽力了。。但是ios对于scroll的触发频率太低了,只在停止滚动或页面回到顶部、底部才触发,希望后来者居上,能够修复这个问题。
$(window).on("scroll", function (e) {
    // do something   
})

中间的注释我留着呢。。我真的想不到更好的解决办法了。。。

解释一下,通过touchmove,可以精确的控制用户在滑动的时候是会不断的触发滚动事件,并作出响应的。至于滑动后的惯性滑动,则可以交给 scroll 来处理,应为scroll 正好可以在滚动结束的时候触发一次,这一次触发,刚好可以解决惯性带来的问题。

其实,我们正则匹配 agent 里面的 ios 或者 safari,然后针对这些设备绑定 touchmove 事件, 用于解决安卓 既会触发touch又会触发scroll的尴尬。

至于安卓和chrome,人家很好,不用关心。。。

4.模拟滚动

祭出终极解决方案:iscroll!或者完全禁用本机滚动,并使用JavaScript来模仿滚动。

$('window').on('touchmove', function(event) {
    //Prevent the window from being scrolled.
    event.preventDefault();

    //Do something like call window.scrollTo to mimic the scrolling
    //request the user made.
});

iScroll 大体是通过 CSS translation 此类的技术实现的滚动,便于安装和引入,既能够实现平滑的滚动,又可以持续不断的触发滚动事件。可以解决我们的问题。

当然,如果你的项目框架迫使你不能引入第三方库的话,iScroll 可能就不能用了(这也是我没有使用的原因),但我们仍然可以使用上述代码,来模拟滚动。

5. 特殊情况。

如果你是想制作一个在滚动时自动定在屏幕上方的区块(如下图),那么你可以分别针对安卓和ios设置不同的实现方式。

上图中,Iphone8的title刚加载时是在一个位置,但是当屏幕滚动后,会自动定位到屏幕的最上方,并固定住。如果是简单的实现这个功能,建议:

1、安卓上直接使用 scroll 的数值判定,并设置这个元素的position。(因为安卓上的css支持情况良莠不齐,且素质不高)

2、ios,推荐使用CSS的 sticky 属性。

设置了sticky的元素,在屏幕范围(viewport)时该元素的位置并不受到定位影响(设置是top、left等属性无效),当该元素的位置将要移出偏移范围时,定位又会变成fixed,根据设置的left、top等属性成固定位置的效果。

可以知道sticky属性有以下几个特点:

  • 该元素并不脱离文档流,仍然保留元素原本在文档流中的位置。
  • 当元素在容器中被滚动超过指定的偏移值时,元素在容器内固定在指定位置。亦即如果你设置了top: 50px,那么在sticky元素到达距离相对定位的元素顶部50px的位置时固定,不再向上移动。
  • 元素固定的相对偏移是相对于离它最近的具有滚动框的祖先元素,如果祖先元素都不可以滚动,那么是相对于viewport来计算元素的偏移量

sticky的使用需要添加webkit前缀,支持情况如下: