瀑布流实现其实已经不是什么新鲜的玩意了,国内外多个展示性网站如花瓣网百度图片等都早已采用了瀑布流的页面布局方式。瀑布流布局巧妙地重排元素并填补了容器的所有空间,适合小数据块,每个数据块内容相近且没有侧重。通常,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。所以,我们给这样的布局起了一个形象的名字 — 瀑布流布局。

今天心血来潮,决定自己开发一个瀑布流布局,希望能兼容IE6+,而且能实现响应式布局,便在纸上构思其实现逻辑和思路。折腾了一个下午,于是,便有了下文。

HTML和CSS布局

一看到这种不规则的布局,第一时间蹦入脑中的就是父容器相对定位和子元素绝对定位,通过动态定义left和top的值来实现排版。

以下是我的HTML和CSS代码():

/* 这里用了通配选择器一键清除默认样式,大家不要学我,一般不建议这样使用,建议使用Normalize.css完成样式初始化 */
* {
    margin: 0;
    padding: 0;
    border: 0;
}
.waterfall {
    width: 960px;
    margin: 10px auto;
    position: relative;
}
.waterfall:after, .waterfall:before {
    content: " ";
    display: table;
}
.flow {
    width: 310px;
    background: #333;
    position: absolute;
    border: 1px solid #ccc;
    box-shadow: #cccccc 2px 3px 3px;
    transition: left .5s linear;
    -webkit-transition: left .5s linear;
    -moz-transition: left .5s linear;
    -o-transition: left .5s linear;
}
.flow .flowItem {
    width: 100%;
    font-size: 42pt;
    color: #fff;
    text-align: center;
}
<div class="waterfall" id="waterfall">
    <div class="flow"><div class="flowItem" style="height: 100px;">1</div></div>
    <div class="flow"><div class="flowItem" style="height: 200px;">2</div></div>
    <div class="flow"><div class="flowItem" style="height: 150px;">3</div></div>
    <div class="flow"><div class="flowItem" style="height: 400px;">4</div></div>
    <div class="flow"><div class="flowItem" style="height: 180px;">5</div></div>
    <div class="flow"><div class="flowItem" style="height: 120px;">6</div></div>
    <div class="flow"><div class="flowItem" style="height: 300px;>7</div></div>
    <div class="flow"><div class="flowItem" style="height: 100px;">8</div></div>
    <div class="flow"><div class="flowItem" style="height: 120px;">9</div></div>
    <div class="flow"><div class="flowItem" style="height: 105px;">10</div></div>
    <div class="flow"><div class="flowItem" style="height: 180px;11</div></div>
    <div class="flow"><div class="flowItem" style="height: 120px;">12</div></div>
    <div class="flow"><div class="flowItem" style="height: 300px;">13</div></div>
    <div class="flow"><div class="flowItem" style="height: 100px;">14</div></div>
    <div class="flow"><div class="flowItem" style="height: 120px;">15</div></div>
    <div class="flow"><div class="flowItem" style="height: 105px;">16</div></div>
</div>

快掩住我的眼,亮瞎了,什么鬼东西哇,丑死咧!!!

莫急,下面我们好好来分析一下用JS动态分配定位元素的left和top值,重新排版一下。

JS实现一:测试版,仅为了实现而实现

为了测试,我先拟定了3列布局,每一个瀑布流元素块的宽度为310px,元素块之间的垂直和水平间距均为15px,经过我的人工计算,包裹容器的宽度刚好960px。

首先,我要思考的是,要如何实现定位呢?一开始的时候,我想着,要不比较当前的块元素的前三个元素的offsetTop和offsetHeight得到最矮的一列,然后根据这一列定位当前元素。很快,这个想法实现起来漏洞百出,而且不合逻辑,deny掉啦。

这种局部的过程是什么?就是当前元素排布在最矮的列上啊。如果我们能动态获取当前最矮的列高以及第几列,不就能准确定位当前元素了吗?那么问题来了,怎么获取当前最矮的列高和列序号呢?把每一列的高度存在一个数组里面,每次定位完当前元素,都实时修改当前定位的列高,那样每一次比较都能获得准确的最矮列了。原理就这么简单。

那么如何快速获取一个数组中最小的值呢?最简单的方法之直接调用Math.min()方法Math.min.apply(null, myArray)。当然也可以用循环递归来实现判断,但是使用原生的Math方法性能会更高一些。

