Pinterest Masonry 瀑布流布局方案详解
最近爬了一些漂亮的小姐姐的图片,做成一个小网站 妹子图-魔力美少女[1],http://meizitu.mzh.ren/ 专门孝敬给各位码农大哥。网站使用瀑布流布局,简单总结经验如下:
什么是瀑布流?
Pinterest Masonry 瀑布流布局是一种常见的网页布局方式,常见于图片类网站。看两个例子:
妹子图 - Bing images[2]
小红书[3]
它的特点是每个元素的宽度相同,但是高度不同,且元素之间的高度差异较大。大家厌烦了普通的网格布局,而 Pinterest Masonry 瀑布流让元素有错位的感觉,看起来更加有趣。
当然,这种布局现在也被广泛应用在各种网站上,也造成了审美疲劳。任何创意都有其适用范围,最好的布局还是因地制宜,不要盲目追求创意。
比如,图片类型的网站,依然非常适合 Pinterest Masonry 瀑布流布局。因为图片的大小不一,而且图片之间的高度差异较大,这种布局就非常适合。而视频类的网站,因为视频的长宽比一般固定,自然就也不需要此布局。
为什么叫瀑布流(无限滚动)?
老外把这种布局叫做 Masonry,是“彻砖”的意思,侧重于该布局的静态表现。
中文把这种布局叫做 “瀑布流”,侧重于强调该布局的动态效果。指网页向下滚动,数据自动加载,然后页面高度增长,也称作无限滚动(infinite scroll) 。
接下来介绍几种 Masonry 布局的几种实现方案,并陈述其利弊。
最省事的方案 columns
columns 是纯CSS方案,不需要JS,也不需要任何库。只需要在父元素上设置 column-count 属性,然后在子元素上设置 break-inside 属性即可(break-inside 也可以不用设置)。
columns:用于设置元素的列宽和列数。它是column-width和column-count的简写属性。
结构如下 (HTML):
<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 如下:
.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的高度,来实现瀑布流布局的。原理如下:
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-gap: 1rem;
}
横向的图片设置成short, 纵向的图片设置成tall
.short{
grid-row:span 1;
}
.tall{
grid-row:span 2;
}
这种方式,可以实现瀑布流布局的特性,但是,这种方式,需要手动计算每个砖的高度,然后设置row的高度,这样做,会导致砖墙布局的实现,和砖的高度,有很大的关系,如果砖的高度,发生了变化,那么,砖墙布局的实现,也会发生变化。
flexbox 方案
CSS masonry with flexbox, :nth-child(), and order | Tobias Ahlin[4]
flexbox 通过设置子元素的 order 属性,来实现瀑布流布局的。原理如下:
/* 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,推荐两实用的类库:
• Masonry[5]
• Infinite Scroll[6]
其原理原理[7] 大致为:
• 写一个分列的方法 generateMasonryGrid(columns_count,posts)
• 网页 resize 时再重新分列
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的方案,这样简单省事。除了做滚动加载之外,我还在前面也添加了“加载更多”的按钮。同时创造性的数据前插
,一定程度上让图片的展示更加凌乱。
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[8]
目前 CSS 布局模块Level 3已经进入到 ED(Editor’s Draft)阶段,该规范为 grid 添加了一个名为 masonry
值,可以轻松的实现瀑布流。届时,浏览器布局将消耗更多的计算性能。
参考资料
• CSS { In Real Life } | Masonry? In CSS?![9]
• Masonry[10]
• CSS Grid Layout Module Level 3[11]
• break-inside | CSS-Tricks - CSS-Tricks[12]
• CSS Multi-column Layout Module Level 1[13]
• Using multi-column layouts - CSS: Cascading Style Sheets | MDN[14]
• grid-auto-rows - CSS: Cascading Style Sheets | MDN[15]
• break-inside - CSS: Cascading Style Sheets | MDN[16]
• html - CSS-only masonry layout - Stack Overflow[17]
• CSS masonry with flexbox, :nth-child(), and order | Tobias Ahlin[18]
• Pinterest-like masonry layout with placed and spanning items[19]
• flex 布局的基本概念 - CSS(层叠样式表) | MDN[20]
往期推荐
欢迎关注我的公众号“码中人”,原创技术文章第一时间推送。
引用链接
[1]
妹子图-魔力美少女: http://meizitu.mzh.ren/[2]
妹子图 - Bing images: https://www.bing.com/images/search?q=%E5%A6%B9%E5%AD%90%E5%9B%BE&qs=n&form=QBIR&sp=-1&pq=%E5%A6%B9%E5%AD%90%E5%9B%BE&sc=10-3&cvid=76204E25004B4273B0A715F5267195EC&ghsh=0&ghacc=0&first=1&tsc=ImageHoverTitle&cw=1177&ch=697[3]
小红书: https://www.xiaohongshu.com/explore[4]
CSS masonry with flexbox, :nth-child(), and order | Tobias Ahlin: https://tobiasahlin.com/blog/masonry-with-css/[5]
Masonry: https://masonry.desandro.com/[6]
Infinite Scroll: https://infinite-scroll.com/[7]
原理: https://github.com/conorbailey90/masonry-grid-layout/blob/main/app.js[8]
CSS Grid Layout Module Level 3: https://drafts.csswg.org/css-grid-3/[9]
CSS { In Real Life } | Masonry? In CSS?!: https://css-irl.info/masonry-in-css/[10]
Masonry: https://masonry.desandro.com/[11]
CSS Grid Layout Module Level 3: https://drafts.csswg.org/css-grid-3/[12]
break-inside | CSS-Tricks - CSS-Tricks: https://css-tricks.com/almanac/properties/b/break-inside/[13]
CSS Multi-column Layout Module Level 1: https://www.w3.org/TR/css-multicol-1/[14]
Using multi-column layouts - CSS: Cascading Style Sheets | MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Columns/Using_multi-column_layouts[15]
grid-auto-rows - CSS: Cascading Style Sheets | MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/grid-auto-rows[16]
break-inside - CSS: Cascading Style Sheets | MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/break-inside[17]
html - CSS-only masonry layout - Stack Overflow: https://stackoverflow.com/questions/44377343/css-only-masonry-layout[18]
CSS masonry with flexbox, :nth-child(), and order | Tobias Ahlin: https://tobiasahlin.com/blog/masonry-with-css/[19]
Pinterest-like masonry layout with placed and spanning items: https://drafts.csswg.org/css-grid-3/examples/pinterest-with-span.html[20]
flex 布局的基本概念 - CSS(层叠样式表) | MDN: https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Flexible_Box_Layout/Basic_Concepts_of_Flexbox#%E7%AE%80%E5%86%99%E5%B1%9E%E6%80%A7_flex-flow