这样的布局并不陌生,从2011年Pinterest创立以来,中国互联网就迅速掀起了一股模仿Pinterest的热潮,国内有众多网站采用瀑布流的布局方式,例如花瓣网、美丽说等等。而事实上在中国互联网,模仿一些在国外被人看好的模式(当然,你也可以说是山寨或抄袭,呵呵!!)向来都是一个不错的idea。
OK,现在进入正题。这里主要介绍瀑布流的一种实现方法:绝对定位(css)+javascript+ajax+json。简单一点如果不做滚动加载的话就是绝对定位(css)+javascript了,ajax和json是滚动加载更多内容的时候用到的。
下面是实现思路:
1、计算页面的宽度,计算出页面可放数据块的列数(如上图所示就有6列)。
2、将各个数据块的高度尺寸记入数组中(需要等所有图片加载完成,否则无法知道图片的高度)。
3、用绝对定位先将页面第一行填满,因为第一行的top位置都是一样的,然后用数组记录每一列的总高度。
4、继续用绝对定位将其他数据块定位在最短的一列的位置之后然后更新该列的高度。
5、当浏览器窗口大小改变时,重新执行一次上面1-4步以重新排放(列数随页面宽度而改变,因而需要重新排放)。
6、滚动条滚动到底部时加载新的数据进来后也是定位在最短的一列的位置之后然后更新该列的高度。
思路有了,然后就是如何用代码实现。当然,如果看完以上的6个步骤你已经知道如何实现,那么下面的内容大可不必细看。
首先在页面上写好基本的HTML和CSS(为方便起见,CSS就不外联了),代码如下:
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 5 <title>瀑布流布局</title> 6 <style type="text/css"> 7 body{margin:0; font-family:微软雅黑;} 8 #flow-box{margin:10px auto 0 auto; padding:0; position:relative} 9 #flow-box li{ 10 width:190px; position:absolute; padding:10px; border:solid 1px #efefef; list-style:none; 11 opacity:0; 12 -moz-opacity:0; 13 filter:alpha(opacity=0); 14 -webkit-transition:opacity 500ms ease-in-out; 15 -moz-transition:opacity 500ms ease-in-out; 16 -o-transition:opaicty 500ms ease-in-out; 17 transition:opaicty 500ms ease-in-out;} 18 #flow-box li img{width:100%;} 19 #flow-box li a{display:block; width:100%; text-align:center; font-size:14px; color:#333; line-height:18px; margin-top:10px; text-decoration:none;} 20 .loadwrap{position:absolute; left:0; width:100%; text-align:center;} 21 </style> 22 </head> 23 <body> 24 <ul id="flow-box"> 25 <li><img src="http://www.mitxiong.com/NewsImages/2012121821504156.jpg" /><a href="#">图片标题1</a></li> 26 <li><img src="http://www.mitxiong.com/NewsImages/2012112718241731.jpg" /><a href="#">图片标题2</a></li> 27 <li><img src="http://www.mitxiong.com/NewsImages/2012111806582944.jpg" /><a href="#">图片标题3</a></li> 28 <li><img src="http://www.mitxiong.com/NewsImages/2012110907231232.jpg" /><a href="#">图片标题4</a></li> 29 <li><img src="http://www.mitxiong.com/NewsImages/2012110406319529.jpg" /><a href="#">图片标题5</a></li> 30 <li><img src="http://www.mitxiong.com/NewsImages/2012101808066955.jpg" /><a href="#">图片标题6</a></li> 31 <li><img src="http://www.mitxiong.com/NewsImages/2012101307276582.jpg" /><a href="#">图片标题7</a></li> 32 <li><img src="http://www.mitxiong.com/NewsImages/2012082223432719.jpg" /><a href="#">图片标题8</a></li> 33 <li><img src="http://www.mitxiong.com/NewsImages/2012082121509065.jpg" /><a href="#">图片标题9</a></li> 34 <li><img src="http://www.mitxiong.com/NewsImages/2012081922387254.jpg" /><a href="#">图片标题10</a></li> 35 <li><img src="http://www.mitxiong.com/NewsImages/2012081700252403.jpg" /><a href="#">图片标题11</a></li> 36 <li><img src="http://www.mitxiong.com/NewsImages/2012081407597304.jpg" /><a href="#">图片标题12</a></li> 37 <li><img src="http://www.mitxiong.com/NewsImages/2012081218248259.jpg" /><a href="#">图片标题13</a></li> 38 <li><img src="http://www.mitxiong.com/NewsImages/2012080621278799.jpg" /><a href="#">图片标题14</a></li> 39 <li><img src="http://www.mitxiong.com/NewsImages/2012072907484455.jpg" /><a href="#">图片标题15</a></li> 40 <li><img src="http://www.mitxiong.com/NewsImages/2012072521564314.jpg" /><a href="#">图片标题16</a></li> 41 <li><img src="http://www.mitxiong.com/NewsImages/2012072507238259.jpg" /><a href="#">图片标题17</a></li> 42 <li><img src="http://www.mitxiong.com/NewsImages/2012072409035684.jpg" /><a href="#">图片标题18</a></li> 43 <li><img src="http://www.mitxiong.com/NewsImages/2012072219405236.jpg" /><a href="#">图片标题19</a></li> 44 <li><img src="http://www.mitxiong.com/NewsImages/2012071218416980.jpg" /><a href="#">图片标题20</a></li> 45 </ul> 46 <div id="loadimg" class="loadwrap"><img src="Images/load.jpg" /></div> 47 </body> 48 </html>
以上代码非常简单,可以看出页面最初将会先加载20个数据块。值得一提的是在CSS里面定义了opacity为0,目的是在数据块未排放好之前先隐藏起来,排放好后再将opacity设为1显示出来,另外这里用了css3的transition做一点体验上的升级;还有一点就是可以看到页面底部有一个id为“loading”的DIV,用来表示数据正在加载中。下面开始用JS实现以上思路(6个步骤)。
1、计算页面的宽度,计算出页面可放数据块的列数
1 <script type="text/javascript"> 2 function flow(mh, mv) {//参数mh和mv是定义数据块之间的间距,mh是水平距离,mv是垂直距离 3 var w = document.documentElement.offsetWidth;//计算页面宽度 4 var ul = document.getElementById("flow-box"); 5 var li = ul.getElementsByTagName("li"); 6 var iw = li[0].offsetWidth + mh;//计算数据块的宽度 7 var c = Math.floor(w / iw);//计算列数 8 ul.style.width = iw * c - mh + "px";//设置ul的宽度至适合便可以利用css定义的margin把所有内容居中
9 } 10 </script>
注释写得非常明白,这一步不说应该都很容易懂。
2、将各个数据块的高度尺寸记入数组中
1 <script type="text/javascript"> 2 function flow(mh, mv) {//参数mh和mv是定义数据块之间的间距,mh是水平距离,mv是垂直距离 3 //... 省略上一步的部份代码 ... 8 ul.style.width = iw * c - mh + "px";//设置ul的宽度至适合便可以利用css定义的margin把所有内容居中 9 10 var liLen = li.length; 11 var lenArr = []; 12 for (var i = 0; i < liLen; i++) {//遍历每一个数据块将高度记入数组 13 lenArr.push(li[i].offsetHeight); 14 } 15 } 16 </script>
由于数据块里面含有图片,也没有给定图片的尺寸,所以需要等待图片加载完成后方可获取其高度;那么可以在window.onload的时候调用flow方法。代码变成:
1 <script type="text/javascript"> 2 function flow(mh, mv) {//参数mh和mv是定义数据块之间的间距,mh是水平距离,mv是垂直距离 3 //... 省略上一步的部份代码 ... 8 ul.style.width = iw * c - mh + "px";//设置ul的宽度至适合便可以利用css定义的margin把所有内容居中 9 10 var liLen = li.length; 11 var lenArr = []; 12 for (var i = 0; i < liLen; i++) {//遍历每一个数据块将高度记入数组 13 lenArr.push(li[i].offsetHeight); 14 } 15 } 16 //图片加载完成后执行 17 window.onload = function() {flow(10, 10)};
18 </script>
3、用绝对定位先将页面第一行填满,因为第一行的top位置都是一样的,然后用数组记录每一列的总高度。
1 <script type="text/javascript"> 2 function flow(mh, mv) {//参数mh和mv是定义数据块之间的间距,mh是水平距离,mv是垂直距离 //... 省略上一步的部份代码 ...
12 for (var i = 0; i < liLen; i++) {//遍历每一个数据块将高度记入数组 13 lenArr.push(li[i].offsetHeight); 14 } 15 16 var oArr = []; 17 for (var i = 0; i < c; i++) {//把第一行排放好,并将每一列的高度记入数据oArr 18 li[i].style.top = "0"; 19 li[i].style.left = iw * i + "px"; 20 li[i].style.opacity = "1"; 21 li[i].style["-moz-opacity"] = "1"; 22 li[i].style["filter"] = "alpha(opacity=100)"; 23 oArr.push(lenArr[i]); 24 } 25 document.getElementById("loadimg").style.top = _getMaxValue(oArr) + 50 + "px";//将loading移到下面 26 } 27 //图片加载完成后执行 28 window.onload = function() {flow(10, 10)}; 29 //获取数字数组的最大值 30 function _getMaxValue(arr) { 31 var a = arr[0]; 32 for (var k in arr) { 33 if (arr[k] > a) { 34 a = arr[k]; 35 } 36 } 37 return a; 38 } 39 </script>
截至目前为止,可以到浏览器里面预览一下效果:
OK,接下来开始放置其他的数据块了,也就是到思路的第4步了。
4、继续用绝对定位将其他数据块定位在最短的一列的位置之后然后更新该列的高度。
1 <script type="text/javascript"> 2 function flow(mh, mv) {//参数mh和mv是定义数据块之间的间距,mh是水平距离,mv是垂直距离 3 //... 省略上一步的部份代码 ...
17 for (var i = 0; i < c; i++) {//把第一行排放好,并将每一列的高度记入数据oArr 18 li[i].style.top = "0"; 19 li[i].style.left = iw * i + "px"; 20 li[i].style.opacity = "1"; 21 li[i].style["-moz-opacity"] = "1"; 22 li[i].style["filter"] = "alpha(opacity=100)"; 23 oArr.push(lenArr[i]); 24 } 25 26 for (var i = c; i < liLen; i++) {//将其他数据块定位到最短的一列后面,然后再更新该列的高度 27 var x = _getMinKey(oArr);//获取最短的一列的索引值 28 li[i].style.top = oArr[x] + mv + "px"; 29 li[i].style.left = iw * x + "px"; 30 li[i].style.opacity = "1"; 31 li[i].style["-moz-opacity"] = "1"; 32 li[i].style["filter"] = "alpha(opacity=100)"; 33 oArr[x] = lenArr[i] + oArr[x] + mv;//更新该列的高度 34 } 35 document.getElementById("loadimg").style.top = _getMaxValue(oArr) + 50 + "px";//将loading移到下面 36 } 37 //图片加载完成后执行 38 window.onload = function() {flow(10, 10)}; 39 //获取数字数组的最大值 40 function _getMaxValue(arr) { 41 //... 省略部份代码 ...
48 } 49 //获取数字数组最小值的索引 50 function _getMinKey(arr) { 51 var a = arr[0]; 52 var b = 0; 53 for (var k in arr) { 54 if (arr[k] < a) { 55 a = arr[k]; 56 b = k; 57 } 58 } 59 return b; 60 } 61 </script>
到这一步可以到浏览器里面再看一次效果,可以说整个瀑布流的雏形都出来了:
5、当浏览器窗口大小改变时,重新执行一次上面1-4步以重新排放
这一步操作起来也相当便捷,在改变窗口大小时,再执行一次flow方法即可
1 <script type="text/javascript"> 2 function flow(mh, mv) {//参数mh和mv是定义数据块之间的间距,mh是水平距离,mv是垂直距离 3 //... 省略部份代码 ...
37 //图片加载完成后执行 38 window.onload = function() {flow(10, 10)}; 39 //改变窗口大小时重新布局 40 var re; 41 window.onresize = function() { 42 clearTimeout(re); 43 re = setTimeout(function() {flow(10, 10);}, 200); 44 } 45 //获取数字数组的最大值 46 function _getMaxValue(arr) { 47 //... 省略部份代码 ...
54 } 55 //获取数字数组最小值的索引 56 function _getMinKey(arr) { 57 //... 省略部分代码 ...
66 } 67 </script>
这里值得注意的便是setTimeout,由于onresize的触发频率非常高,用setTimout设定一个间隔时间可以减低flow方法的执行频率,降低性能损耗。
6、滚动条滚动到底部时加载新的数据进来后也是定位在最短的一列的位置之后然后更新该列的高度。
1 <script type="text/javascript"> 2 function flow(mh, mv) {//参数mh和mv是定义数据块之间的间距,mh是水平距离,mv是垂直距离 3 //... 省略部份代码 ...
35 document.getElementById("loadimg").style.top = _getMaxValue(oArr) + 50 + "px";//将loading移到下面 36 37 function scroll() {//滚动加载数据 38 var st = oArr[_getMinKey(oArr)]; 39 var scrollTop = document.documentElement.scrollTop > document.body.scrollTop? document.documentElement.scrollTop : document.body.scrollTop; 40 if (scrollTop >= st - document.documentElement.clientHeight) { 41 window.onscroll = null;//为防止重复执行,先清除事件 42 _request(null, "GetList.php", function(data) {//当滚动到达最短的一列的距离时便发送ajax请求新的数据,然后执行回调函数 43 _addItem(data.d, function() {//追加数据 44 var liLenNew = li.length; 45 for(var i = liLen; i < liLenNew; i++) { 46 lenArr.push(li[i].offsetHeight); 47 } 48 for(var i = liLen; i < liLenNew; i++) { 49 var x = _getMinKey(oArr); 50 li[i].style.top = oArr[x] + 10 + "px"; 51 li[i].style.left = iw * x + "px"; 52 li[i].style.opacity = "1"; 53 li[i].style["-moz-opacity"] = "1"; 54 li[i].style["filter"] = "alpha(opacity=100)"; 55 oArr[x] = lenArr[i] + oArr[x] + 10; 56 } 57 document.getElementById("loadimg").style.top = _getMaxValue(oArr) + 50 + "px";//loading向下移位 58 liLen = liLenNew; 59 window.onscroll = scroll;//执行完成,恢愎onscroll事件 60 }); 61 }) 62 } 63 } 64 window.onscroll =scroll; 65 } 66 //图片加载完成后执行 67 window.onload = function() {flow(10, 10)}; 68 //... 省略部份代码 ...
74 //追加项 75 function _addItem(arr, callback) { 76 var _html = ""; 77 var a = 0; 78 var l = arr.length; 79 (function loadimg() { 80 var img = new Image(); 81 img.onload = function() { 82 a += 1; 83 if (a == l) { 84 for (var k in arr) { 85 var img = new Image(); 86 img.src = arr[k].img; 87 _html += '<li><img src="' + arr[k].img + '" /><a href="#">' + arr[k].title + '</a></li>'; 88 } 89 _appendhtml(document.getElementById("flow-box"), _html); 90 callback(); 91 } 92 else { 93 loadimg(); 94 } 95 } 96 img.src = arr[a].img; 97 })() 98 } 99 //ajax请求 100 function _request(reqdata, url, callback) { 101 var xmlhttp; 102 if (window.XMLHttpRequest) { 103 xmlhttp = new XMLHttpRequest(); 104 } 105 else { 106 xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); 107 } 108 xmlhttp.onreadystatechange = function () { 109 if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { 110 var data = eval("(" + xmlhttp.responseText + ")"); 111 callback(data); 112 } 113 } 114 xmlhttp.open("POST", url); 115 xmlhttp.setRequestHeader("Content-Type", "application/json; charset=utf-8"); 116 xmlhttp.send(reqdata); 117 } 118 //追加html 119 function _appendhtml(parent, child) { 120 if (typeof (child) == "string") { 121 var div = document.createElement("div"); 122 div.innerHTML = child; 123 var frag = document.createDocumentFragment(); 124 (function() { 125 if (div.firstChild) { 126 frag.appendChild(div.firstChild); 127 arguments.callee(); 128 } 129 else { 130 parent.appendChild(frag); 131 } 132 })(); 133 } 134 else { 135 parent.appendChild(child); 136 } 137 } 138 //获取数字数组的最大值 139 function _getMaxValue(arr) { 140 //... 省略部份代码 ...
147 } 148 //获取数字数组最小值的索引 149 function _getMinKey(arr) { 150 //... 省略部份代码 ...
159 } 160 </script>
这一步涉及的代码比较多,简单概括其实就是多了几个方法:scroll()、_addItem()、_request()、_appendhtml()。
主要是看scroll()。在这里_addItem()和_requeat()是供scroll()调用的,而_appendhtml()是供_addItem()调用的。
这一步的整个过程是:当页面滚动到最短的一列数据的底部时就发出ajax请求加载新的数据,然后待数据中的图片全部load完后就追加到页面上,然后将这些数据项的高度写入到数组lenArr中,并对新加入的这些数据项进行定位,按照每一项都放在最短列的后面的规则而排放在适当的位置上,最后再将loading图片向下移到最底部的位置。
总结以上的整个思路,有4个地方值得一说:
1、缩放浏览器窗口时,onresize的触发很频繁,为降低性能损耗,需要待缩放结束后再执行重排,以上思路是使用setTimeout来处理。
2、页面滚动到最下面加载新数据的时候,只需对新数据排列。
3、以上思路中加载新数据要等图片都加载完成后才知道其高度,但实际项目中最好是服务器能给定高度值。
4、滚动触发加载新数据时,要避免事件多次触发,以上思路是将onscroll事件置为空,加载完成后再将事件恢复。
最后附上完整的代码:
flow.html
GetList.php