直接上代码(因为用到querySelectorAll()和indexOf(),所以不能兼容IE6~8):

 var waterfall = document.getElementById("waterfall");
 var flowItems = waterfall.querySelectorAll(".flow");

 // 简单版(只兼容至IE9,宽度、列数固定)
 // 共3列,每一列的宽度固定为310px,元素块之间的水平和垂直间距均为15px;瀑布流包含块的宽度为960px;

 // 声明瀑布流中每一列高度的数组pin[]
 var pin = [];
 pin[0] = flowItems[0].offsetTop + flowItems[0].offsetHeight;
 pin[1] = flowItems[1].offsetTop + flowItems[1].offsetHeight;
 pin[2] = flowItems[2].offsetTop + flowItems[2].offsetHeight;
 // 循环瀑布流元素的高度
 for(var i = 0, len = flowItems.length; i < len; i++) {
     if(i >= 3) {
         // 获取三个数中的最小值
         var minH = Math.min.apply(null, pin);
         // 获取高度数组中最小高度的索引
         var minHItem = pin.indexOf(minH);
         // 把当前元素在视觉上置于最小高度的一列
         flowItems[i].style.left = minHItem * (310 + 15) + "px";
         flowItems[i].style.top = minH + 15 + "px";
         // 重置列的高度
         pin[minHItem] += flowItems[i].offsetHeight + 15;
         }else if(i < 3){
         flowItems[i].style.top = 0;
         flowItems[i].style.left = (i % 3) * (310 + 15) + "px";
     }
 }

效果图如下:

在线DEMO请戳这里~

无疑,如你所见,耦合度高得无法直视,因为一些参数都是写死的,复用性较差。我们可以把一些参数抽离出来,把函数封装成独立的模块。见下面升级版的方法。

升级版:参数抽离和解决低版本IE的兼容性

重新对瀑布流定位函数进行了封装。并且把一些可自定义的参数抽离出来,由于参数数量较多,所以使用对象来存储数据。这样,我们就能自定义修改参数获得多种布局方式啦。

由于上面的实现方法中因为使用了querySelectorAll()和indexOf()函数而导致不能兼容IE6~8。在这里,我重写了这两个函数实现,让低版本浏览器也能愉快地打开网站。

JS代码见下:

 var waterfallParent = document.getElementById("waterfall");
 var flowItems = getClassName(waterfallParent, "flow");
 // 定义瀑布流布局参数,如下:
 // parent:瀑布流包裹容器,类型为DOM对象;floowItems:瀑布流布局子元素组,类型为DOM对象数组;pin:列数,类型为int;
 // width:每个瀑布流布局元素的宽度,类型为int;horizontalMargin:元素块之间的水平间距,类型为int;
 // verticalMargin:元素块之间的垂直间距,类型为int;
 var currentFlow = {
 parent: waterfallParent,
 flowItems: flowItems,
 pin: 4,
 width: 310,
 horizontalMargin: 15,
 verticalMargin: 15
 };

 waterfall(currentFlow);

 // 其中flow是一个对象,分别包含如下键值:
 // parent:瀑布流包裹容器,类型为DOM对象;floowItems:瀑布流布局子元素组,类型为DOM对象数组;pin:列数,类型为int;
 // width:每个瀑布流布局元素的宽度,类型为int;horizontalMargin:元素块之间的水平间距,类型为int;
 // verticalMargin:元素块之间的垂直间距,类型为int;
 function waterfall(flow) {
     // 声明瀑布流中每一列高度的数组pin[]
     var pin = new Array(flow.pin);
     // 瀑布流框块数组
     var flowItems = flow.flowItems;
     // 声明每一列高度的初始值
     for(var i = 0, pinLen = pin.length; i < pinLen; i++) {
         pin[i] = flowItems[i].offsetTop + flowItems[i].offsetHeight;
     }
     // 循环瀑布流元素的高度
     for(var i = 0, len = flowItems.length; i < len; i++) {
         if(flow.width) {
             flowItems[i].style.width = flow.width + "px";
         }

         if(i >= flow.pin) {
             // 获取pin数组中的最小值
             var minH = Math.min.apply(null, pin);
             // 获取高度数组中最小高度的索引
             var minHItem = pin.indexOf(minH);
             // 把当前元素在视觉上置于最小高度的一列
             flowItems[i].style.left = minHItem * (flow.width + flow.horizontalMargin) + "px";
             flowItems[i].style.top = minH + flow.verticalMargin + "px";
             // 重置列的高度
             pin[minHItem] += flowItems[i].offsetHeight + flow.verticalMargin;
         }else if(i < flow.pin){
             flowItems[i].style.top = 0;
             flowItems[i].style.left = (i % flow.pin) * (flow.width + flow.horizontalMargin) + "px";
         }
     }
     // 计算瀑布流容器的宽度
     flow.parent.style.width = flow.pin * flow.width + (flow.pin - 1) * flow.horizontalMargin + "px";

 }

 // 获取className的元素集合
 // 参数:obj指父元素;oClassName为元素的class属性值
 function getClassName(obj, oClassName) {
     // IE9+及标准浏览器可以直接使用getElementsByClassName()获取className元素集合
     if(document.getElementsByClassName) {
        return obj.getElementsByClassName(oClassName);
     }else {
         // classNameArr用来装载class属性值为oClassName的元素;
         var classNameArr = [];
         // 获取obj的直接子元素
         var objChild = obj.children || obj.childNodes;
         // 遍历obj元素,获取class属性值为oClassName的元素列表
         for(var i = 0; i < objChild.length; i++) {
         // 判断obj子元素的class属性值中是否含有oClassName
         if( hasClassName(objChild[i], oClassName) ) {
         classNameArr.push(objChild[i]);
         }
     }
     return classNameArr;
     }
 }

 // Array.indexOf()函数的兼容性重写
 if (!Array.prototype.indexOf) {
     Array.prototype.indexOf = function(ele) {
         // 获取数组长度
         var len = this.length;
         // 检查值为数字的第二个参数是否存在,默认值为0
         var fromIndex = Number(arguments[1]) || 0;
         // 当第二个参数小于0时,为倒序查找,相当于查找索引值为该索引加上数组长度后的值
         if(fromIndex < 0) {
            fromIndex += len;
         }
        // 从fromIndex起循环数组
         while(fromIndex < len) {
             // 检查fromIndex是否存在且对应的数组元素是否等于ele
             if(fromIndex in this && this[fromIndex] === ele) {
                 return fromIndex;
             }
             fromIndex++;
         }
         // 当数组长度为0时返回不存在的信号:-1
         if (len === 0) {
            return -1;
         }
     }
 }

