geekwen.com

瀑布流布局的实现

前段时间工作上的事情不是很多。于是利用一些时间慢慢的试着开发自己的插件。目前有一个瀑布流布局的插件已经完工了(项目地址:@geekwen - waterfall)。瀑布流布局不是个什么新鲜玩意儿。很多网站都可以看到这种布局的身影。但目前纯css的解决方案在PC端的兼容性还不是很理想,所以大多数的瀑布流布局还是通过JS来计算的。下面大致分享一下实现的思路。

瀑布流布局的逻辑大致是这样:假设我们是将一组元素一个个往某个容器里面加,每次添加的规则是这样:找到高度最低的列然后插入到该列,若有几列的高度一样则从左往右排布;按如此规则循环,直到所有元素添加到容器中。整个逻辑中共有这么些关键点:

  1. 元素的最小宽度和容器的宽度以及元素之间的间距。这样才能计算出有几列
  2. 列如何实现?
  3. 每次“插入”元素时如何确定哪一列的高度最低?

参数约定

  1. colMinWidth:代表元素(或者说列)最小宽度;
  2. wrapperWidth:代表容器宽度;
  3. colGapWidth:代表列之间间隙;
  4. colWidth:代表列的实际宽度;
  5. columnCount: 代表一共有多少列;

如何计算有几列?

进行布局之前,我们可以确定拿到的数据有:wrapperWidth(直接通过js去获取)以及我们可以进行自己设置的colMinWidthcolGapWidth。根据这三个值我们可以通过一个简单的公式来计算出columnCount的值。公式如下:

1
columnCount = Math.floor((wrapWidth + colGapWidth) / (colMinWidth + colGapWidth))

因为列数肯定是整数,所以这里用到了Math.floor来取得小于且离公式结果最近的整数。

为何还需要计算列的实际宽度?

因为需要填充整个容器。如果完全按照给定的colMinWidthcolGapWidth来布局,很有可能不能填充整个容器。所以这里的colMinWidth只是一个预设的最小宽度,一个参照值。真正的列宽度则是利用刚才计算出来的columnCount重新推算:

1
colWidth = (wrapWidth - (columnCount - 1) * colGapWidth) / columnCount;

布局时利用这个值来做列的宽度,才能使瀑布流真正的布满整个容器。

如何布局?

按照之前所说的布局逻辑,很自然的想到,瀑布流中的各元素肯定是用position:absolute,通过计算每个元素对应的top, left值来定位。那么怎么来判断哪一列高度最低?这里我们可以通过用一个数组来储存每一列的高度。

首先,当我们计算出了一共有多少列后,初始化一个拥有列个数长度的数组,且数组中每一项的值为零,也就是说一开始每一列的高度都是零:

1
2
3
4
var columns = [];
for (var i = 0; i < columnCount; i++) {
columns[i] = 0;
}

然后,开始循环计算:

  1. columns中取出当前最小的值,及其对应的序号
  2. 然后计算该元素的top, left
  3. 最后将该元素的高度加上colGapWidth,累加到对应的columns项中
  4. 开始计算下一元素,直到结束
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
//$item 是瀑布流中的所有元素
$item.each(itemEach);
function itemEach() {
var $this = $(this),
// 该元素的高度;之所以用outerHeight是因为要包括border
// 但同时,outerHeight会包括margin,所以瀑布流元素的margin必需为0
thisHeight = $this.outerHeight(),
// 当前高度最小的列
colMinHeight = Math.min.apply(Math, columns),
// 当前高度最小列在数组中的序号
colMinHeightIndex = columns.indexOf(colMinHeight);
// 设置top值:即当前最小高度的值
// 设置left值:就是该元素左边所有的列宽加所有的间隙宽度
$this.css({
"top": colMinHeight,
"left": colMinHeightIndex * (colWidth + colGapWidth)
});
// 将该元素的高度累加到对应的columns中去,并加上间隔宽度
columns[colMinHeightIndex] += parseInt(thisHeight) + colGapWidth;
$this = null;
}

最后,为了增加插件的实用性,还可以监听resize事件,让瀑布流动态响应容器的宽度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// action 是封装好的布局方法
$(window).resize(function () {
delay_till_last("resize", action, 500);
});
var timer = {};
// 减少重复调用次数
function delay_till_last(id, fn, wait) {
// 如果已经设置了倒计时,则删除之前的倒计时
if (timer[id]) {
window.clearTimeout(timer[id]);
delete timer[id];
}
// 设置倒计时,若执行完了目标函数,则在timer对象中删除储存的信息
return timer[id] = window.setTimeout(function () {
fn();
delete timer[id];
}, wait);
}

此处用了一个小技巧:延迟执行。因为resize事件很有可能会在短时间内大量重复的触发,为了保证性能,需要限制每隔多久才能执行一次。

到这里,整个插件的核心都讲解完毕。仅不到100行的代码就解决了这样的问题。源码在 GitHub 上,如有任何问题欢迎与我邮件交流。希望对各位访客有帮助。如果觉得不错,在 GitHub 上我个星吧😁。

@waterfall by Geekwen

参考链接:瀑布流布局的实现步步升级(原生JS)