现代 CSS

图解CSS: Grid布局(Part17)

特别声明:如果您喜欢小站的内容,可以点击申请会员进行全站阅读。如果您对付费阅读有任何建议或想法,欢迎发送邮件至: airenliao@gmail.com!或添加QQ:874472854(^_^)

自从多列布局、Flexbox布局和Grid布局得到浏览器支持之后,就可以使用这些特性来实现 瀑布流的布局 ,但这些技术实现的瀑布流布局都或多或少存有一定的缺陷。不过,值得庆幸的是,CSS 网格布局的第3级(CSS Grid Layout Module Level 3) 将真正的瀑布流布局纳入了 W3C 规范中,称得上真正的瀑布流布局。不过遗憾的是,支持该规范草案的主流浏览器并不多,只有Firefox 和 Firefox Nightly。虽然这个功能还不能用于实际生产中,但你的试用以及使用之后的反馈是很有价值的,这有助于确保它满足你对这种布局的要求。

简单地说,该规范定义了 grid-template-columnsgrid-template-rows 新的属性值,即 masonry。可以像下面这样使用,实现一个瀑布流布局:

.grid__container {
    display: grid;
    grid: masonry / 50px 100px auto;
    grid-auto-columns: 200px;
    gap: 10px;
}

.grid__container {
    display: grid;
    grid:  50px 100px auto / masonry;
    grid-auto-rows: 200px;
    gap: 10px;
}

很简单,对吗?为什么以前没有人这样做呢?最主要的原因是瀑布流布局很难标准化,也正因为这个原因,瀑布流布局直到今天才开始进入到 W3C 规范,而且还是草案当中。换句话说,到目前为止,规范中所描述有关于瀑布流布局的方案或特性都有可能随着后续的讨论和浏览器的支持的变化而变化。

即使如此,我们总算是看到了希望。接下来,将和大家一起探讨瀑布流布局相关的技术。在开始之前,我们先简单地了解什么是瀑布流布局?

什么是瀑布流布局

瀑布流布局的出现源于 Pinterest 网站的出现:

因此,有时候你会听到有人会把这种布局称为 Pinterest布局。而且这种模式的布局常用于一些图片展示的Web应用上:

瀑布流布局有着自己独特的特点。在一个容器中有很多项目(通常是图像或像文章的摘要),它们依次一个接一个地按内联方向排列。当他们移到下一行时,项目将移到第一行中较短(高度较低)的项目所留下的任何空隙中。有点类似于我们生活中”砌砖“的方式:

这种布局有点类似于网格布局中自动放置网格项目(Auto Placement)的布局,但又没有严格遵循该布局模式。

瀑布流布局真的是一个网格布局吗?

虽然瀑布流布局被纳入到CSS网格布局模块中,那么瀑布流布局真的是一个网格布局吗?就此话题,社区中很多Web专家有着不同的看法。 @Rachel Andrew 曾经这样说过:

网格不是瀑布流,因为它是一个有严格行和列的网格。如果你再看一下瀑布流创建的布局,并没有严格的行和列。

通常情况下,我们有定义好的行,但列的作用更像是一个Flexbox布局,或多列布局。你用多列布局得到的布局和瀑布流布局之间的关键区别是,在多列布局中,项目是按列排列的。通常,在瀑布流布局中,你希望它们按行排列。

@Rachel Andrew 曾一度建议不要把瀑布流布局作为CSS网格规范的一部分。主要原因是瀑布流布局看上去像网格布局,但它更像是一个相对专业的布局模式,实际上根本不是一个网格。它更类似于Flexbox布局而不是网格布局。也基于这个原因,W3C 的 CSS工作小组才把瀑布流布局当作一个独立的规范,就算是要把他和 CSS 网格规范关联起来,也只能说 CSS 瀑布流布局规范是 CSS 网格布局的一个附本。

@Jen也有过类似的观点:

CSS 瀑布流布局是一个实验性的属性,正处于规范草案的讨论阶段。它还不是官方的,而且可能会改变。不要在博文(或相关教程)中说这已是 W3C 规范,至今为止还不是规范。它只是一个实验性特性,一个原型而以。如果对瀑布流布局特性有任何想法,都可以在 CSSWG 上发表自己的看法。

开启瀑布流布局

