瀑布流布局

作者 Simmin 日期 2017-08-16
瀑布流布局

简单介绍

瀑布流布局,在视觉上表现为由一系列等宽不等高的元素组成的多栏布局。

在实际案例中,瀑布流元素多为图片。

示意图如下:
瀑布流示意图

三种实现方式

以瀑布流无限加载图片为例。说是三种实现方式,其实只有两种思路:

  1. 计算图片位置,利用绝对定位进行定位摆放
  2. 利用CSS3 的多栏布局

第一种方式可以由原生JS和jQuery实现,第二种方式利用CSS3 的column 属性实现。

HTML 布局

<body>
<div class="box-group" id="waterfall">
<div class="box">
<div class="pic"><img alt="" src="image/1.jpg"></div>
</div>
<div class="box">
<div class="pic"><img alt="" src="image/2.jpg"></div>
</div>
<div class="box">
<div class="pic"><img alt="" src="image/3.jpg"></div>
</div>
<div class="box">
<div class="pic"><img alt="" src="image/4.jpg"></div>
</div>
<div class="box">
<div class="pic"><img alt="" src="image/5.jpg"></div>
</div>
...
</body>

方式一思路:

  1. 确定每个瀑布流元素的宽度w
  2. 列数c = Math.floor( 网页可见区的宽度 / 单个元素宽度w )
  3. 最外层包裹盒的宽度 = 列数c * 单个元素宽度w
  4. 建立临时高度数组,初始值为前c个元素的高度。
  5. 从第c+1个开始,每次计算当前高度数组中最小值,并将其插入到高度最小的位置,更新高度数组。

方式二思路:

  1. 确定每个瀑布流元素的宽度w
  2. 最外层容器设置column-width设置每列的宽度 或者 column-count设置列数

原生JS

CSS代码

#waterfall{
position: relative;
margin: 0 auto;
}
.box{
padding: 15px 0 0 15px;
// display: inline-block;
// vertical-align: top;
float: left;
}
.pic{
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0px 0px 5px #ddd;
}
.pic img{
width: 300px;
}

JS 代码

window.onload = function(){
var parentNode = document.getElementById('waterfall');
var boxNodes = document.getElementsByClassName('box');
var dataSet = {"images":[{"src":"image/1.jpg"},{"src":"image/2.jpg"},{"src":"image/3.jpg"},{"src":"image/4.jpg"},{"src":"image/5.jpg"}]};
waterfall(parentNode,boxNodes);
//无限滚动加载图片
window.onscroll = function(){
if(checkScrollSlide(parentNode,boxNodes)){ // 检测滚动条件
var images = dataSet.images;
var imagesNum = images.length;
var codeFrag = document.createDocumentFragment();//创建代码片段以减少DOM插入操作
for (var i = 0; i < imagesNum; i++) {
var boxNode = document.createElement('div');
boxNode.className = 'box';
codeFrag.appendChild(boxNode);
var picNode = document.createElement('div');
picNode.className = 'pic';
boxNode.appendChild(picNode);
var imgNode = document.createElement('img');
imgNode.src = images[i].src;
picNode.appendChild(imgNode);
}
parentNode.appendChild(codeFrag);
waterfall(parentNode,boxNodes);
}
}
}
//瀑布流
function waterfall(parentNode,boxNodes){
var boxNum = boxNodes.length;
if(boxNum){
var boxWidth = boxNodes[0].offsetWidth; //单个box的宽度
var columnNum = Math.floor(document.body.clientWidth / boxWidth); //根据body对象可见区的宽度计算最大能放置多少列。
parentNode.style.cssText = "width:"+boxWidth*columnNum+"px"; //计算并设置parentNode 的宽度
var heigthArr = []; //高度存放数组,初始值为第一行元素的高度
for (var i = 0; i < boxNum; i++) {
if(i<columnNum){
heigthArr.push(boxNodes[i].offsetHeight);
}else{
var minHeight = Math.min.apply(null,heigthArr);//计算当前最小高度
var index = heigthArr.indexOf(minHeight); //计算当前最小高度所在的列
boxNodes[i].style.position = 'absolute';
boxNodes[i].style.top = minHeight+'px';
boxNodes[i].style.left = boxNodes[index].offsetLeft+'px';
heigthArr[index] += boxNodes[i].offsetHeight;
}
}
}
}
//判断最后一个box距父元素顶部的距离是否小于滚动距离与网页可视区高度之和
function checkScrollSlide(parentNode,boxNodes){
var boxNum = boxNodes.length;
if(boxNum){
var lastBoxTop = boxNodes[boxNum-1].offsetTop; //最后一个box距父元素顶部的距离
var scrollTop = document.body.scrollTop || document.documentElement.scrollTop;//滚动距离,兼容标准模式和怪异模式
var clientHeight = document.body.clientHeight || document.documentElement.clientHeight;//网页可视区高度,兼容标准模式和怪异模式
return lastBoxTop < (scrollTop + clientHeight) ? true: false;
}else{
return false;
}
}

