caoruiy‘s blog

Wisdom outweighs any wealth

让fixed的元素跟随页面左右滚动

问题描述

在实际项目中,有一个需求是:

菜单需要一直固定在页面顶部,我们使用了最常见的实现方式:使用 fixed 定位,固定在顶部。

上下滑动时,没有任何问题,但是当页面比较宽度比较窄时(比如小分辨率,人为调整浏览器宽度),由于使用了 min-width 属性设置了顶部菜单的最小宽度,所以,当页面宽度小于菜单宽度时,右侧总会有一些会被遮挡住,无法显示完全。

他的样子大概像下面这样(请先别点任何按钮,查看最初的效果)(如果网速较差,可能无法显示jsfiddle的在线demo):

如果看不到DEMO效果,可以点击该链接查看这个DEMO!
https://blog.plcent.com/demo/fixed-scroll-with-page.html

测试给的要求是:
1. 当页面宽度较小时,左右滑动页面可以显示被遮挡的菜单。
2. 页面放大缩小操作后,菜单不能有显示异常,且可以正常左右滚动。

为什么fixed定位的元素,没有办法跟随滚动条滚动?

虽然这是常识性的问题,但是还是要给出一些明确的解释才能让人更为信服:MDN-CSS-position属性

MDN的文档中解释说:position:fixed 定位时,是通过指定元素相对于屏幕视口(viewport)的位置来指定元素位置。元素的位置在屏幕滚动时不会改变。

相对于屏幕定位,这就是原因。

最初的解决办法

DEMO:

<div id="banner" class="bannerFixed"> 
    顶部fixed的导航,position:fixed; min-width:1280px;
</div>
<div id="content" class="content">
    页面可以滚动的内容,position:relative; min-width:1280px;
</div>

所有,我们一下子就想到了,使用JS来修改菜单的 left 值为 window.scrollLeft来解决这个问题,当屏幕滚动向左滚动时,设置菜单的left值为 向左滚动的距离,这样不就可以了。开心了写了代码:

$(window).on('scroll', function(){
    var sl=-Math.max(document.body.scrollLeft,document.documentElement.scrollLeft);
    // 设置left为向左滚动的距离
    $('#banner').css({left:sl})
})

代码的效果可以点击DEMO中滚动时设置left属性 按钮后,点击 添加滚动监听事件 按钮来查看。

查看效果时,我们非常开心的觉得解决了问题,但是,当我们放大页面时,我们立马发现了问题,菜单滚动的速度明显快与下面页面滚动的速度。

放大页面时,页面元素改变了什么?

查阅资料时也发现了有人有这样的问题,也是这样解决的:segmentfault:div设置了position: fixed属性后如何可以做到随浏览器左右移动?

但是,这样的效果肯定是不行的。

可以,为什么会出现当页面放大时,banner部分和content部分滚动速度不一致的情况呢?

通过DEMO上的案例观察,我们可以发现,当我们不对页面进行放大时,左右滚动页面,只有banner的left在改变,这是我们期望的。但是当放大页面时,我们banner的left还在变化,不同的是:content的 offsetLeft 也在变化,当 content的 offsetLeft 增大到某一个值时,不再变化。这时也可以同步滚动了。

现在的问题就转变成:为什么放大页面时, position:relative 的元素(content)offsetLeft 会增大??

这里说的 offsetLeft 是 jQuery中 $('#content').offset().left 的值。原生的 element.offsetLeft 属性是只读属性,无法修改!

因为:content的offsetLeft的变化,导致了banner和content的移动速度不同。

在解释这个原因之前,需要了解一些概念!

先了解一些概念

jQuery的offset()实现原理

不想去讲源码,其实现原理可以简单的说成:

ele.offset().left = ele.getBoundingClientRect().left + window.pageXOffset – document.documentElement.clientLeft

ele.offset().left = 元素相对于视窗的左边距 + 文档向左滚动的像素 – 元素左侧边框的宽度

ele.offset().left = ele.getBoundingClientRect().top + window.pageYOffset – document.documentElement.clientTop

ele.offset().top = 元素相对于视窗的顶部的边距 + 文档向上滚动的像素 – 元素顶部边框的宽度

视口(viewport)

简单的来讲,可以把视口理解成浏览器可以显示页面内容部分的大小,所有html元素对应的就是视口的大小。