在线DEMO请戳这里~

这种实现似乎很完美了,至少现在的我觉得还算OK。但是由于我们现在做的网站大多都是响应式布局的,而以上的JS实现都是固定瀑布流容器宽度和列数的,显然并不能满足需求。

响应式版

有了上面封装好的可自定义列数的瀑布流布局函数,下面的实现就轻松多啦。我们可以检测当前设备的宽度,并根据探测的设备宽度决定当前排布的列数以及瀑布流包裹容器的宽度。

在这里,我声明的响应断点是1200px, 960px, 767px 和320px。

具体代码见下:

// 超升级版(列数和每一列的宽度、元素块之间的边距为不定值,兼容IE6~8,实现响应式布局)
var waterfallParent = document.getElementById("waterfall");
var flowItems = getClassName(waterfallParent, "flow");
// 声明瀑布流浮动参数
// parent:瀑布流包裹容器,类型为DOM对象;floowItems:瀑布流布局子元素组,类型为DOM对象数组;pin:列数,类型为int;
// width:每个瀑布流布局元素的宽度,类型为int;horizontalMargin:元素块之间的水平间距,类型为int;
// verticalMargin:元素块之间的垂直间距,类型为int;
var currentFlow = {
    parent: waterfallParent,
    flowItems: flowItems,
    pin: 4,
    width: 310,
    horizontalMargin: 15,
    verticalMargin: 15
};

// 声明响应式的响应断点
var deviceWidth = {
    D: 1200,
    C: 960,
    B: 767,
    A: 320
};

// 响应式瀑布流布局绘制
window.onresize = responseFlow;
responseFlow();
function responseFlow() {
    var deviceW;
    // 判断当前的设备屏幕宽度
    function checkDeviceW() {
        var screenW = document.documentElement.offsetWidth || document.body.offsetWidth;
        if(screenW >= deviceWidth.A && screenW < deviceWidth.B) {
            deviceW = "A";
        }else if(screenW >= deviceWidth.B && screenW < deviceWidth.C) {
            deviceW = "B";
        }else if(screenW >= deviceWidth.C && screenW < deviceWidth.D) {
            deviceW = "C";
        }else if(screenW >= deviceWidth.D) {
            deviceW = "D";
        }
    }
    checkDeviceW();

    // 修改不同响应下瀑布流布局的列数
    switch(deviceW) {
        case "A":
            currentFlow.pin = 1;
            break;
        case "B":
            currentFlow.pin = 2;
            break;
        case "C":
            currentFlow.pin = 3;
            break;
        case "D":
            currentFlow.pin = Math.floor(currentFlow.parent.offsetWidth / currentFlow.width);
            break;
    }
    // 瀑布流重绘
    waterfall(currentFlow);
}

