吸顶效果的兼容性实现

448 views
Skip to first unread message

star

unread,
Oct 18, 2013, 1:56:49 AM10/18/13
to f2...@googlegroups.com

由于IE7+,ff,chrome均支持fixed定位方式,这篇主要研究IE6的fixed定位方式,及其他浏览器下的特殊处理。(部分内容来源于网络搜集并整理,第一次发帖,略紧张。

IE6 position:fixed的实现方法

一共有三种形式:

  • css hack

  • css expression

  • js模拟

使用css hack形式实现fixed效果

fixed定位的实现原理是基于视口坐标系进行定位,使用css实现fixed的关键点在于参照物的选择以及合理的定位方法。

  • 参照物的选取

    视口的一个特性是滚动条移动而视口不动,所以选择参照物的时候,也要依据这个特性。

    • 伪视口

    如css2标准描述,overflow可以胜任模拟视口功能,将父元素的overflow属性设置为scroll,该元素即可实现视口的功能。

    另外一个问题是视口的宽高均为100%,html节点初始化时的宽和高均为当前视口宽度的100%,所以可以基于html节点的宽度和高度设置“伪视口”的宽高。

    <!--[if IE 6]>
    <style type="text/css">
        #fixed {
            position: absolute;
            top : 20px;
            right: 20px;
        }
        html{overflow:hidden;}
        body {
            overflow: auto;
            height: 100%;
            margin:0;
            padding:0;
        }
    </style>
    <![endif]-->
    

    cssHack.html

    注意:这种实现方式有一个很大的缺点,就是伪窗口下基于html使用absolute方式定位的元素全部将变成fixed定位效果,属于“侵入式方案”,维护成本较大,尤其是使用该方案改变原有页面的布局。

    • 动态定位

    根据fixed的特性,即元素跟随滚动条滚动时与视口的位置保持一致。也就是说元素的位置是动态赋予的,基于标准的css是无法实现的,但是可以通过css表达式和js来实现。

使用css表达式

css表达式使得IE6可以动态设置css样式,在实现fixed定位上有以下几个挑战:

  • 支持复杂情况下的fixed定位

    css表达式的使用方法为,样式名:expression(表达式或者函数),如:

    /**
     * expression中的内容为表达式
     */
    #fixed{
        position:absolute;
        top:expression(Math.max(document.body.scrollTop, document.documentElement.scrollTop) + "px");
    }
    

    表达式写法的一个问题是很难写出复杂的逻辑。如果想实现复杂的逻辑,如绝对定位的元素不是相对html定位,而是相对于它的父元素;需要根据一些逻辑判断节点的定位,这时就需要使用函数,如:

    <style>
    #fixed {
        position: absolute;
        left : expression(function() {return Math.max(document.body.scrollLeft, document.documentElement.scrollLeft) + 20 + "px";}());
        top : expression(function() {return Math.max(document.body.scrollTop, document.documentElement.scrollTop) + 20 + "px";}());
    }
    </style>
    

    用函数来执行的复杂逻辑如下:

    /**
    * 如果元素的offsetParent不是html节点,该元素定位的left和top值需要根据父元素的位置来做适配
    */
    <script>
    function left(el) {
        var left = 0,
            p = el;
        while(p = p.offsetParent) {
            left += p.offsetLeft;
        }
        el.style.left = Math.max(document.body.scrollLeft, document.documentElement.scrollLeft) - left + 20 + "px";
    }
    </script>
    <style>
    #fixed {
        position: absolute;
        left : expression(left(this));
    }
    </style>
    

    有一个更有意思的实现,因为函数可以传递当前的节点,而每一个样式表达式都要去算一次,所以,可以按照如下的方案,将left以及top的计算逻辑都放到一个函数里,如下:

    <script type="text/javascript">
    function fixedHack(el) {
    var left = 0,
        top = 0,
        p = el;
    
    while(p = p.offsetParent) {
        left += p.offsetLeft;
        top += p.offsetTop;
    }
    
    el.style.left = Math.max(document.body.scrollLeft, document.documentElement.scrollLeft) - left + 20 + "px";
    el.style.top = Math.max(document.body.scrollTop, document.documentElement.scrollTop) - top + 20 + "px";
    }
    </script>
    <style type="text/css">
    #fixed {
        position: absolute;
        fixedHack: expression(fixedHack(this)); 
    }
    </style>
    

    另外一个需要注意的问题是,当前方案的css表达式的计算是在节点渲染之后的,所以伴随着滚动条的滚动会产生抖动,解决办法如下:

    <style type="text/css">
    
    /**
    * IE有一个多步的渲染进程。当你滚动或调整你的浏览器大小的时候,它将重置所有内容并重画页面,
    * 这个时候它就会重新处理css表达式。这会引起一个丑陋的“振动”bug,在此处固定位置的元素需要调
    * 整以跟上你的(页面的)滚动,于是就会“跳动”。
    * 解决此问题的技巧就是使用background-attachment:fixed为body或html元素添加一个background-
    * image。这就会强制页面在重画之前先处理CSS。因为是在重画之前处理CSS,它也就会同样在重画之前
    * 首先处理你的CSS表达式。这将让你实现完美的平滑的固定位置元素!
    */
    body{
      background-image:url(about:blank); /* for IE6 */ 
      background-attachment:fixed; /*必须*/
    }
    
    </style>
    

    cssExpression.html

  • 效率

    这里先给出一个比较极端的例子

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>IE6下记录expression的执行次数</title>
    <!--[if IE 6]>
    <script type="text/javascript">
    
    var i = 0;
    
    function trans() {
    i ++;
    document.getElementById('result').innerHTML = i;
    }
    </script>
    <style type="text/css">
    #list li {
        left : expression(trans());
    }
    </style>
    <![endif]-->
    
    </head>
    <body>
    <p id="result"></p>
    <ul id="list">
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
        <li></li>
    </ul>
    </body>
    </html>
    

    初始化的时候,记录的数值为:

    init_ce

    把鼠标沿着箭头方向移动后,记录的数值为:

    slide_ce

    可以看出来两点:

    • 用户操作使得表达式的计算量非常大
    • 计算次数与css选择器匹配的元素个数成正比

    所以在使用css表达式时要注意:

    • 在匹配了较多元素的css选择器中慎用css表达式
    • 避免计算量大的表达式,因为表达式的计算消耗和表达式的计算量成正比
    • 在参考上述第2个规则的基础上,尽可能的合并表达式的计算函数,如上文fixedHack做到的那样。