Caniuse.com可以得知,支持瀑布流布局的主流浏览器非常的少,到目前为止,只有 Firefox 和 Firefox Nightly 浏览器可以看到瀑布流布局的效果:

正如上图所示,要在 Firefox 或 Firefox Nightly 浏览器查看瀑布流布局的效果,还需要开启相关的配置。在 Firefox 和 Firefox Nightly浏览器的地址栏中输入 about:config,并且搜索 "masonry" 关键词,然后在搜索结果 layout.css.grid-template-masonry-value.enabled标记上双击(鼠标左键双击)其值,从false(默认值)切换到true

你可以尝试着在修改完配置的Firefox或Firefox Nightly浏览器中打开 @Rachel Andrew在Codepen上提供的瀑布流案例,可以看到像下图这样的效果:

我们可以使用成熟的CSS布局方式实现瀑布流布局

其实在《纯CSS实现瀑布流布局》一文中,和大家一起探讨了一些实现瀑布流布局的CSS方案。这几种方案中最接近的方案就是使用CSS的多列布局。

我们使用JavaScript在<body>中动态插入51<div class="item">,并且设置了相应的背景图像:

for (let i = 0; i <= 50; i++) {
    const div = document.createElement("div");
    div.classList.add("item");
    div.style.backgroundImage = `url(https://picsum.photos/500/500?random=${i})`;
    document.body.appendChild(div);
}

.item设置一些基本样式:

.item {
    border-radius: 10px;
    background-size: cover;
    background-position: center;
    height: 10em;
    position: relative;
    display: grid;
    grid-template-rows: 1fr auto;
    margin-bottom: 10px;
    break-inside: avoid;
}

并且给一些.item设置不同的高度:

.item:nth-child(2n) {
    height: 14em;
}

.item:nth-child(3n) {
    height: 18em;
}

.item:nth-child(4n) {
    height: 22em;
}

.item:nth-child(5n) {
    height: 24em;
}

.item:nth-child(6n) {
    height: 30em;
}

.item:nth-child(7n) {
    height: 34em;
}

.item:nth-child(8n) {
    height: 40em;
}

body使用column-countcolumn-gap来设置列数和列与列之间的间距:

body {
    column-count: 4;
    column-gap: 10px;
}

效果如下:

从上面的效果来看,它看起来像瀑布流布局。但是,每外项目的顺序是按列排列的。如果是一个搜索结果页面,期望搜索出来的结果是排列在前面,比如说页面最顶部就能看到搜索的结果,而不是像上面的示例效果,排在前面的都在第一列。

在使用CSS网格模块来构建瀑布流布局时,我原以为 CSS网格布局中的自动定位(Auto Placement) 特性可以让网格项目自动排列:

我们在上面的示例基础上稍作调整,使用CSS网格来实现瀑布流布局:

body {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    grid-auto-flow: dense;
    gap: 10px;
}

.item {
    border-radius: 10px;
    background-size: cover;
    background-position: center;
    height: 10em;
    position: relative;
}

效果如下:

虽然grid-auto-flow: dense;可以实现网格项目自动定位,但它仍然是一个网格布局,无法让网格项目自动往上排列,来填充顶部相邻的网格单元留下的空白:

使用该方案要实现真正的瀑布流布局,仍然离不开JavaScript。使用JavaScript改进的案例可以参阅@Andy Barefoot的《Masonry style layout with CSS Grid》文章中提供的案例:

网格布局的瀑布流特性

上面我们看到的两种CSS实现瀑布流布局的方案可以说不是正宗的。CSS Grid Level 3正在讨论的规范和相关特性才是专门为瀑布流布局服务的。

CSS在 grid-template-columnsgrid-template-rows 属性上提供了一个新的属性值,即 masonry。在网格容器上可以使用这两个属性来显式地指定网格的列和行。比如:

.grid-container {
    display: grid;
    grid-template-columns: 300px 1fr 300px;
    grid-template-rows: repeat(2, auto);
    gap: 10px;
}

上面的代码创建了一个两行三列的网格:

也就是说,如果我们要创建一个瀑布流布局,我们就需要在 grid-template-columnsgrid-template-rows 上显式的设置值为 masonry,比如:

body { 
    display: grid; 
    grid-template-columns: repeat(4, 1fr); 
    grid-template-rows: masonry; 
}

如果你使用Firefox浏览器查看的话这个示例的话,看到的效果像下面这样:

再来看 grid-template-columns 上使用 masonry

