Wonderjar's Blog

ScrollChaining链式滑动的解决办法

2018-03-28

滑动传递

滑动在移动端是非常常见的操作,当出现一个或者多个支持滚动的叠加层时,即使是滚动最上层,滚动也会作用到所有层级,比如这样

1
2
3
4
<div style="text-align: left;">
<video controls="controls" src="/videos/chatbox-chaining.mp4" style="width: 150px; height: 300px;">
</video>
</div>

如果不希望滚动传递,最先想到的大概就是监听最上层叠加层的touchmove事件,然后prevent它

边缘滑动传递

这样确实工作,但接下里会发现另一个问题,当上层滚动到边缘之后,这个滚动又被传递到了下层,比如这样

1
2
3
4
<div style="text-align: left;">
<video controls="controls" src="/videos/modal-off.mp4" style="width: 450px; height: 300px;">
</video>
</div>

为什么还会向外传递滑动,不是已经prevent了?
这是因为当没有滑动到边界时,当前overflow: scroll的元素能”消化”这个滑动,会触发滑动事件,但到了边界之后,当前元素不能”消化”这个滑动,就由它的支持scroll的父元素来”消化”这个滑动
对于这个行为,IE过去就有一个CSS属性

-ms-scroll-chaining: chained | none
Specifies the scrolling behavior that occurs when a user hits the scroll limit during a manipulation.
而在CSS3中,也终于有了这一增强,
The overscroll-behavior property is a new CSS feature that controls the behavior of what happens when you over-scroll a container (including the page itself). You can use it to cancel scroll chaining, disable/customize the pull-to-refresh action, disable rubberbanding effects on iOS (when Safari implements overscroll-behavior), and more. The best part is that using overscroll-behavior does not adversely affect page performance like the hacks mentioned in the intro!
The property takes three possible values:
auto - Default. Scrolls that originate on the element may propagate to ancestor elements.
contain - prevents scroll chaining. Scrolls do not propagate to ancestors but local effects within the node are shown. For example, the overscroll glow effect on Android or the rubberbanding effect on iOS which notifies the user when they’ve hit a scroll boundary. Note: using overscroll-behavior: contain on the html element prevents overscroll navigation actions.
none - same as contain but it also prevents overscroll effects within the node itself (e.g. Android overscroll glow or iOS rubberbanding).
好了,有了这个属性,看起来问题被很优雅地解决了,不过不幸的是,在我测试下,我的手机就不支持这个CSS属性

Tricks

CSS不支持,那为了隔绝掉这个影响,可以用些小办法,比如借助事件,当上层打开时,通过事件通知到下层的组件,将下层组件的overflow属性改掉,从而取消掉下层组件的滑动,当上层关掉时,同样通过事件通知恢复下层的滚动功能
我之前有次做微信上那种介绍网页,就是一页一页可以上下滑动的,但向下滑的时候,会优先触发微信浏览器的默认行为,就是整个浏览窗被向下拖动了,后来用这段代码搞定
vue component中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
created() {
var overscroll = function(el) {
el.addEventListener('touchstart', function() {
var top = el.scrollTop
, totalScroll = el.scrollHeight
, currentScroll = top + el.offsetHeight;
//If we're at the top or the bottom of the containers
//scroll, push up or down one pixel.
//
//this prevents the scroll from "passing through" to
//the body.
if(top === 0) {
el.scrollTop = 1;
} else if(currentScroll === totalScroll) {
el.scrollTop = top - 1;
}
});
el.addEventListener('touchmove', function(evt) {
//if the content is actually scrollable, i.e. the content is long enough
//that scrolling can occur
if(el.offsetHeight < el.scrollHeight)
evt._isScroller = true;
});
}
setTimeout(() => {
overscroll(document.querySelector('.needScroll'));
}, 500);

document.body.addEventListener('touchmove', function(evt) {
//In this case, the default behavior is scrolling the body, which
//would result in an overflow. Since we don't want that, we preventDefault.
if(!evt._isScroller) {
evt.preventDefault();
}
});
},

//避免影响其他页面
beforeRouteLeave(to, from, next) {
console.log('leave intro');
let scrollDiv = document.querySelector('.needScroll');
if(scrollDiv) {
scrollDiv.addEventListener('touchstart', function(){});
scrollDiv.addEventListener('touchmove', function(){});
}
document.body.addEventListener('touchmove', function(){});
next();
},