前面《iOS 与 弹性滚动》里讲到,iOS 的 UIWebkit 内核浏览器中启用弹性滚动后,滚动事件不会立即触发的问题。不过话说回来,绑定 scroll
本来就对整体 UI 性能影响很大,某些通常需要绑定 scroll
事件的东西其实有其他更为简便的实现方式。
比如说有这样一个很常见需求:一个长列表里分了很多小节,每个小节有一个头部标题。要求当各小节尚未完全滚动到屏幕外时,小节的头部标题始终固定在屏幕顶部。好多人一看到“滚动”这个词就直接监听 scroll
事件开始搞,其实对于这个需求有更好的、更方便的解决方式,这就是本文的主角: position: sticky
MDN 上的解释
The box position is calculated according to the normal flow (this is called the position in normal flow). Then the box is offset relative to its flow root and containing block and in all cases, including table elements, does not affect the position of any following boxes. When a box B is stickily positioned, the position of the following box is calculated as though B were not offset. The effect of ‘position: sticky’ on table elements is the same as for ‘position: relative’.
我是看了几遍没看懂,但根据实践, sticky
(下文我把他翻译为粘性定位)是这样一种定位方式。如果有
#one { position: sticky; top: 10px; }
当 #one
处于可视范围内时, #one
表现的就像一个普通的 static
元素(但是它仍非 static
定位,所以仍然是内部元素的 offsetParent
)。
当 #one
处于可视范围之外(相对于 #one
外层的第一个非 overflow: visible
元素而言,如果没有则为整个 window
,这里以 #parent
代替), #one
即被“粘”在 #parent
的顶部。
其中 10px
是 #one
的上边框( border-box
)至 #parent
的上边框( content-box
)的距离。如果未设置,则粘性定位效果对于该边不起作用。
当 #one
与 #parent
中间还有其他静态定位的父级元素, #one
将被限制在其父级元素之内。当与 条款2 冲突时,本条款覆盖上一条。
对 table
元素无效,相当于 position: relative
。
说起来比较抽象,下面以几个示例说明。
例子中,整个 window
为一个滚动区域,所有 dt
相对于 window
粘性定位。向下滚动时,所有 dt
会堆叠到窗口顶部。
例子中, #wrapper
嵌在 #parent
内,构成一片较大的滚动区域, #child
处于 #wrapper
的中心,相对于 #parent
( #child
外层第一个非 position: static
的元素)粘性定位。可以看到,无论怎样滚动,中间的黑框 #child
始终处于 #parent
的内部。
为了便于更好的说明 条款3,下面的例子对上面两例略加修改
本例将 例1 略微改动,把各个 dt
单独放入一个 dl
中。向下滚动时可以看到类似下面的 dl
把上面的 dl
顶上去的效果。 其实并非如此,多个粘性定位元素并无关联。产生这样效果的原因仅仅是因为 dt
的父元素 dl
整个都被滚动到了窗口外, dl
随之把粘性定位的 dt
给带走了
本例将 例2 略微修改,给 #child
包了一层大小一致的 div#wrapper1
,同样是水平竖直居中,粘性定位却“失效”了。其实原因与 例3 一样,粘性定位的 #child
只是被 #wrapper1
牢牢地固定住了,定位效果并未失效。
首先就是引题中的需求:固定列表头。示例 1、3 已经实现了这个效果。不仅仅是列表头,文档头、段标题,甚至两边的侧边栏都可以用——如果你想给侧边栏一个浮动效果的话。一个例外是表格的标题栏,可以看到 MDN 的最后一句话: position: sticky
对表格元素不起作用,当然你完全可以用别的方式模拟 table
布局。
首先最大的优点:有了它我们不用再绑定恶心、缓慢,还有各种兼容性问题的 scroll
事件了。其次:简单。设置一个 CSS 属性的事情干嘛要 JS 操心,布局的东西本来就应该使用纯 CSS 实现。最后: position: sticky
与 -webkit-overflow-scrolling: touch
相性极佳,滚动效果无比顺滑,并非 scroll
事件可以模拟。
这是一个不可避免的问题。很不辛,Android 阵营全部阵亡。Chrome 当前的状态是 In development ,canary 版本上已经可以体验到其初步实现,相信不久之后就会看到 Chrome(Blink) 的正式支持。除 Safari 阵营外,Firefox 也已经支持了此属性,建议调试粘性定位效果时在 Firefox 上调试,怎么说也比 Safari 的调试器好用。
另外还是由于 iOS 的限制,所有的 iOS 浏览器包括 Chrome 在内,和其他内置的比如微信内嵌浏览器全部支持此属性。
顺便一提 Edge 的状态是 Under Consideration ,微软的浏览器怎样都好了。。。
在 Chrome 的原生粘性定位实现来临前,我们仍需要一个 fallback 实现,使用 absolute 模拟 sticky 效果。简单起见,这里只考虑纵向滚动 window 的情况,以例 http://codepen.io/CarterLi/pen/qZmKzX 为基础做修改。
原始页面如下所示
- var n = 1 while n <= 20 dl dt= 'TITLE ' + (n++) each val in [1, 2, 3, 4, 5, 6, 7, 8, 9] dd= val + ' ' + val
将标题行用一个 div 包一层,用于绝对定位元素之后给原位置占位。
- var n = 1 while n <= 20 dl dt div= 'TITLE ' + (n++) each val in [1, 2, 3, 4, 5, 6, 7, 8, 9] dd= val + ' ' + val
首先需要检测浏览器是否支持 position: sticky
var elem = document.createElement('div'); elem.style.position = '-webkit-sticky'; elem.style.position = 'sticky'; if (elem.style.position.indexOf('sticky') < 0) { // 当前浏览器不支持粘性定位,需要 fallback 实现 }
预先把占位 div 的高度设置好
Array.prototype.forEach.call(document.querySelectorAll('dt'), function (elem) { elem.style.height = elem.clientHeight + 'px'; });
监听 scroll 事件,遍历所有标题行,找到需要 sticky 效果的行,添加类名 sticky
。
addEventListener('scroll', function() { var stickyElements = document.querySelectorAll('dt'); for (let idx = 0; idx < stickyElements.length; ++idx) { var elem = stickyElements[idx]; // 对于滚 window 而言,BoundingClientRect 就是元素的视口坐标值 var clientRect = elem.getBoundingClientRect(); // 如果标题行被滚到了窗口外 if (clientRect.top < 0) { const parentBottom = elem.parentElement.getBoundingClientRect().bottom; if (parentBottom < 0) { // 如果父元素整个区域都滚动到了窗口外,则去除标题行的 sticky 类 elem.classList.remove('sticky'); } else { // 添加 sticky 类,将标题行固定在顶部 elem.classList.add('sticky'); // 动态计算 style.top,表现推上去的效果 elem.style.top = Math.min(0, parentBottom - clientRect.height) + 'px'; } } else { // 如果标题行还在窗口内部或下面,则中断循环,将其后的所有标题行的 sticky 类全部删除 // 用于解决用户滚动太快时的问题 for (let j = idx; j < stickyElements.length; ++j) { stickyElements[j].classList.remove('sticky'); } break; } } });
CSS 代码中添加 sticky 类,用于置顶标题栏
.sticky div { position: fixed; left: 0; right: 0; top: inherit; }
完整示例: http://codepen.io/CarterLi/full/LNwJmq