最近爬了一些漂亮的小姐姐的图片,做成一个小网站妹子图-魔力美少女,专门孝敬给各位码农大哥。网站使用瀑布流布局,简单总结经验如下:
什么是瀑布流?
Pinterest Masonry 瀑布流布局是一种常见的网页布局方式,常见于图片类网站。看两个例子:
妹子图 - Bing images
小红书
它的特点是每个元素的宽度相同,但是高度不同,且元素之间的高度差异较大。大家厌烦了普通的网格布局,而 Pinterest Masonry 瀑布流让元素有错位的感觉,看起来更加有趣。
当然,这种布局现在也被广泛应用在各种网站上,也造成了审美疲劳。任何创意都有其适用范围,最好的布局还是因地制宜,不要盲目追求创意。
比如,图片类型的网站,依然非常适合 Pinterest Masonry 瀑布流布局。因为图片的大小不一,而且图片之间的高度差异较大,这种布局就非常适合。而视频类的网站,因为视频的长宽比一般固定,自然就也不需要此布局。
为什么叫瀑布流(无限滚动)?
老外把这种布局叫做 Masonry,是“彻砖”的意思,侧重于该布局的静态表现。
中文把这种布局叫做 “瀑布流”,侧重于强调该布局的动态效果。指网页向下滚动,数据自动加载,然后页面高度增长,也称作无限滚动(infinite scroll) 。
接下来介绍几种 Masonry 布局的几种实现方案,并陈述其利弊。
最省事的方案 columns
columns 是纯CSS方案,不需要JS,也不需要任何库。只需要在父元素上设置 column-count 属性,然后在子元素上设置 break-inside 属性即可(break-inside 也可以不用设置)。
columns:用于设置元素的列宽和列数。它是column-width和column-count的简写属性。
结构如下 (HTML):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<div class="container">
<div class="item" style="height: 140px"></div>
<div class="item" style="height: 190px"></div>
<div class="item" style="height: 170px"></div>
<div class="item" style="height: 120px"></div>
<div class="item" style="height: 160px"></div>
<div class="item" style="height: 180px"></div>
<div class="item" style="height: 140px"></div>
<div class="item" style="height: 150px"></div>
<div class="item" style="height: 170px"></div>
<div class="item" style="height: 170px"></div>
<div class="item" style="height: 140px"></div>
<div class="item" style="height: 190px"></div>
<div class="item" style="height: 170px"></div>
</div>
|
CSS 如下:
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
|
.container {
max-width: 600px;
margin: 20px auto;
columns: 4;
gap: 20px;
counter-reset: items;
}
.item {
border-radius: 3px;
background-color: #a1cbfa;
border: 1px solid #4290e2;
color: #fff;
padding: 15px;
margin-bottom: 10px;
box-sizing: border-box;
/*防止元素内部分离而导致出现在两列*/
break-inside: avoid;
}
/* item排序 */
div.item::before {
counter-increment: items;
content: counter(items);
}
|
效果如下:
这种方式可以简单快速实现砖墙布局,几近完美。有两个瑕疵:
一个是子元素是从上到下排列,这个是下一个问题的源头。
一般而言,砖彻上去之后,理论上是不地动的,在上面添加新的砖,只会增加墙的高度,而不会重新排列旧的砖头的位置。而这种方式,添加新的元素,会导致旧的元素重新排列,这是不符合砖墙布局的特性的。
例如,上面的例子,新增加砖头之后,那么,旧的砖头,会重新排列,这是不符合砖墙布局的特性的。
原本第4、5 块砖头,是在第2列的,但是,当新增加砖头之后,第4、5块砖头,就会被移动到第1列,这是不符合现实情况,当然,网页世界也不用完全对应现实世界。但是,这种方式会导致浏览过的内容,反复出现在不同的位置,这是不符合用户的预期的。
Grid Masonry
这种方式是通过设置row的高度,来实现瀑布流布局的。 原理如下:
1
2
3
4
5
|
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-gap: 1rem;
}
|
横向的图片设置成short, 纵向的图片设置成tall
1
2
3
4
5
6
|
.short{
grid-row:span 1;
}
.tall{
grid-row:span 2;
}
|
这种方式,可以实现瀑布流布局的特性,但是,这种方式,需要手动计算每个砖的高度,然后设置row的高度,这样做,会导致砖墙布局的实现,和砖的高度,有很大的关系,如果砖的高度,发生了变化,那么,砖墙布局的实现,也会发生变化。
flexbox 方案
CSS masonry with flexbox, :nth-child(), and order | Tobias Ahlin
flexbox 通过设置子元素的 order 属性,来实现瀑布流布局的。原理如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/* Render items as columns */
.container {
display: flex;
flex-flow: column wrap;
}
/* Re-order items into rows */
.item:nth-child(3n+1) { order: 1; }
.item:nth-child(3n+2) { order: 2; }
.item:nth-child(3n) { order: 3; }
/* Force new columns */
.container::before,
.container::after {
content: "";
flex-basis: 100%;
width: 0;
order: 2;
}
|
效果如下:
flex-flow 是 flex-direction 和 flex-wrap 组合的简写属性 。第一个指定的值为 flex-direction ,第二个指定的值为 flex-wrap.
这种方式非常的巧妙,而且让元素看上去是从左到右横向排列的(从左到右,符合预期)。这个方案算得上真正的完美,唯一的瑕疵就是代码量大。如果要做成响应式,适配不同的窗口大小,那css的代码量会更大一点。
JavaScript 方案
以上CSS的方案各有优缺点,也存在浏览器兼容性问题,要想真正实现 Masonry 布局,还得引入 JavaScript,推荐两实用的类库:
其原理原理 大致为:
- 写一个分列的方法 generateMasonryGrid(columns_count,posts)
- 网页 resize 时再重新分列
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
const container = document.querySelector('.container');
function generateMasonryGrid(columns, posts){
container.innerHTML = '';
let columnWrappers = {};
for(let i = 0; i < columns; i++){
columnWrappers[`column${i}`] = [];
}
for(let i = 0; i < posts.length; i++){
const column = i % columns;
columnWrappers[`column${column}`].push(posts[i]);
}
for(let i = 0; i < columns; i++){
let columnPosts = columnWrappers[`column${i}`];
let div = document.createElement('div');
div.classList.add('column');
columnPosts.forEach(post => {
let postDiv = document.createElement('div');
postDiv.classList.add('post');
let image = document.createElement('img');
image.src = post.image;
let hoverDiv = document.createElement('div');
hoverDiv.classList.add('overlay');
let title = document.createElement('h3');
title.innerText = post.title;
hoverDiv.appendChild(title);
postDiv.append(image, hoverDiv)
div.appendChild(postDiv)
});
container.appendChild(div);
}
}
let previousScreenSize = window.innerWidth;
window.addEventListener('resize', () => {
imageIndex = 0;
if(window.innerWidth < 600 && previousScreenSize >= 600){
generateMasonryGrid(1, posts);
}else if(window.innerWidth >= 600 && window.innerWidth < 1000 && (previousScreenSize < 600 || previousScreenSize >= 1000)){
generateMasonryGrid(2, posts);
}else if(window.innerWidth >= 1000 && previousScreenSize < 1000){
generateMasonryGrid(4, posts)
}
previousScreenSize = window.innerWidth;
})
if(previousScreenSize < 600){
generateMasonryGrid(1, posts)
}else if(previousScreenSize >= 600 && previousScreenSize < 1000){
generateMasonryGrid(2, posts)
}else{
generateMasonryGrid(4, posts)
}
|
我的方案
我写的妹子图主要使用了columns的方案,这样简单省事。除了做滚动加载之外,我还在前面也添加了“加载更多”的按钮。同时创造性的数据前插
,一定程度上让图片的展示更加凌乱。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
useEffect(() => {
if (!isFetching) return;
let url = `/api/page/${page}`;
setTimeout(() => {
fetch(url)
.then((res) => res.json())
.then((data) => {
setIsFetching(false);
// 数据往前插
setPosts([...data, ...posts])
});
}, 1000);
}, [isFetching, page, posts])
|
未来的方案
CSS Grid Layout Module Level 3
目前 CSS 布局模块Level 3已经进入到 ED(Editor’s Draft)阶段,该规范为 grid 添加了一个名为 masonry
值,可以轻松的实现瀑布流。届时,浏览器布局将消耗更多的计算性能。
参考资料