js模拟实现

使用js实现定位效果的本质是文档相对于视口有位置变化的时候(包括视口大小改变,即window.resize;用户滚动了屏幕,即window.scroll;)重新计算节点的位置。原理其实很简单,如下:

最基本的实现方式

var el = document.getElementById('floatArea');    //要浮动的dom

//当发生resize和sroll事件的时候,计算要显示的位置
//需要注意的是el的position属性为"absolute"
$(window).on('resize scroll', function() {
    var left = 0,
        top = 0,
        p = el;
    while(p = p.offsetParent) {
        left += p.offsetLeft;
        top += p.offsetTop;
    }
    el.style.left = Math.max(document.body.scrollLeft, document.documentElement.scrollLeft) - left + 20 + "px";
    el.style.top = Math.max(document.body.scrollTop, document.documentElement.scrollTop) - top + 20 + "px";
});

下面说几个比较有意思的话题,需要浮动的节点将被简称为"浮块":

抖动

抖动产生的根本原因是浮块的渲染和页面的渲染不同步。而最根本的原因是浏览器的渲染引擎决定的,首先要认清楚一个事实,那就是浏览器渲染是单线程的。滚动条滚动以及窗口的resize产生的原因来源于用户交互,而用户交互行为的句柄被放置在浏览器的事件处理队列中。说白了就是浏览器渲染结果的准确性是由浏览器处理事件队列和浏览器视图的刷新策略决定的。

我们先来看屏幕渲染图像的过程,拿LED屏幕举例,推荐的刷新频率为每秒60次,每次刷新屏幕的过程就是将新画面数据形成图像输出到屏幕。浏览器的渲染是将页面视图的数据交给图像处理器,输出到屏幕展示出来。

如果我们想在滚动条滚动的过程中准确的显示浮块的位置,就需要保证浏览器在渲染下一桢视图的时候通知浏览器浮块正确的位置。我做了如下的试验:

<html>
<head>
    <title>浏览器的渲染顺序</title>
    <meta charset="utf8">
    <style type="text/css">
    #float{width: 100px; height: 100px; background: red; position: relative;}
    body{height:3000px;}
    </style>
    <script type="text/javascript" src="../jquery.js"></script>
