一.场景
“吸顶”是一种比较老的交互方式,在PC页面已经用了很多年了,如图:
sticky
吸顶元素的初始位置一般靠近页面顶部,但与顶部有一定距离,这块区域放的是最醒目的元素,比如Banner图。页面向下滚动超过吸顶元素初始位置时,把吸顶元素固定在顶部
要求吸顶的元素一般是二级导航栏、搜索框、文章标题栏(h1
)、表头(thead
)、tab条等等,共同特点是在内容或功能上比较重要,但又不是最重要的元素(最重要的元素通常固定在页面顶部,navbar-fixed-top
)
二.PC解决方案
页面滚动到一定位置时,做一些事情
“回到顶部”按钮也是这样的,页面向下滚动超过150px
时,显示该按钮,否则隐藏
所以实现思路是监听scroll
事件:
var stickyEl = document.querySelector('.sticky');
var stickyT = stickyEl.offsetTop;
window.onscroll = function(e) {
var scrollT = document.body.scrollTop;
// console.log(scrollT, stickyT);
if (scrollT > stickyT) {
stickyEl.classList.add('fixed-top');
}
else {
stickyEl.classList.remove('fixed-top');
}
};
和“回到顶部”的实现方式一模一样,效果好像还不错,但很快会发现滚动到临界位置stickyT
的时候,页面抖了一下,向上缩了一截。因为stickyEl
此时fixed
出去了,下面的元素上来,抢占sticky元素老家,所以页面抖了一下
我们希望平滑,不要抖动,所以还需要一个占位符,守住stickyEl
老家:
var stickyEl = document.querySelector('.sticky');// 守家占位符
var stickyHolder = document.createElement('div');
var rect = stickyEl.getBoundingClientRect();
// console.log(rect);
stickyEl.parentNode.replaceChild(stickyHolder, stickyEl);
stickyHolder.appendChild(stickyEl);
stickyHolder.style.height = rect.height 'px';var stickyT = stickyEl.offsetTop;
window.onscroll = function(e) {
var scrollT = document.body.scrollTop;
// console.log(scrollT, stickyT);
if (scrollT > stickyT) {
stickyEl.classList.add('fixed-top');
}
else {
stickyEl.classList.remove('fixed-top');
}
};
把吸顶元素用相同高度的占位符包起来,临界位置stickyEl
被fixed
出去,空间由stickyHolder
撑起来,下面元素挤不上来,页面不抖了
这样做还有一些问题,吸顶元素上方的各个元素加载很慢的话,拿到的stickyT
比实际的小,甚至为0(如果上方是一张很大的Banner图的话)。所以需要配合默认图片占位符(base64)使用,或者偷懒先用min-height
顶着,上方图片onload
时再修正stickyT
三.移动端解决方案
从原理上看,直接搬过来是可以的。在Android 4.0 确实可以,但IOS几乎全家都行不通
Android scroll
Android 4.0的scroll
事件不那么实时(自带节流的感觉),但Android 4.1之后scroll
事件和PC几乎没什么区别
The Android browser in Ice Cream Sandwich fires the event but doesn’t feel very responsive and only sporadically re-paints the DOM to move the blue box. Luckily, Jelly Bean’s Android browser handles this example perfectly; everything is updated and rendered smoothly as the user scrolls.
(引自参考资料1)
只要页面还在滚动,scroll
事件就疯狂触发,需要手动节流,这正是我们需要的效果。如果scroll
本身自带节流,就很容易错过临界点判断,导致吸顶元素“跳一下”,体验不平滑
IOS scroll
IOS 8-的Safari,包括UIWebView
,对scroll
事件做了很大限制:
手指划动屏幕 -> 滚动 -> 手指抬起 -> 惯性滚动 -> 停止滚动
整个过程,直到停止滚动时才会触发1次scroll
事件,也就是说,IOS8以下的scroll
变成了scrollend
。监听滚动判断位置的方法完全失效,平滑吸顶效果变成了滚过临界位置直到停止滚动时,吸顶元素跳到目标位置,体验非常差,不可忍受
scroll
不能用,但还可以有一些奇怪的思路,比如定时器读scrollTop
,touchmove
,iscroll
等等
有前辈做了详细测试,见参考资料1
定时器在手指没有离开屏幕时不会执行,touchmove
触发频率足够,也能拿到scrollTop
,但touchend
后,惯性滚动期间,没有任何事件可用,拿不到这段的scrollTop
,很难预测这段惯性滚动距离(减速运动),甚至不确定各IOS版本这段距离的计算方式是否相同
iscroll
这种假滚动,自然可以实时获取滚动位置,iscroll
有一个专用版本来做这个事情:
iscroll-probe.js, probing the current scroll position is a demanding task, that’s why I decided to build a dedicated version for it. If you need to know the scrolling position at any given time, this is the iScroll for you. (I’m making some more tests, this might end up in the regular iscroll.js script, so keep an eye on it).
IOS 8 的Safari和WKWebView
能够疯狂触发scroll
,无论手指在不在屏幕上,无论是不是惯性滚动期间。但IOS 8 的UIWebView
,scroll
限制还在
如果要支持IOS 8-设备以及任意IOS版本的UIWebView
,此路不通,忘掉scroll
sticky
虽然scroll
方案行不通,但IOS提供了另一种方式:position: sticky
,自IOS 6.1就支持了,最近Chrome56才支持
这个CSS规则专门负责吸顶,一般用法:
代码语言:javascript复制.sticky {
// 滚过初始位置时自动吸顶
position: -webkit-sticky;
position: sticky;
// 吸顶时的定位
top: 0;
left: 0;
// z比下方所有z高
z-index: 9999;
}
没有滚过初始位置时,和position: relative
表现类似(占据空间,!static
能为后代元素提供定位参照),但top
和left
无效
滚过初始位置时,和position: fixed
表现类似,top
和left
生效,固定在屏幕可见区域,但页面不会抖动,原本占据的空间还在(自带守家占位符的感觉)
吸顶效果非常平滑,比Android scroll
方案体验更平滑,但限制很明显,无法实时获知吸顶状态,于此相关的各种效果都受限制,比如吸顶tab列表:
sticky-tab
非吸顶状态时可以划动列表部分,让页面滚动,转到吸顶状态,多个tab列表无缝切换,浏览状态互不影响 吸顶状态时划动当前tab列表,到头,让页面滚动,转到非吸顶状态
也就是说,非吸顶状态时,让tab列表不能滚动(overflow-y: hidden
);吸顶状态时,让tab列表可以滚动(overflow-y: auto
)
但是IOS sticky不由我们控制,且无法实时获知吸顶状态,想要获知吸顶状态的话,又回到了最初的问题,页面滚动过程中,怎样实时获知滚动条位置?CSS sticky
并不能解决这个问题
笔者还没有找到合适的解决方案,目前方案是牺牲tab浏览状态独立性,多tab共用body
的滚动条,切换tab时滚回之前的位置。这样做避免了判断吸顶状态,但牺牲了tab列表无缝切换的完美体验
如果有新思路、好点子,或者成熟方案,麻烦告知,感激不尽
四.在线Demo
- PC、Android 4.0 及
WKWebView
方案:http://www.ayqy.net/temp/sticky/sticky-pc.html - IOS 6.1 方案:http://www.ayqy.net/temp/sticky/sticky-ios.html
五.总结
- 一般元素吸顶:Android用
scroll
方案,在效果可接受范围内手动节流,提升性能;IOS用CSSsticky
,如果不需要兼容IOS 8-以及任意版本UIWebView
的话,也可以采用scroll
方案 - 吸顶tab列表:没有好的解决方案,暂用牺牲无缝切换的方案
整页iScroll是一个冒险方案,页面复杂的话,不要轻易尝试,即便页面不复杂,也难保以后不会变得复杂
参考资料
- onscroll Event Issues on Mobile Browsers:一篇详尽的
scroll
事件测试,帮很多人节省了很多时间 - Why the Scroll Event Change in iOS 8 is a Big Deal:实例介绍IOS8取消
scroll
事件限制后的变化,也是上面的前辈写的 - javascript scroll event for iPhone/iPad?:图解IOS的
scroll
事件限制 - CSS “position: sticky” – Introduction and Polyfills:polyfills都是针对PC的,没什么用
- Can I use