Please enable Javascript to view the contents

Pinterest Masonry 瀑布流布局方案详解

 ·  ☕ 5 分钟

封面

最近爬了一些漂亮的小姐姐的图片,做成一个小网站妹子图-魔力美少女,专门孝敬给各位码农大哥。网站使用瀑布流布局,简单总结经验如下:

什么是瀑布流?

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);
}

效果如下:

css column 瀑布流效果

这种方式可以简单快速实现砖墙布局,几近完美。有两个瑕疵:

一个是子元素是从上到下排列,这个是下一个问题的源头。

一般而言,砖彻上去之后,理论上是不地动的,在上面添加新的砖,只会增加墙的高度,而不会重新排列旧的砖头的位置。而这种方式,添加新的元素,会导致旧的元素重新排列,这是不符合砖墙布局的特性的。

例如,上面的例子,新增加砖头之后,那么,旧的砖头,会重新排列,这是不符合砖墙布局的特性的。

css column 瀑布流的问题

原本第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;
}

效果如下:

Flexbox 瀑布流布局效果

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 值,可以轻松的实现瀑布流。届时,浏览器布局将消耗更多的计算性能。

CSS Grid Masonry

参考资料

分享

码中人
作者
码中人
Web Developer