更多细节可以阅读文章:两个viewport的故事(第一部分): 的 “概念:viewport” 部分。

window.pageXOffset 和 ele.clientLeft 以及 ele.getBoundingClientRect()

window.pageXOffset 是滚动条向左滚动的距离,他和 window.scrollLeft是一个意思,为了兼容性,请使用前者。参照:MDN-Window.pageXOffset

ele.clientLeft 是元素左边框的宽度,参照:MDN-Element.clientLeft

ele.getBoundingClientRect() 返回元素的大小及其相对于视口的位置。参见:MDN-Element.getBoundingClientRect()

如果你对 clientLeft offsetLeft style.left 这些概念不甚了解,可以参照CSDN博文:js中的scrollleft、style.left、clientLeft、offsetLeft

解释

  1. 先解释第一个问题,为什么banner会快速移动?

首先页面放大,页面banner 和content 本身没有发生位置变化,当向左移动时,顶部banner本身也在移动,但是由于多设了向左移动的left,所有banner会以2倍的速度向左移动。

当移动到页面原始大小时的最右侧,由于banner本身不可以移动,作用在banner身上的只有left的值,所有可以看到正常移动。

  1. 第二个问题就是,为什么content的offset().left会不断增大,直到固定在某一个值?

有了上面的了解和概念,特别是针对Jquery的实现,我们可以很明显的知道,为什么content的offset.left会增大,并且可以当页面宽度较小时,offset.left 增大到一定值之后会保持不变。

其原因在于:页面放大时,原来不可以左右滚动的页面,由于视口宽度没变,但是页面宽度却变大了,为了展示原来的页面内容,页面本身就可以滚动了,content的offset.left变化是由于jQuery的实现中,加上了 window.pageXOffset, 所有随着页面滚动,content的offset.left会不断增加,当页面本身滚动到最右侧时(也就是显示出没有放大时,页面可以显示的最右侧内容时),页面将不能继续向左滚动。但是,content本身是可以左右滚动的,所以,元素本身相对于视口左侧的距离不断增加[content.getBoundingClientRect().left是负值,且不断增加],页面向左侧滚动的距离不断增大[window.pageXOffset是正值,且不断增加], 正负值增加相同,所以其本身offset().left 不会变化。

与此同时,banner是相对于视窗定位的,所以本身的banner.getBoundingClientRect().left 始终是0,所以他的offset().left会不断增加。

所有此时的效果是:当页面不放大时,可以正常移动。当页面放大时,在页面移动到页面最右侧之前,banner以2倍的scollLeft速度向左移动,在移动到页面最右侧之后,可以正常滚动。

如何兼容放大和缩小的情况?

新方法

所有,我们换了一个思路,当页面向左移动到页面最右侧时,才为banner添加 banner.left = winodw.pageXOffset , 而这个临界点,可以监听 content 的 scroll 事件,

$("#content").on('mousewheel', function(){
    console.log('asd')
    var uiViewOffsetLeft = $(this).offset().left
    var scrollLeft = $(window).scrollLeft();
    var different = scrollLeft - uiViewOffsetLeft;
    if(different > 0){
        $("#banner").css({left:-different})
    }else {
        $("#banner").css({left:0})
    }
})

以上的方法是可以的,但是遗憾的是,在safari下,页面的滚动策略和其他浏览器不同,无法触发content的scroll事件。而 mousewheel 事件也只在手指滑动的过程中触发一次。

但是幸运的是,jquery的 offset() 方法为我们提供了设置元素 offset.left 的功能(需要注意的是:元素的offsetLeft属性是只读的,jQuery的方法也是变相的修改元素的left值,来确保语义上的offset.left),所有我们可以把上下两个元素的offset.left射程相同的值,即可保证上下滚动时,保持相同的速度。

最终的办法

$(window).on('scroll', function(){
    $('#banner').offset({left:0})
    $('#content').offset({left:0})
});

这样,jQuery会为我们自动设置left值,保证两个元素的对齐,从而保证在放大和缩小时,都可以有相同的滚动速度。

有时候大费周章,最后可能只有一两行代码就解决了问题

附录

DEMO演示参见:https://jsfiddle.net/caoruiy/zek60fqt/2/

相关问题:

segmentfault:设置fixed,为什么不能随屏幕位置改变?

点赞
  1. 八达说道:

    感受学习的力量!

发表评论

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