一种很受图片网站欢迎的版面布局方式,叫做 Masonry Layout 瀑布流布局。
排版方式特点
内容是由多个不同高度的方格组成,而版面切割为多个直行。这些方格在每一个直行中一个接着一个排列,所以看起来行与行之间会有一些不整齐的感觉。
实现方法
最容易最完美
直接加载别人写好的 JavaScript 套件,
- Masonry.js
- Isotope.js
JavaScript 实现原理
通过 JavaScript 计算合共有多少个方格,再计算每一个方格的宽度和高度,因应容器的宽度可以放置多少行等等要求。
将全部方式的 position 都设定为 absolute,逐一计算出 top 和 left 去定位。
由于所有方格的位置得了是计算出来的,所以还可以在容器宽度改变的时候,将方格动态移动并重新排列。
纯 CSS - CSS Column
准备
https://picsum.photos 服务,可以随机生成一些假图
关键属性
- column-count
column-gap ```html <!DOCTYPE html>
<a name="U2v7f"></a>
## 一些缺陷
用两个 CSS 属性就能生成瀑布流布局,但是这样会现在一些问题。
为每个 item 增加一个 counter 的 小标签:
```css
.item {
padding: 2px;
position: relative;
counter-increment: item-counter;
}
.item::after {
position: absolute;
display: block;
top: 2px;
left: 2px;
width: 24px;
height: 24px;
text-align: center;
line-height: 24px;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
content: counter(item-counter);
}
出来的效果是这样子
我们会发现它的顺序是由每一个直行由上而下,然后再到第二个直行由上而下排列的。
如果想列出的内容是根据时间由新至旧排列,这样的排列顺序就不合适。
纯 CSS - Flexbox
我们期望是由左至右,再由上至下排列。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Masonry</title>
<style>
body {
margin: 4px;
}
.masonry {
display: flex;
flex-direction: column;
flex-wrap: wrap;
height: 1500px;
}
.item {
position: relative;
width: 25%;
padding: 2px;
box-sizing: border-box;
counter-increment: item-counter;
}
.item img {
display: block;
width: 100%;
height: auto;
}
.item::after {
position: absolute;
display: block;
top: 2px;
left: 2px;
width: 24px;
height: 24px;
text-align: center;
line-height: 24px;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
content: counter(item-counter);
}
</style>
</head>
<body>
<div class="masonry">
<div class="item">
<img src="https://picsum.photos/200/300?random=1" />
</div>
<div class="item">
<img src="https://picsum.photos/200/300?random=2" />
</div>
<div class="item">
<img src="https://picsum.photos/360/450?random=3" />
</div>
<div class="item">
<img src="https://picsum.photos/200/300?random=4" />
</div>
<div class="item">
<img src="https://picsum.photos/360/450?random=5" />
</div>
<div class="item">
<img src="https://picsum.photos/600/600?random=6" />
</div>
<div class="item">
<img src="https://picsum.photos/360/450?random=7" />
</div>
<div class="item">
<img src="https://picsum.photos/360/450?random=8" />
</div>
<div class="item">
<img src="https://picsum.photos/100/100?random=9" />
</div>
<div class="item">
<img src="https://picsum.photos/200/300?random=10" />
</div>
<div class="item">
<img src="https://picsum.photos/700/600?random=11" />
</div>
<div class="item">
<img src="https://picsum.photos/200/300?random=12" />
</div>
<div class="item">
<img src="https://picsum.photos/450/600?random=13" />
</div>
<div class="item">
<img src="https://picsum.photos/500/300?random=14" />
</div>
<div class="item">
<img src="https://picsum.photos/300/300?random=15" />
</div>
</div>
</body>
</html>
这样发现与 CSS Column 一样是由上至下,再由左至右排列。
但是 Flexbox 有一个 order 的属性可以让其不跟随 HTML 结构的顺序来排列 。
.item:nth-child(4n+1) {
order: 1;
}
.item:nth-child(4n+2) {
order: 2;
}
.item:nth-child(4n+3) {
order: 3;
}
.item:nth-child(4n) {
order: 4;
}
一些缺陷
要设定 Flex 容器的高度,需要知道内容加起来的高度是多少,还要考虑分多少直行。来为 Flex 容器计算一个合理的高度。
JavaScript
纯 CSS 的瀑布流都会现在一些缺陷,不够完美。借助 JavaScript 使瀑布流布局变得完善。
以每次以直行高度最小的来增加。当需要想由左至右,由上至下的布局,也就修改一下其逻辑就可以了。大逻辑对每张图片的 item 的 top 和 left 的设置。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Masonry</title>
<style>
body {
margin: 4px;
}
.masonry {
position: relative;
width: 1200px;
margin: 0 auto;
}
.item {
position: absolute;
counter-increment: item-counter;
}
.item img {
display: block;
width: 100%;
height: auto;
}
.item::after {
position: absolute;
display: block;
top: 2px;
left: 2px;
width: 24px;
height: 24px;
text-align: center;
line-height: 24px;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
content: counter(item-counter);
}
</style>
</head>
<body>
<div class="masonry">
<div class="item">
<img src="https://picsum.photos/200/300?random=1" />
</div>
<div class="item">
<img src="https://picsum.photos/200/300?random=2" />
</div>
<div class="item">
<img src="https://picsum.photos/360/450?random=3" />
</div>
<div class="item">
<img src="https://picsum.photos/200/300?random=4" />
</div>
<div class="item">
<img src="https://picsum.photos/360/450?random=5" />
</div>
<div class="item">
<img src="https://picsum.photos/600/600?random=6" />
</div>
<div class="item">
<img src="https://picsum.photos/360/450?random=7" />
</div>
<div class="item">
<img src="https://picsum.photos/360/450?random=8" />
</div>
<div class="item">
<img src="https://picsum.photos/100/100?random=9" />
</div>
<div class="item">
<img src="https://picsum.photos/200/300?random=10" />
</div>
<div class="item">
<img src="https://picsum.photos/700/600?random=11" />
</div>
<div class="item">
<img src="https://picsum.photos/200/300?random=12" />
</div>
<div class="item">
<img src="https://picsum.photos/450/600?random=13" />
</div>
<div class="item">
<img src="https://picsum.photos/500/300?random=14" />
</div>
<div class="item">
<img src="https://picsum.photos/300/300?random=15" />
</div>
</div>
</body>
</html>
制作一个瀑布流的插件,使用的属性与 CSS Column 相似
new Waterfall({
el: '.masonry',
column: 5, // column-count
gap: 10, // column-gap
})
定义插件
; (function () {
var Waterfall = function (opt) {
this.el = document.querySelector(opt.el);
this.oItems = this.el.querySelectorAll('div');
this.column = opt.column;
this.gap = opt.gap;
// 计算出每一张图片的宽度
this.itemWidth = (this.el.offsetWidth - (this.column - 1) * this.gap) / this.column;
this.heightArr = [];
this.init();
}
Waterfall.prototype.init = function () {
this.render();
}
Waterfall.prototype.render = function () {
var item = null,
minIdx = -1;
for (var i = 0; i < this.oItems.length; i++) {
item = this.oItems[i];
item.style.width = this.itemWidth + 'px';
if (i < this.column) { // 第一排 高度都为 0
item.style.top = '0px';
item.style.left = i * (this.itemWidth + this.gap) + "px";
var oImg = item.querySelector('img');
if (oImg.complete) {
// 循环后把对应的高度放置 heightArr 中
this.heightArr.push(item.offsetHeight);
}
} else { // 第二排每一个都会找最度最小的直行加入
minIdx = getMinIdx(this.heightArr);
item.style.left = this.oItems[minIdx].offsetLeft + 'px';
item.style.top = (this.heightArr[minIdx] + this.gap) + 'px';
this.heightArr[minIdx] += item.offsetHeight + this.gap;
}
}
function getMinIdx(arr) {
return arr.indexOf(Math.min.apply(null, arr));
}
}
window.Waterfall = Waterfall;
})();
图片加载与图片高度
由于图片是网络加载,高度要图片加载完成后才知道。所以加入一点延迟等待图片完全加载后再进行瀑布布局的重排。
还有一种方案是,由后端提供图片的 URL 和图片的宽高的对象回来 [{src: ‘xxxxx/1.jpg’, width: 300, height: 400}],然后动态生成 img。这样就不用等待网络把加载完的图片回来。
function isImgLoaded(callback) {
const checkLoaded = () => {
const imgs = [].slice.call(document.querySelectorAll('img'));
return imgs.every((img) => {
return img.complete === true;
});
}
const t = setInterval(() => {
if (checkLoaded()) {
clearInterval(t);
callback && callback();
} else {
}
}, 500);
}
最终实现
再加点动画让,图片加载、布局重排后显示没有那么突兀感。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Masonry</title>
<style>
.masonry {
position: relative;
width: 1200px;
margin: 0 auto;
opacity: 0;
transition: opacity 1.2s;
}
.item {
position: absolute;
counter-increment: item-counter;
}
.item img {
display: block;
width: 100%;
height: auto;
}
.item::after {
position: absolute;
display: block;
top: 2px;
left: 2px;
width: 24px;
height: 24px;
text-align: center;
line-height: 24px;
background-color: rgba(0, 0, 0, 0.5);
color: #fff;
content: counter(item-counter);
}
</style>
</head>
<body>
<div class="masonry">
<div class="item">
<img src="https://picsum.photos/100/100?random=1" />
</div>
<div class="item">
<img src="https://picsum.photos/250/200?random=2" />
</div>
<div class="item">
<img src="https://picsum.photos/360/450?random=3" />
</div>
<div class="item">
<img src="https://picsum.photos/300/300?random=4" />
</div>
<div class="item">
<img src="https://picsum.photos/320/420?random=5" />
</div>
<div class="item">
<img src="https://picsum.photos/600/600?random=6" />
</div>
<div class="item">
<img src="https://picsum.photos/360/450?random=7" />
</div>
<div class="item">
<img src="https://picsum.photos/360/450?random=8" />
</div>
<div class="item">
<img src="https://picsum.photos/100/100?random=9" />
</div>
<div class="item">
<img src="https://picsum.photos/200/300?random=10" />
</div>
<div class="item">
<img src="https://picsum.photos/700/600?random=11" />
</div>
<div class="item">
<img src="https://picsum.photos/200/300?random=12" />
</div>
<div class="item">
<img src="https://picsum.photos/450/600?random=13" />
</div>
<div class="item">
<img src="https://picsum.photos/500/300?random=14" />
</div>
<div class="item">
<img src="https://picsum.photos/300/300?random=15" />
</div>
</div>
<script>
function isImgLoaded(callback) {
const checkLoaded = () => {
const imgs = [].slice.call(document.querySelectorAll('.masonry img'));
return imgs.every((img) => {
return img.complete === true;
});
}
const t = setInterval(() => {
if (checkLoaded()) {
clearInterval(t);
callback && callback();
} else {
}
}, 500);
}
; (function () {
var Waterfall = function (opt) {
this.el = document.querySelector(opt.el);
this.oItems = this.el.querySelectorAll('div');
this.column = opt.column;
this.gap = opt.gap;
// 计算出每一张图片的宽度
this.itemWidth = (this.el.offsetWidth - (this.column - 1) * this.gap) / this.column;
this.heightArr = [];
this.init();
}
Waterfall.prototype.init = function () {
this.render();
}
Waterfall.prototype.render = function () {
var item = null,
minIdx = -1;
for (var i = 0; i < this.oItems.length; i++) {
item = this.oItems[i];
item.style.width = this.itemWidth + 'px';
if (i < this.column) { // 第一排 高度都为 0
item.style.top = '0px';
item.style.left = i * (this.itemWidth + this.gap) + "px";
// 循环后把对应的高度放置 heightArr 中
this.heightArr.push(item.offsetHeight);
} else { // 第二排每一个都会找最度最小的直行加入
minIdx = getMinIdx(this.heightArr);
item.style.left = this.oItems[minIdx].offsetLeft + 'px';
item.style.top = (this.heightArr[minIdx] + this.gap) + 'px';
this.heightArr[minIdx] += item.offsetHeight + this.gap;
}
}
function getMinIdx(arr) {
return arr.indexOf(Math.min.apply(null, arr));
}
this.el.style.opacity = '1';
}
window.Waterfall = Waterfall;
})();
isImgLoaded(() => {
new Waterfall({
el: '.masonry',
column: 5, // column-count
gap: 3, // column-gap
})
});
</script>
</body>
</html>