body {
    display: grid;
    grid-template-columns: masonry;
    grid-template-rows: repeat(4, 1fr);
    gap: 10px;
}

效果如下:

grid-template-rowsgrid-template-columns 取值为 masonry 两者之间的差异如下图所示:

也可以同时在 grid-template-columnsgrid-template-rows 设置值为 masonry ,只不过这个时候grid-template-columns 使用的值为none (计算值)。

如果要使用瀑布流布局,那么网格轴(grid-template-columnsgrid-template-rows)至少有一个值是 masonry。也就是说,显式在 grid-template-columnsgrid-template-rows 设置值为 masonry 时,该轴就变成了瀑布轴(Masonry Axis),另一个轴将是网格轴:

这样就允许我们在网格轴中使用CSS网格的全部功能。比如可以指定网格线,网格轨道大小,在网格轨道中放置网格项目,也可以对网格行或列进行合并等。比如下面这个示例:

body {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    grid-template-rows: masonry;
    gap: 10px;
}

div:nth-of-type(1) {
    grid-column: 1 / 3;
}

div:nth-of-type(2) {
    grid-area: 1 / 3 / 2 / 5;
}

div:nth-of-type(3) {
    grid-column: span 3;
}

效果如下:

使用浏览器开发者工具查看网格线的编号:

masonry-auto-flow

masonry-auto-flow 是专门为瀑布流布局定义的属性。该属性的主要使用是 让你在瀑布流布局中可以控制瀑布流布局中的项目流(每个网格项目)。它主要接受的值主要有:

masonry-auto-flow:  [ pack | next ] || [definite-first | ordered ] 

它的默认值为pack,该属性只能用于瀑布流布局的网格容器上。

首先,可以通过 masonry-auto-flow: ordered 将导致瀑布流布局中忽略有确定位置的项目,这样就可以使用order修改文档顺序。其次,还可以通过指定masonry-auto-flow指定值为next,来将网格项目依次放置在网格轴上,而不是像上面描述的那样将它们放置在剩余空间最多的轨道上。比如下面这个示例:

<!-- HTML -->
<div class="grid-container">
    <div class="grid-item">1</div>
    <div class="grid-item">2</div>
    <div class="grid-item">3</div>
    <div class="grid-item">4</div>
</div>

/* CSS */
.grid-container {
    display: inline-grid;
    grid: masonry / repeat(3, 1fr);
    border: 1px solid;
    masonry-auto-flow: next;
}

效果如下:

justify-tracks 和 align-tracks

CSS Box Alignment Module Level 3 规范 提供了用于Flexbox容器,Grid容器,Flex项目和Grid项目上的对齐属性。在瀑布流布局新增了justify-tracksalign-tracks属性。当瀑布流是在块轴(Column)方向时,justify-tracks有效;当瀑布流是在内联轴(Row)方向时,align-tracks有效。

align-tracks: [normal | <baseline-position> | <content-distribution> | <overflow-position>? <content-position>]#

justify-tracks: [normal | <content-distribution> | <overflow-position>? [ <content-position> | left | right ] ]#

<baseline-position> = [ first | last ]? && baseline
<content-distribution> = space-between | space-around | space-evenly | stretch
<overflow-position> = unsafe | safe
<content-position> = center | start | end | flex-start | flex-end

align-tracksjustify-tracks 的默认值都是normal,而且都应该应用在是瀑布流布局的网格容器上。他们的属性值和align-contentjustify-content相同,不同的是align-tracksjusttify-tracks可以接受以逗号(,)分隔的多个值。

先来感受一下,align-contentjustify-content 在Flexbox容器和Grid容器中的效果:

这里暂且忽略Flexbox容器上justify-contentalign-content。单独来看网格容器,假设网格轨道整体占据的空间小于网格容器,那么就可以在容器中对齐网格轨道。针对块方向和文本方向的轴线,分别使用 align-content 对齐到块方向的轴线,使用 justify-content 对齐到文本方向的轴线。

瀑布流也是网格布局中的一种,那么align-contentjustify-content也可以运用到瀑布流容器上,但不同的是,对于瀑布流轴,这两个属性是不生效的,在瀑布流轴上,分别被align-tracksjustify-tracks替代。比如下面这个示例:

