最近看taobaoued的一篇博客关于瀑布流布局,之前也看过同学公司的网站用的这种布局,很新颖但不知道是什么名字,为提升实践能力,我就打算写一个瀑布流插件,弄了一天,现在放在github waterfallplugin上了。
Pinterest最早开始使用这种布局,后来国内的蘑菇街之类的网站开始跟风。这种布局明显适合那种大量图片显示的网站
实现方式
一.多列浮动
使用这种方式的网站有:
蘑菇街,我同事的公司网站
这种方式最容易想到,让几个div float在一个container中,每次加载出新内容时按计算结果向不同div中添加新的模块
优点:
- 简单易实现
- 不用明确知道数据块高度,当数据块中有图片时,就不需要指定图片高度。(这一点很重要)
缺点:
- 列数固定,当浏览器窗口变化时,只能固定n列,出现滚动条
二.css3多栏布局
可查询column-count了解不多叙述。
优点:
- 简单不用写代码
缺点:
- css3兼容性问题
三.绝对定位
这种方式很高端,有难度,我写的插件也是采用的这种方式。使用这种方式的有:Pinterest, KISSY
使用我的WaterFall需要一句初始化代码即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| new WaterFall({ container: "#main-container", //父容器 minColumn: 2, //resize window时保留的最小列数 colWidth: 208, //每列宽度,当前列数就去 max(container.width/(colWidth+colGap), minColumn) colGap: 10, //单元格列间距 rowGap: 10, //单元格行坚决 load: function(successCallBack){ //每次加载固定数据的函数,success为调整位置的函数 $.ajax({ url: 'items.json', data: {}, type: 'get', dataType: 'json', success: function(result){ if(result.code == 0) successCallBack(result.data, '#watertmpl'); } }); } })
|
从这里也大概可以看出代码的核心:
1. 获取列数,用container宽度除以设置的(列宽+列间隙)与设置的最小列数进行比较取最大; 这样保证了可以在window resize时自动变化列数,同时避免过窄保证有基本的列数
2. 保存每一列的高度,记录最高高度列和最低高度列;在最低高度列中插入新的元素(设置position:absolute; left:index*(colWidth+colGap); top: colsHeight[index]); 然后更新整个高度数组,并将容器高度设置为最高高度列的高度;
有以下几点值得思考:
(1). 插入元素没有使用拼接字符串,而是采用doT模板引擎;这样是为了可扩展性,因为不知道在每个item内部dom结构,这个需要用户自己来填写; 限定定了容器id=”main-container”,单个item class=”waterfall-item”
1 2 3 4 5 6 7 8
| <script id="watertmpl" type="text/x-dot-template"> <div class="waterfall-item"> <img src="{{= it.imgUrl}}"/> <p>{{= it.briefIntro}}</p> </div> </script> <div id="main-container"> </div>
|
(2). 使用模板引擎之后在元素填充到dom之前,需要修改其style定义left和top值将元素插入到相应的列。因为采用了分批加载,每次获得的都是一组数组,所以要对数组数据进行分批处理。
这里就出现了一个问题:
只有在每个元素插入到dom中之后才能获取它实际的高度,进而更新每个列的高度,进而进行下一个元素的插入计算,但如果item中含有图片,则在图片没有加载完之前获取的offsetHeight是不准确的,怎么办???
解决方法有两个:
一.服务器端返回item中图片的高度。
二.服务器端不支持,客户端在加载一张图片结束之后进行下一个元素的插入。
实际效果可想而知,明显第一个方法体验更好,性能更好。
但是我的插件这里采用的第二种方法,为了更加独立的完成这个效果:
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| var loadingTag = (function(){ //串行渲染标识 var isLoading = false; return function(il){ if(il != undefined){ isLoading = il; } return isLoading; }; })(); /** * 每次加载一页数据之后,添加到dom中的函数。 * 此方法不能并行被调用,即一组数据还没有彻底填充完,另一组数据不能开始填充。这也是由图片高度未知导致的 * @param arr * @param tmplId */ function addItems(arr, tmplSelector){ if(!isArray(arr)) return; var waterTmpl = doT.template(document.querySelector(tmplSelector).innerText); addBundleItems(arr, 0, waterTmpl); } function addBundleItems(arr, i, waterTmpl){ if(i >= arr.length){ loadingTag(false); return; } var newItemText = waterTmpl(arr[i]); var posItemText = calculatePosition(newItemText); container.innerHTML += posItemText; var imgs = document.querySelectorAll('.waterfall-item img'); imgs[imgs.length-1].onload = function(){ updateHeight(this.offsetHeight + 30); //在服务器没有返回图片高度的情况下。之后自己计算高度,等到图片加载完成之后才能确切高度;进行下一个item插入 i++; addBundleItems(arr, i, waterTmpl); } };
|
由于存在onload事件,所以一定会用到递归调用。另外这个方法是不能并行被调用的,即必须在一批数组全部填充到dom中之后才能开始填充下一批数据。这也是由于图片加载完成之前高度未知决定的。所以这里使用了一个标记位。isLoading放到闭包中,避免被误修改。
(3). calculatePosition函数用于向doT渲染出来的html字符串中插入left和top属性值。之后将修改的字符串返回填充到dom中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| /** *将html字符串中加入left和top值 * @param newItemText */ function calculatePosition(newItemText){ var leftStyle = /[^<]*(style\s*=\s*").*waterfall-item[^>]*/ig; var rightStyle = /[^<]*waterfall-item.*(style\s*=\s*")[^>]*/ig; var leftResult = leftStyle.exec(newItemText); //匹配结果:[" div class="waterfall-item" style = "" ", "style = ""] var rightResult = rightStyle.exec(newItemText); //匹配结果:[" div class="waterfall-item" style = "" ", "style = ""] var leftPos = minIndex * (configs.colWidth + configs.colGap); var topPos = colsHeight[minIndex] + configs.rowGap; if(!!leftResult && leftResult.length != 0){ //style在class左侧 newItemText = newItemText.replace(leftResult[1].toString(), leftResult[1].toString() + "left:" + leftPos + "px; top:" + topPos + "px; width:" + configs.colWidth + "px;"); } else if(!!rightResult && rightResult.length != 0){ //style在class右侧 newItemText = newItemText.replace(rightResult[1].toString(), rightResult[1].toString() + "left:" + leftPos + "px; top:" + topPos + "px; width:" + configs.colWidth + "px;"); } else { //没有style var temp = newItemText.match(/[^<]*waterfall-item[^>]*/i)[0]; newItemText = newItemText.replace(temp.toString(), temp.toString() + 'style = "top:' + topPos + 'px; left:' + leftPos + 'px; width:' + configs.colWidth + 'px;"'); } return newItemText; }
|
分为
- class=”waterfall-item” style=””
- style=”” class=”waterfall-item”
- class=”waterfall-item”
三种情况进行正则匹配,之后修改style属性值添加left和top属性
(4). updateHeight方法用于在填充dom之后,同步每一列的高度数据。提供给下一次的calculatePosition方法执行调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| /** * 更新列高度数组,和最小高度列的下标 * @param newItemHeight */ function updateHeight(newItemHeight){ newItemHeight = newItemHeight || 0; colsHeight[minIndex] += newItemHeight; for(var i in colsHeight){ colsHeight = colsHeight; //没有初始化的进行初始化 if(colsHeight[i] < colsHeight[minIndex]){ minIndex = i; } if(colsHeight[i] > colsHeight[maxIndex]){ maxIndex = i; } } container.style.height = colsHeight[maxIndex] + "px"; //更改容器高度为所有列中最大 }
|
3. 设置window.onresize,动态改变列数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| /** * 窗口变化时,动态改变已有元素排列,只对已有元素 */ function reflow(){ colNum = Math.max(parseInt(container.offsetWidth/(configs.colWidth+configs.colGap)), configs.minColumn); //列数 colsHeight.length = 0; minIndex = 0; maxIndex = 0; //初始化高度数组都为0 for(var i=0; i<colNum; i++){ colsHeight[i] = 0; } var items = document.querySelectorAll('.waterfall-item'); Array.prototype.forEach.call(items, function(item, i){ var leftPos = minIndex * (configs.colWidth + configs.colGap); var topPos = colsHeight[minIndex] + configs.rowGap; item.style.left = leftPos + "px"; item.style.top = topPos + "px"; updateHeight(item.offsetHeight); }); } var timer; window.onresize = function(){ clearTimeout(timer); timer = setTimeout(function(){ reflow(); }, 300); };
|
resize时使用setTimeout优化性能;对已有元素重排
1 2 3 4 5 6 7 8 9 10 11
| /** * 滚动条是html.clientHeight < html.offsetHeight 所以是由于html过长导致的滚动条出现 * @param e */ window.onscroll = function(e){ var t = document.documentElement.scrollTop || document.body.scrollTop; if(!loadingTag() && t + document.documentElement.clientHeight > container.offsetHeight - configs.loadDistance){ loadingTag(true); configs.load(addItems); } }
|
普通html页面由于过长产生的滚动条是由于html元素太长在window中显示出的滚动条;
5. 设置首屏加载。注意临界条件的判断
1 2 3 4 5 6 7 8 9 10 11 12
| /** * 加载第一屏 */ var firstPageInterval; firstPageInterval = setInterval(function(){ if(!loadingTag() && container.offsetHeight < document.documentElement.clientHeight) { loadingTag(true); configs.load(addItems); } else if(container.offsetHeight > document.documentElement.clientHeight){ clearInterval(firstPageInterval); } }, 200);
|
以下是最终效果