// 其中flow是一个对象,分别包含如下键值:
// pin:列数,类型为int;
function waterfall(flow) {
    // 声明瀑布流中每一列高度的数组pin[]
    var pin = new Array(flow.pin);
    // 瀑布流框块数组
    var flowItems = flow.flowItems;
    // 声明每一列高度的初始值
    for(var i = 0, pinLen = pin.length; i < pinLen; i++) {
        pin[i] = flowItems[i].offsetTop + flowItems[i].offsetHeight;
    }
    // 循环瀑布流元素的高度
    for(var i = 0, len = flowItems.length; i < len; i++) {
        if(flow.width) {
            flowItems[i].style.width = flow.width + "px";
        }

        if(i >= flow.pin) {
            // 获取pin数组中的最小值
            var minH = Math.min.apply(null, pin);
            // 获取高度数组中最小高度的索引
            var minHItem = pin.indexOf(minH);
            // 把当前元素在视觉上置于最小高度的一列
            flowItems[i].style.left = minHItem * (flow.width + flow.horizontalMargin) + "px";
            flowItems[i].style.top = minH + flow.verticalMargin + "px";
            // 重置列的高度
            pin[minHItem] += flowItems[i].offsetHeight + flow.verticalMargin;
        }else if(i < flow.pin){
            flowItems[i].style.top = 0;
            flowItems[i].style.left = (i % flow.pin) * (flow.width + flow.horizontalMargin) + "px";
        }
    }
    // 计算瀑布流容器的宽度
    flow.parent.style.width = flow.pin * flow.width + (flow.pin - 1) * flow.horizontalMargin + "px";
}

// 获取className的元素集合
// 参数:obj指父元素;oClassName为元素的class属性值
function getClassName(obj, oClassName) {
    // IE9+及标准浏览器可以直接使用getElementsByClassName()获取className元素集合
    if(document.getElementsByClassName) {
        return obj.getElementsByClassName(oClassName);
    }else {
        // classNameArr用来装载class属性值为oClassName的元素;
        var classNameArr = [];
        // 获取obj的直接子元素
        var objChild = obj.children || obj.childNodes;
        // 遍历obj元素,获取class属性值为oClassName的元素列表
        for(var i = 0; i < objChild.length; i++) {
            // 判断obj子元素的class属性值中是否含有oClassName
            if( hasClassName(objChild[i], oClassName) ) {
                classNameArr.push(objChild[i]);
            }
        }
        return classNameArr;
    }
}

// Array.indexOf()函数的兼容性重写
if (!Array.prototype.indexOf) {
    Array.prototype.indexOf = function(ele) {
        // 获取数组长度
        var len = this.length;
        // 检查值为数字的第二个参数是否存在,默认值为0
        var fromIndex = Number(arguments[1]) || 0;
        // 当第二个参数小于0时,为倒序查找,相当于查找索引值为该索引加上数组长度后的值
        if(fromIndex < 0) {
            fromIndex += len;
        }
        // 从fromIndex起循环数组
        while(fromIndex < len) {
            // 检查fromIndex是否存在且对应的数组元素是否等于ele
            if(fromIndex in this && this[fromIndex] === ele) {
                return fromIndex;
            }
            fromIndex++;
        }
        // 当数组长度为0时返回不存在的信号:-1
        if (len === 0) {
            return -1;
        }
    }
}

效果如下:

在线DEMO请戳这里~

总结

还可以用Ajax动态加载数据,以实现数据源源不断加载的效果,篇幅太长,分下一篇文章写,就酱纸。

这种实现方法有一个十分不好的地方就是:一旦用户禁止了JavaScript或者JavaScript未加载完成,就不能正常显示页面内容。另外,由于布局采用父容器相对定位和子元素绝对定位,而绝对定位会使元素脱离文档流,从而导致父容器高度塌陷。所以,一般常见的处理办法是将瀑布流布局置于页面的尾部,或者动态获取父容器的高度。

总的来说,虽然网上有很多瀑布流布局的插件和实现方法,而且实现原理也比较简单。但是亲自实践就会发现一些技巧和难点譬如如何获取数组中的最小值、动态判断屏幕宽度等都是很值得思考和优化的。

喜欢就拿去用吧。源码在此~

本文作者:子匠_Zijor,转载请注明出处:http://www.dengzhr.com/js/405