body {
    display: grid;
    grid-template-columns: repeat(4, 160px);
    grid-template-rows: masonry;
    gap: 10px;

    justify-content: var(--justify-content);
    align-content: var(--align-content);
    align-tracks: var(--align-tracks);
}

就上面示例而言,当grid-template-rows设置值为masonry时,内联轴(Row Axis)是对应的瀑布流轴,块轴(Column Axis)是网格轴。在这种情况之下,align-content在瀑布轴上是不生效的,同时被align-tracks替代(这个时候align-tracks相当于align-content),具体效果如下:

切换示例中justify-contentalign-contentalign-tracks 的效果如下:

再来看另外一个场景,就是grid-template-columns设置为masonry,这个时候内联轴(Row Axis)是网格轴,块轴(Column Axis)是瀑布轴。在这种情况之下,justify-content在瀑布轴上不生效,同时被justify-tracks替代:

body {
    display: grid;
    grid-template-rows: repeat(4, 160px);
    grid-template-columns: masonry;
    gap: 10px;

    justify-content: var(--justify-content);
    align-content: var(--align-content);
    justify-tracks: var(--justify-tracks);
}

具体效果如下:

前面我们提到过,align-tracksjustify-tracks可以同时取多个值。比如下面这个示例,我们有四列的瀑布流网格布局,给align-tracks设置多个值:

body {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    grid-template-rows: masonry;
    gap: 10px;

    align-tracks: start, center, end, space-between;
}

效果如下:

使用JavaScript做降级处理

目前为止,纯CSS实现的瀑布流布局仅得到Firefox或Firefox Nightly浏览器的支持。如果希望在其他浏览器也要实出瀑布流布局的效果,需要一定的JavaScript的脚本。@Ana Tudor 在她的《A Lightweight Masonry Solution》教程中对这方面做过详细的介绍。

我们来看一个简单的示例。在HTML中有一个瀑布流布局的容器,比如:

<!-- HTML -->
<div class="grid__masonry"></div>

使用JavaScript动态往该容器中插入一些网格项目:

for (let i = 0; i <= 50; i++) {
    const div = document.createElement("div");
    div.classList.add("item");
    div.style.backgroundImage = `url(https://picsum.photos/500/500?random=${i})`;
    document.querySelector(".grid__masonry").appendChild(div);
}

可以根据上面介绍的方式,使用CSS实现一个简单的瀑布流布局效果:

.grid__masonry {
    display: grid;
    grid-template-rows: masonry;
    grid-template-columns: repeat(4, 1fr);
    gap: 10px;
}

根据@Ana Tudor 在她的《A Lightweight Masonry Solution》教程提供的示例代码,我们可以使用下面的JavaScript代码,让其他浏览器也实现瀑布流布局效果:

let grids = [...document.querySelectorAll('.grid__masonry')];

if(grids.length && getComputedStyle(grids[0]).gridTemplateRows !== 'masonry') {
    grids = grids.map(grid => ({
        _el: grid, 
        gap: parseFloat(getComputedStyle(grid).gridRowGap), 
        items: [...grid.childNodes].filter(c => c.nodeType === 1 && +getComputedStyle(c).gridColumnEnd !== -1), 
        ncol: 0
    }));

    function layout() {
        grids.forEach(grid => {
            /* get the post relayout number of columns */
            let ncol = getComputedStyle(grid._el).gridTemplateColumns.split(' ').length;

            /* if the number of columns has changed */
            if(grid.ncol !== ncol) {
                /* update number of columns */
                grid.ncol = ncol;

                /* revert to initial positioning, no margin */
                grid.items.forEach(c => c.style.removeProperty('margin-top'));

                /* if we have more than one column */
                if(grid.ncol > 1) {
                    grid.items.slice(ncol).forEach((c, i) => {
                        /* bottom edge of item above */
                        let prev_fin = grid.items[i].getBoundingClientRect().bottom, 

                        /* top edge of current item */
                        curr_ini = c.getBoundingClientRect().top ;

                        c.style.marginTop = `${prev_fin + grid.gap - curr_ini}px`
                    })
                }
            }
        })
    }

    addEventListener('load', e => {
        layout(); /* initial load */
        addEventListener('resize', layout, false) /* on resize */
    }, false);
}

最终的效果如下:

Firefox浏览器下的效果如下:

使用 @supports 做检测