实现效果:

JavaScript 实现效果

注意点:

1.计算元素绝对定位之前,要先使用float:left使得元素有个初始定位。
(这里没有使用display:inline-block的原因有两个:①对齐方式默认是vertical-align:baseline,由于最开始的第一行需要顶部对齐,所以需要设置vertical-align:top;②display:inline-block设置后元素中间有不明原因的间隙)

float:left 的效果:
float:left

display:inline-block 的效果:
inline-block

display:inline-block + vertical-align:top 的效果:
inline-block + top

元素间间隙问题:
元素间间隙问题

2.各种宽、高和距离问题

【offsetWidth】与【style.width】

同:都是当前对象的宽度(width + padding + border

异:
1) style.width的返回值带有单位px;

2) 对象宽度设置为百分比时,无论页面放大缩小,style.width返回此百分比,offsetWidth返回不同页面大小下的宽度具体值而非百分比;

3) offsetWidth不能赋值,style.width可赋值;

4) 如果没有给 HTML 元素指定过width样式,则 style.width返回的是空字符串。

【offsetHeight】与【style.height】同上

【offsetLeft】与【style.left】

同:都是当前对象距离父元素左边的距离

异:

1) style.left的返回值带有单位px

2) 对象距离父元素左边的距离设置为百分比时,无论页面放大缩小,style.left返回此百分比,offsetLeft返回不同页面大小下的距离具体值而非百分比;

3) offsetLeft不能赋值,style.left可赋值;

4) 如果没有给 HTML 元素指定过left样式,则 style.left返回的是空字符串

【offsetTop】与【style.top】同上

【scrollWidth】获取对象的滚动宽度 。

【scrollHeight】 获取对象的滚动高度。

【scrollLeft】设置或获取位于对象左边界和对象中目前可见内容的最左端之间的距离(width+padding为一体)

【scrollTop】设置或获取位于对象最顶端和对象中可见内容的最顶端之间的距离;(height+padding为一体)

【clientWidth】 获取对象可见内容的宽度,不包括滚动条,不包括边框;

【clientHeight】获取对象可见内容的高度,不包括滚动条,不包括边框;

【clientLeft】 获取对象的border宽度

【clientTop】获取对象的border高度

【offsetParent】当前对象的上级层对象.

jQuery实现

