折腾:瀑布流布局(基于多栏列表流体布局实现)

一、开篇无关紧要的话

今天四处闲逛,看到迅雷UEDxwei兄写了篇名为“浅谈个人在瀑布流网页的实现中遇到的问题和解决方法(http://cued.xunlei.com/log031)”的文章,我两只沉沉的萝卜眼顿时放出无数闪亮的小星星。

倒不是文章本身,而是可以用来制作demo的图片资源啊,啊咔咔(得瑟中……)!!

因此,本文即将展示的demo中的图片都有迅雷UED提供,这里先郑重感谢。

二、稍稍要紧的话

跟风,尤其受pinterest的煽风点火,瀑布流现在不少人关注。我正好最近比较闲,加上有人曾在我站点提出希望我介绍点瀑布流的东西,所以,今儿个也随下大流。

pinterest以及上面迅雷UED xwei的瀑布流demo(至少在FireFox下还是有致命的显示bug的)都是采用的绝对定位实现的,有相对复杂的位置计算。

我一向不喜欢吃别人嚼过的米饭,于是尝试使用另外的原理实现。我是个流体布局控,对绝对定位啊、浮动啊什么的一向没什么好感,于是,这里要介绍的就是基于多栏列表流体布局实现的瀑布流布局效果。

大致结构、布局见下面的手绘图:
流体布局下的瀑布流结构草图 张鑫旭-鑫空间-鑫生活

没有复杂的位置计算,不需要知道里面元素的高度以及宽度,且易理解,关键是具体实现~~

三、高潮来了:demo展示

您可以狠狠地点击这里:基于多栏列表瀑布流布局demo

瀑布流demo页面效果截图 张鑫旭-鑫空间-鑫生活

欢迎各种滚动,缩放等测试。低版本IE浏览器也是兼容滴。问题嘛也是有滴,就是滚动到一定位置再F5刷新的时候,部分加载的内容有丢失,需要重新滚动加载。这个嘛,我个人觉得小小demo,没必要折腾啦(实际上要实现也比较容易,改动如下:每次滚动不是append一个节点,而是连续回调直到加载到屏幕下方。不懂什么意思?花点功夫看看JS实现原理就会明白了)~~

四、说说原理

第一次进入的时候,根据浏览器宽度以及每列宽度计算出列表个数,然后不管三七二十一,每列先加载个5张图片再说。

当滚动的时候,对每一列的底部位置做检测,如果在屏幕中或屏幕上方,则立即append一个新图片(注意:为了简化代码,提高性能,同时便于演示等,这里只append了一个)。因为,滚动时连续的,因此,我们实际看到的效果是图片不断load出来。

当浏览器宽度改变的时候,页面上有个idwaterFallDetectspan标签,这个标签作用有两个:一是实现两端对齐效果,二是用来检测瀑布流布局是否需要刷新。

检测原理如下:
span标签宽度与一个列表宽度一致,当浏览器宽度变小的时候,如果小到一定程度,显然,浏览器最右边的列表就会跑到下一行,把空span挤到后面去,空span发生较大的水平位移,显然,可以通知脚本,布局需要刷新;当浏览器宽度变大的时候,如果变大的尺寸超过一列的宽度,显然,这个空span灰跑到第一行去,同样是发生较大的水平位移,因此,又可以通知脚本刷新瀑布流布局了。

这个方法的好处是几乎没有计算就可以一点不差地知道何时瀑布流布局需要刷新。这显然要比设置resize定时器+位置尺寸计算要简单高性能地多。

浏览器宽度变小时触发瀑布流更新的原理示意 浏览器宽度变宽时触发的瀑布流更新原理示意

滚动时的页面刷新是基于HTML字符串的处理,而不是更改每个DOM元素的位置(这是绝对定位实现的处理),因此,这里的效率显然更高。

五、总结:基于多栏列表流体布局瀑布流效果优点

  1. 简单:最大限度利用了浏览器的流体特性进行布局,省去了很多计算的麻烦;新人更易懂和上手
  2. 更好的性能:这个体现在多处,如浏览器宽度改变,瀑布流刷新时候的效率等
  3. 无需知晓尺寸:如果是要绝对定位实现瀑布流,必须知道每个小模块的高度以及宽度(否则无法定位),而基于列表的布局则无需知道高宽

无聊时候的折腾,有不足与不准确之处欢迎指正。一些实现的具体细节等也是非常欢迎提问交流的。

原创文章,转载请注明来自张鑫旭-鑫空间-鑫生活[http://www.zhangxinxu.com]
本文地址:http://www.zhangxinxu.com/wordpress/?p=2308

(本篇完)

分享到:

标签: , , , ,

赞助商推荐(我也要赞助)

想学到点真东西? ×
如果你有1~3年前端开发经验,不妨 ×
想要工资30K? ×


发表评论(目前43 条评论)

  1. allenCn说道:

    有两个BUG,一个是container里面设置font-size为0。不然当宽度恰好为整数倍的时候,会因为最后一栏多出来的空白,导致最后一栏下沉。第二个,在refresh里面,判定detect的位置变化的时候,需要将offsetTop和offsetLeft一起处理,不然在两栏切一栏的过程中,无法触发detect,因此此时只有offsetTop发生了变化。Left不变。

  2. 子任说道:

    三、高潮来了:demo展示   滑动到最后,问题出现了,基于7列的,中间第4列空了。

  3. falcon说道:

    自适应时有问题,当屏幕宽度只够显示一列时数据时,奇数列无法显示出来

  4. 请教说道:

    您的最后生成的html都是innerHTML以及字符串拼接出来的,demo里只有a,img,strong,可实际中会有很丰富的标签嵌套,都需要字符串拼接?实际中用到了您的这项技术,a链接写死,img的src是靠近线上图片格式,这些我找到了解决办法。还请您不吝赐教,谢谢,祝您工作顺利,家庭幸福!

  5. weiba112027说道:

    小屏幕下会出现问题啊

  6. janmi说道:

    瀑布流加载数据会出现重复的数据。

  7. 默默的学习中说道:

    js运用的炉火纯青了 羡慕啊

  8. kjy说道:

    挺好的… .

  9. bsn说道:

    学习了。。。

  10. HJane说道:

    我参照你的瀑布流效果,用对象字面量的形式写了一个js,只想问一个问题,为什么浏览器滚动时,sideMenu.scroll()没有响应呢?
    我的设计思路是把页面上所有类anchor-link的绝对位置存入一个数组,每次滚动时,遍历这个数组,如果滚动值大于数组内的某个值,则为相应的类添加样式。表现形式就是浏览器滚动到某个锚点时,侧栏的锚点链接有相应的变化,比如背景色改变等等。
    ps.我上传的页面是老版本的,结构还没改。我从上午10点纠结到现在,还是没解决,希望有时间的时候能帮我看下……感激不尽
    var sideMenu = {
    itemLink: “anchor-link”,
    menuID: “side-menu”,
    scrollTop: document.documentElement.scrollTop || document.body.scrollTop,
    arrItem: function() {
    var arr = [];
    var itemOffset = [];
    var itemName = [];
    var items = getElementsByClassName(this.itemLink);
    var itemMenu = getChildNodes(this.menuID,”a”);
    for (var i = 0; i 100) {
    self.scrollTop = scrollTop;
    self.addDetect();
    }
    };
    return this;
    },
    addDetect: function(){
    var start = 0;
    do {
    var itemOffset = arr[0][i];
    if (start >= arr[0].length-1) {
    break;
    };
    start ++;
    } while (itemOffset > this.scrollTop);
    var currentItem = arr[1][start];
    this.addEffect(currentItem);
    return this;
    },
    addEffect: function( currentItem ){
    alert(currentItem);
    return currentItem;
    },
    init: function() {
    if (this.itemLink && this.menuID) {
    this.arrItem().scroll();
    }
    }
    };
    sideMenu.init();
    /*用到的函数*/
    var getElementsByClassName = function(searchClass,node,tag) {
    if(document.getElementsByClassName){
    return document.getElementsByClassName(searchClass)
    }else{
    node = node || document;
    tag = tag || ‘*';
    var returnElements = []
    var els = (tag === “*” && node.all)? node.all : node.getElementsByTagName(tag);
    var i = els.length;
    searchClass = searchClass.replace(/\-/g, “\\-“);
    var pattern = new RegExp(“(^|\\s)”+searchClass+”(\\s|$)”);
    while(–i >= 0){
    if (pattern.test(els[i].className) ) {
    returnElements.push(els[i]);
    }
    }
    return returnElements;
    }
    }
    function getElementTop(element) {
    var actualTop = element.offsetTop;
    var current = element.offsetParent;
    while (current !== null){
    actualTop += current.offsetTop;
    current = current.offsetParent;
    }
    return actualTop;
    }
    function getChildNodes(){
    var node = new Array();
    if(arguments.length == 1 ){ //1.判断参数个数
    var temp = document.getElementById(arguments[0]).childNodes; //2.获取ul下所有子节点(包括text节点、空格)
    for(var i=0;i<temp.length;i++){
    if(temp.nodeType == 1 && temp.tagName){ //3.判断节点类型
    node.push(temp); //4.节点压栈
    }
    }
    return node; //5.返回节点列表(类似数组)
    }
    else if(arguments.length == 2){ //1.判断参数个数
    return document.getElementById(arguments[0]).getElementsByTagName(arguments[1]); //2.返回节点列表(类似数组)
    }
    }

    • HJane说道:

      代码被格式化了 不全,我更新了在线的,有时间的话,希望能帮我看下……

    • HJane说道:

      我预想的实现结果应该是
      浏览器滚动,进行判断scroll(),滚动距离>100,跳转到addDetect遍历,看那个链接的位置大于滚动值,然后获取数组索引,得到相应的类名,然后到addEffect(),这里直接输出类。
      但是我滚动滚动条没反应,在 scroll:functions(){}的第一行加alert();也没有输出,似乎根本就没有执行……
      如果调用的时候直接写个alert(sideMenu.scroll());倒是能弹出值。只有一次。

    • HJane说道:

      找到原因了……

  11. HJane说道:

    很喜欢,不过我觉得demo的布局在改变浏览器宽度时会增加间距,效果不好看。
    我把column的display:inline-block;改成了block,加上浮动,然后把宽度等于columnWidth值改成了width:’+ 1/this.columnNumber*100 +’%;’
    给图片加上max-width:100%;
    然后在浏览器窗口大小改变的时候再加一个判断,如果waterFallDetect的宽度等于 100%就立刻刷新。
    这样列的宽度会自动微调,保持列间距不变。

    • HJane说道:

      我最近想给自己添加一个代码片段加预览的页面,页面分成两部分,左边是瀑布流,显示代码片段,预览图片,有预览按钮,点击预览就在右侧载入demo,看到这片博客,我预想中的页面可以实现一部分了……

  12. 比价说道:

    谢谢楼主,原来瀑布流也不难

  13. Joey说道:

    第188行,应该是!self.loadFinish ?

    if (!this.loadFinish && Math.abs(scrollTop – self.scrollTop) > 100) {
    self.scrollTop = scrollTop;
    self.appendDetect();
    }

  14. dgfdsf说道:

    “实际上要实现也比较容易,改动如下:每次滚动不是append一个节点,而是连续回调直到加载到屏幕下方。不懂什么意思?花点功夫看看JS实现原理就会明白了)~~”
    好气人啊 懂的人自然懂 不懂的肯定看不懂 你是写给看的懂的人吗?

  15. yijs说道:

    老兄,我请教下我整的瀑布流的一个bug。我用的正是5楼说的那个JQ。
    可是chorme下有bug。默认加载的时候图片都重叠在一起了。不知道怎么解决。

  16. ccc说道:

    我想请教下博主 ,要是我固定了高度 然后 以从左到右无限瀑布流 那我要怎么写呢?

  17. haha说道:

    Lewis~你写的代码有没什么说明啊

  18. haha说道:

    初始加载数据后,如果数据只有13条,那么后面补填的是空值。有什么办法能去掉补填吗?

  19. Lewis说道:

    我也针对人人逛街的数据,写了一个商品和图片的瀑布流,欢迎使用

    http://www.jsbug.com/tools/wall/

  20. darf说道:

    http://www.zhangxinxu.com/study/201203/waterfall-layout.html

    这个可以修改为通过JSON数据来加载图片吗?也看不出来,第一次是加载了多少张图片?最新更新图片如何显示到最上方呢?

  21. x说道:

    发现一个问题就是从最大化 6 栏调整到 3 栏布局时 因为它不会让空 span 移动任何的距离 (因为 7%6==7%3) 布局就不会刷新结果 #waterFallColumn_3, #waterFallColumn_4, 和 #waterFallColumn_5 都跑到下面去了而不是像预期那样消失,而布局刷新使那些地方的图片到 0, 1 和 2 栏去。。。

  22. alex说道:

    lz create方法里面的
    this.columnNumber = Math.floor(document.body.clientWidth / this.columnWidth);
    值应该还减去1,因为接下来start是从0开始的。应该加上 this.columnNumber -= 1;

  23. EchoFUN说道:

    看了很多楼主的文章,在思路和想法上都另辟蹊径的。不过很多地方是不是应该需要考虑下语义化标签在SEO中的起到的效果。这里lz用span标签上添加上了一个display: inline-block来改变他的显示效果。然后里面再塞了些“瀑布”。但是w3c的标准里,貌似也不建议使用内联元素来包裹其他的元素哦。

  24. 一路说道:

    此解决思路很棒…

  25. leon说道:

    原来FF下刷新后会不断地请求数据,感谢指出~还有什么问题欢迎交流指正哈

  26. leon说道:

    致命的显示bug是指?图片无限拖之后 resize 会卡死吗?

  27. Merainy说道:

    我想问下 瀑布流 是怎么请求数据的

  28. robertsky说道:

    图片挺好看的,技术也比较新颖,唯一的缺点是不可以按照输出顺序从左往右排列,多谢分享

  29. wubai说道:

    5楼推荐的国外插件不错

  30. jun说道:

    这个两端对齐实在是太好用了。
    简化了布局而且在计算是否需要加载新内容的时候也会方便很多。
    多谢楼主分享

  31. 小7说道:

    我开始也是那样写的,不是用绝对定位,但发现有bug的,你的也是,窗体宽度改变,然后加载到最后,会出现一大片空格的情况。

  32. 小7说道:

    之前也写过类似的插件,用js进行绝对定位,看了下,和博主思路不大一样,我写的reflow会比较严重,没怎么进行优化,就是使用简单而已,有兴趣可以看下:
    http://www.7rice.org/demofile/waterfall/waterfall.html

  33. 小磊说道:

    哈。哈。终于更新这个了,等了好些天呢。。嘿嘿。先谢谢了。http://masonry.desandro.com/index.html
    这个是我在国外网站找到不错的关于瀑布流的资料和教程!嘿嘿。

  34. tcdona说道:

    这个实现真实不错!

  35. 沧海难为水说道:

    发现了两个问题哦:
    1、两端对齐在谷歌下支持不是很好;
    2、当窗体缩小到很小时,图片显示不全;

  36. ktsos说道:

    貌似是js宽度计算有误

  37. ktsos说道:

    这个有创意,但5号图片带这几个同伙跑到下面去了