瀑布流布局只有 Firefox 和 Firefox Nightly 浏览器支持。如果你想让其他浏览器也能用纯CSS实现的瀑布流布局,可以使用条件CSS中的@supports做降级处理:

@supports (grid-template-rows: masonry) {
    .list-masonry {
        list-style: none;
        margin: 0;
        padding: 0;
        display: grid;
        grid-gap: var(--grid-gap);
        grid-template-columns: repeat(auto-fill, minmax(16em, 1fr));
        grid-template-rows: masonry;
    }
}

使用CSS Houdini实现瀑布流布局

在不久的将来,我们除了前面提到的一些实现瀑布流布局的方案之外,还可以使用 CSS HoudiniCSS Layout API 来定义一个瀑布流布局。@iamvdo在CSS Houdini示例集锦中就向大家展示了一个使用CSS Houdini实现的瀑布流布局

使用CSS Layout API中的registerLayout()定义一个masonry的布局,代码如下:

// masonry.js
registerLayout('masonry', class {
    static get inputProperties() {
        return [ '--padding', '--columns' ];
    }

    async intrinsicSizes() { /* TODO implement :) */ }
    async layout(children, edges, constraints, styleMap) {
        const inlineSize = constraints.fixedInlineSize;

        const padding = parseInt(styleMap.get('--padding').toString());
        const columnValue = styleMap.get('--columns').toString();

        // We also accept 'auto', which will select the BEST number of columns.
        let columns = parseInt(columnValue);
        if (columnValue == 'auto' || !columns) {
            columns = Math.ceil(inlineSize / 350); // MAGIC NUMBER \o/.
        }

        // Layout all children with simply their column size.
        const childInlineSize = (inlineSize - ((columns + 1) * padding)) / columns;
        const childFragments = await Promise.all(children.map((child) => {
            return child.layoutNextFragment({fixedInlineSize: childInlineSize});
        }));

        let autoBlockSize = 0;
        const columnOffsets = Array(columns).fill(0);
        for (let childFragment of childFragments) {
            // Select the column with the least amount of stuff in it.
            const min = columnOffsets.reduce((acc, val, idx) => {
                if (!acc || val < acc.val) {
                    return {idx, val};
                }

                return acc;
            }, {val: +Infinity, idx: -1});

            childFragment.inlineOffset = padding + (childInlineSize + padding) * min.idx;
            childFragment.blockOffset = padding + min.val;

            columnOffsets[min.idx] = childFragment.blockOffset + childFragment.blockSize;
            autoBlockSize = Math.max(autoBlockSize, columnOffsets[min.idx] + padding);
        }

        return {autoBlockSize, childFragments};
    }
});

这样就可以使用CSS.layoutWorklet.addModule()引入前面已定义好的瀑布流布局的JS文件,比如masonry.js

if ('layoutWorklet' in CSS) {
    // 把自定义的瀑布流布局脚本添加到Layout Worklet中
    CSS.layoutWorklet.addModule('masonry.js');
}

这样就可以在div#masonry使用display:layout(masonry)

<!-- HTML -->
<div id="masonry">
    <div class="masonry__item"></div>
    <!-- ... -->
    <div class="masonry__item"></div>
</div>    

/* CSS */
#masonry {
    display: layout(masonry);
    --padding: 20;
    --columns: 3;
}

正如这个示例所示,使用CSS Layout API可以自定义更多的布局方式,而且使用起来也更灵活,但学习他有一定的成本(可能成本也较高),需要在CSS和JavaScript两个方面都有一定的造诣。因此,CSS Layout API以后注定是小部分开发者的玩具,最终出现的局面一定是少部分人创造,大部分人直接使用。

小结

我们是幸福的,这几年CSS在高速发展,有很多新的CSS特性提出,并且得到浏览器的支持。正如今天我们所聊的瀑布流布局,它是一个新特性,而且是个实验性特性。虽然说还没得到众多主流浏览器支持,但并不用过于担心,我想在不久的将来,这个特性就会得到更多的浏览器支持。当然,如果你在构建一个新的内部项目,那么可以尝试着使用这个新特性,也如文章中所阐述的,也可以基于这个新特性,用一点点JavaScript脚本让更多浏览器支持瀑布流布局效果。

另外,你也可以尝试着用用该布局特性,如果遇到问题,或者无法完成在以前的实现中能够完成的任务,可以向CSSWG提出你的问题。这样,我们才能更快的用上这个新特性。

返回顶部