$(window).on('load',function(){
var parentId = 'waterfall';
var boxClass = 'box';
waterfall(parentId,boxClass);
var dataSet = {"images":[{"src":"image/1.jpg"},{"src":"image/2.jpg"},{"src":"image/3.jpg"},{"src":"image/4.jpg"},{"src":"image/5.jpg"}]};
$(window).on('scroll',function(){
if(checkScrollSlide(parentId,boxClass)){
var boxHtml = '';
$.each(dataSet.images,function(i,item){
boxHtml += '<div class="box"><div class="pic"><img src="'+item.src+'"></div></div>'
})
$('#'+parentId).append(boxHtml);
waterfall(parentId,boxClass);
}
})
})
function waterfall(parentId,boxClass){
var parentNode = $('#'+parentId);
var boxNodes = $('.'+ boxClass);
var boxNum = boxNodes.length;
if(boxNum){
var boxWidth = boxNodes.eq(0).outerWidth(); //单个元素宽度
var columnNum = Math.floor($(window).outerWidth() / boxWidth); //列数
parentNode.width(boxWidth*columnNum); //设置父元素宽度
var hArray = [];
boxNodes.each(function(i,item){
if(i<columnNum){
hArray.push($(item).outerHeight());
}else{
var minHeight = Math.min.apply(null,hArray);
var index = $.inArray(minHeight,hArray);
$(item).css({
'position':'absolute',
'top':minHeight+'px',
'left':index*boxWidth+'px'
})
hArray[index] += $(item).outerHeight();
}
})
}
}
function checkScrollSlide(parentId,boxClass){
var parentNode = $('#'+parentId);
var boxNodes = $('.'+ boxClass);
var boxNum = boxNodes.length;
if(boxNum){
var lastBoxTop = boxNodes.last().offset().top;//最后一个元素距离父元素顶部的距离
var scrollTop = $(window).scrollTop(); // 滚动距离
var clientHeight = $(window).height(); //可视区高度
return lastBoxTop < (scrollTop + clientHeight) ? true : false;
}else{
return false;
}
}

注意点:

1.jQuery 中 width()、innerwidth()、outerWidth()、outerWidth(true)

width():元素宽度

innerWidth()padding + 元素宽度

outerWidth()padding + border + 元素宽度

outerWidth(true)margin + padding + border + 元素宽度

2.window.onload 、$(window).on(‘load’,function(){})、$(document).ready(function(){})、document.addEventListener(‘DOMContentLoad’,function(){})

window.onload$(window).load$(window).on('load',function(){}):

页面内包括图片、音视频加载完毕后才执行;window.onload不能编写多个,如果有多个window.onload方法,只会执行一个;$(window).load新版本jQuery已经不支持,使用$(window).on('load',function(){})

$(document).ready(function(){})document.addEventListener('DOMContentLoad',function(){}):

后者是前者的实质,DOM结构绘制完毕后就执行,不必等到加载完毕; $(document).ready()可以同时编写多个,并且都可以得到执行 ;
$(document).ready(function(){})可以简写成$(function(){})

3.减少DOM操作,提高性能

原生JS:利用createDocumentFragment()实现。

jQuery节流:拼接HTML代码后一次性插入DOM

CSS3 多栏布局

CSS 代码

#waterfall{
position: relative;
margin: 0 auto;
column-width: 336px;
}
.box{
padding: 15px 0 0 15px;
}
.pic{
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0px 0px 5px #ddd;
width: 300px;
}
.pic img{
width: 300px;
}

实现效果:

CSS3 实现效果图

实现方式比较

JavaScript 方式:

【优点】图片排序是按照图片计算的位置横向排列,比较规范。

【缺点】需要计算列数、图片的位置

CSS3 方式

【优点】不需要手动计算,浏览器会计算好。只需要设置列宽或者列数,性能高。

【缺点】

  1. 图片的无限加载还是要使用 JavaScript 来实现。
  2. 列宽会随浏览器窗口大小进行改变,用户体验不好。
  3. 图片是竖向一列一列排下来,图片顺序被打乱。而且有时候会出现元素内断行。

元素内部断行

解决方式是在.box上设置-webkit-column-break-inside: avoid;(避免元素内断行)或者设置display:inblock

但是依然存在的问题是,由于box-shadow设置的阴影部分不属于盒模型的大小,还是会有断行现象,这时需要再给.box在设置一些padding即可。

其他

git代码地址:三种方式实现瀑布流布局

主要学习视频:瀑布流布局

参考文章:JS中关于clientWidth offsetWidth scrollWidth 等的含义