</head>
<body>
<div id="float"></div>
<div id="ff" style="position:absolute; top : 200px; width : 100px; right : 20px; height: 100px; background:black;"></div>
<script type="text/javascript">
function render() {
    $(window).on('scroll', function() {
        console.info('##' + $(document).scrollTop());
        $('#float').css({
            top : $(document).scrollTop()
        });
        var i = 10000000; //chrome下可适当调整数量级,建议10000000000,chrome好样的
        while(i > 0) i--;
    });
}
</script>
</body>
</html>
  • mac操作系统firefox

    点击向下箭头,通过控制台可以看到scroll执行了2次,滚动条相应的运动2次,每一次控制浮块改变位置,每次渲染的位置准确。

    • 第一次滚动

    • 第二次滚动

  • mac操作系统saffari

    点击向下箭头,通过控制台可以看到scroll执行了2次,滚动条运动2次。

    • 第一次滚动,浏览器直接渲染了滚动条操作对整个页面的滚动效果,此时滑块位置是错误的

    • 第二次滚动,浏览器继续执行向下滚动,此时滑块定位用到的位置仍然是错误的

    • 浮块向下移动,此时滑块定位到了正确的位置上

  • mac操作系统chrome浏览器

    点击向下箭头,浏览器scroll执行了2次,滚动条运动1次

    • 控制台输出scrollTop的正确值,滚动条无滑动,浮块的位置不变

    • 滚动条向下滚动到最终位置,浮块的位置发生偏移,使用的scrollTop值为第一步输出的值。

    • 浮块向下移动到正确的位置上

可以看出,每个浏览器对于滚动事件的执行策略并不相同,计算浮块的位置并交给视图渲染不能保证和当前的视图渲染同步,不同步则会最终导致抖动。

从上面的试验可以看出:

同步率最好的是firefox,每一次视图的刷新会把当前的代码执行完,这也是ff运行起来会让用户感觉略卡的一个原因。chrome和saffari在执行的时候并没有完全实现事件与视图的同步。虽然证明了不同步的存在,在现实场景中,因为很难存在如此大开销的事件,所以用户基本上感觉不到这种抖动。

IE7下诡异的错位

IE7下使用fixed定位的时候,在缩放时会产生诡异的错位现象:如缩放比为50%,浮块的定位为原比例的25%。所以在计算的时候要计算当前的缩放比,并修改浮块定位的位置。获取缩放比的代码如下:

function GetZoomFactor () {
    var factor = 1;
    if (document.body.getBoundingClientRect) {
        // rect is only in physical pixel size in IE before version 8
        var rect = document.body.getBoundingClientRect();
        var physicalW = rect.right - rect.left;
        var logicalW = document.body.offsetWidth;

        // the zoom level is always an integer percent value
        factor = Math.round ((physicalW / logicalW) * 100) / 100;
    }
    return factor;
}

requestAnimationFrame

由于浏览器的渲染桢率约为60,基于浏览器滚动事件的定位方法并不能够根据浏览器渲染进行调用,通常浏览器滚动事件的执行频率要大于浏览器的渲染桢率,这将对性能造成影响,IE10,最新版的chrome,ff,saffari提出了一个基于浏览器渲染的事件:requestAnimationFrame,使用这个方法将极大的降低浏览器的执行损耗,并保证渲染的结果正确。

兼容性的代码如下:

// handle multiple browsers for requestAnimationFrame()
var requestAFrame = (function () {
    var func = window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.oRequestAnimationFrame;

    if(!func) {
        func = function(callback) {
            return window.setTimeout(callback, 1000 / 60); // shoot for 60 fps
        };

        func.noSupportAnimationFrame = true;
    }

    return func;
})();

// handle multiple browsers for cancelAnimationFrame()
var cancelAFrame = (function () {
    return window.cancelAnimationFrame ||
        window.webkitCancelAnimationFrame ||
        window.mozCancelAnimationFrame ||
        window.oCancelAnimationFrame ||
        function (id) {
            window.clearTimeout(id);
        };
})();

//执行定位的方法体
function setPosition() {
    //计算并设置浮块的位置
    ...
    setPosition.done = true;
}

setPosition.done = true;

$(window).on('scroll.affix, resize.affix', function() {
    //为了避免重复的提交requestAframe,直到上一个计算完毕再向队列中插入下一个
    if(setPosition.done) {
        setPosition.done = false;
        requestAFrame(setPosition);
    }
})

star

unread,
Oct 18, 2013, 2:01:30 AM10/18/13
to f2...@googlegroups.com
最终版的实现,可参考https://github.com/ilife5/jquery.affix

unread,
Nov 22, 2013, 5:00:20 AM11/22/13
to f2...@googlegroups.com
讲的很全面!~

在 2013年10月18日星期五UTC+8下午1时56分49秒,star写道:
Reply all
Reply to author
Forward
0 new messages