吸顶效果解决方案

一.场景

“吸顶”是一种比较老的交互方式,在PC页面已经用了很多年了,如图:

sticky

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');
    }
};

把吸顶元素用相同高度的占位符包起来,临界位置stickyElfixed出去,空间由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不能用,但还可以有一些奇怪的思路,比如定时器读scrollToptouchmoveiscroll等等

有前辈做了详细测试,见参考资料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+的UIWebViewscroll限制还在

如果要支持IOS 8-设备以及任意IOS版本的UIWebView,此路不通,忘掉scroll

sticky

虽然scroll方案行不通,但IOS提供了另一种方式:position: sticky,自IOS 6.1就支持了,最近Chrome56才支持

这个CSS规则专门负责吸顶,一般用法:

.sticky {
    // 滚过初始位置时自动吸顶
    position: -webkit-sticky;
    position: sticky;
    // 吸顶时的定位
    top: 0;
    left: 0;
    // z比下方所有z高
    z-index: 9999;
}

没有滚过初始位置时,和position: relative表现类似(占据空间,!static能为后代元素提供定位参照),但topleft无效

滚过初始位置时,和position: fixed表现类似,topleft生效,固定在屏幕可见区域,但页面不会抖动,原本占据的空间还在(自带守家占位符的感觉)

吸顶效果非常平滑,比Android scroll方案体验更平滑,但限制很明显,无法实时获知吸顶状态,于此相关的各种效果都受限制,比如吸顶tab列表:

sticky-tab

sticky-tab

非吸顶状态时可以划动列表部分,让页面滚动,转到吸顶状态,多个tab列表无缝切换,浏览状态互不影响

吸顶状态时划动当前tab列表,到头,让页面滚动,转到非吸顶状态

也就是说,非吸顶状态时,让tab列表不能滚动(overflow-y: hidden);吸顶状态时,让tab列表可以滚动(overflow-y: auto

但是IOS sticky不由我们控制,且无法实时获知吸顶状态,想要获知吸顶状态的话,又回到了最初的问题,页面滚动过程中,怎样实时获知滚动条位置?CSS sticky不能解决这个问题

笔者还没有找到合适的解决方案,目前方案是牺牲tab浏览状态独立性,多tab共用body的滚动条,切换tab时滚回之前的位置。这样做避免了判断吸顶状态,但牺牲了tab列表无缝切换的完美体验

如果有新思路、好点子,或者成熟方案,麻烦告知,感激不尽

四.在线Demo

五.总结

  • 一般元素吸顶:Android用scroll方案,在效果可接受范围内手动节流,提升性能;IOS用CSS sticky,如果不需要兼容IOS 8-以及任意版本UIWebView的话,也可以采用scroll方案

  • 吸顶tab列表:没有好的解决方案,暂用牺牲无缝切换的方案

整页iScroll是一个冒险方案,页面复杂的话,不要轻易尝试,即便页面不复杂,也难保以后不会变得复杂

参考资料

吸顶效果解决方案》上有3条评论

  1. ayqy

    document.body.scrollTop在Chrome下始终为0,建议使用:

    (document.documentElement || document.body.parentNode || document.body).scrollTop;
    
    回复
  2. zhangjiashun

    网上找了好多所谓的‘解决方案’都没能解决,直到来到这个页面。不仅是方案,还有原理,知道了原理我自己就能解决了。感谢,微信号已关注

    回复

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code