如此吸引用户的瀑布流布局是怎么写的?

4,765 阅读11分钟

今天我们来讲一讲css中一种很实用的布局方式——瀑布流布局。

什么是瀑布流布局呢?我们可以随便打开一个购物网站或社交网站,它们的布局一般都是瀑布流布局,比如小红书:

PixPin_2024-12-21_13-32-48.gif

你会发现它的布局是两列排列,但其中的图片高度都不一样,使得每一块内容的布局都错落有致,并不是每一行的内容高度都相同,也就是并不是排列整齐的。

为什么要这样设计呢?

这其实是有心理学暗示的。有人统计过,如果页面布局是整齐划一、两边对称的话,用户很容易刷着刷着就感觉很疲劳;而页面要是高度不一、错落有致排列的话,用户投入的时间更多。因为当用户刷内容的时候,可能此时已经疲劳了,但看到右下角露出了半张图片挺有意思,手指不自觉地往下划了划,然后左下角又露出了半张图片,又觉得挺有意思,又划了划。这样,用户在不知不觉中就将时间留给了我们的产品。

这就是瀑布流布局的好处。

瀑布流

优点:

  1. 用户体验不被打断
  2. 空间合理利用

适用场景

  1. 以图片为主的页面
  2. 用户的目的性不强

所以我们一起来学一下如何实现瀑布流布局。

1. mulit-column 多栏布局

我们先来学习一下第一种能实现瀑布流效果的布局——多栏布局。

我们提前准备一份数据在js文件中,然后拿到页面上来展示,我们使用了vue来辅助开发。

就是一个很简单的v-for循环遍历我们准备的数组展示到页面中,然后随便写了一点样式。

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script src="./data.js"></script>
    <style>
        .masonry {
            padding: 10px;
        }

        .item {
            border: 1px solid #999;
            margin-bottom: 10px;
        }

        img {
            width: 100%;
        }

        span {
            display: block;
            margin-left: 5px;
        }
    </style>
</head>

<body>
    <div id="app">
        <div class="masonry">
            <div class="item" v-for="item in listData">
                <img :src="item.img" alt="">
                <span>{{item.value}}</span>
            </div>
        </div>
    </div>

    <script>
        const { createApp, ref } = Vue

        createApp({
            setup() {
                const listData = ref(data)
                return {
                    listData
                }
            }
        }).mount('#app')
    </script>
</body>

</html>

此时页面没有经过布局长这样,合情合理:

PixPin_2024-12-21_16-37-26.gif

那我们想让它变成瀑布流布局,就可以用到多栏布局了。

我们是把item作为了页面中的每一项,item里面有一张图片,一段文字描述。然后item是在masonry容器里的,masonry容器就为item的父容器。我们可以给masonry容器加上这样一个属性。

<style>
        .masonry {
            column-count: 3;
            padding: 10px;
        }

        .item {
            border: 1px solid #999;
            margin-bottom: 10px;
        }

        img {
            width: 100%;
        }

        span {
            display: block;
            margin-left: 5px;
        }
    </style>

column-count,可以将内部的子元素分成列来展示。我们写成了3,masonry内部的子元素就会平均的分成3列来展示,从第一列按顺序从上往下排,放不下的就从第二列开始排。

所以效果就会是这样:

PixPin_2024-12-21_18-43-43.gif

这样是不是就是我们想要的瀑布流布局,但这样还有个小问题:

PixPin_2024-12-21_18-45-00.png

你看第一列最后一个item中的文字跑到第二列的开头去了,它第一列放不下的元素不管是不是一个整体直接排到第二列去了,这并不是我们想要的效果。所以我们要给item加一个属性:break-inside。它是设置元素能不能自适应。

<style>
        .masonry {
            column-count: 3;
            padding: 10px;
        }

        .item {
            border: 1px solid #999;
            margin-bottom: 10px;
            break-inside: avoid;
        }

        img {
            width: 100%;
        }

        span {
            display: block;
            margin-left: 5px;
        }
    </style>

break-inside默认值就为auto,我们设置为avoid,不让它自适应,它就不会断开了。

PixPin_2024-12-21_18-56-27.gif

这样不管我们怎么调整屏幕的宽度,item中的元素都不会断开。这样就搞定了。

多栏布局还有个能调整列间距的属性,column-gap。我们还可以设一下列间距。

<style>
        .masonry {
            column-count: 3;
            padding: 10px;
            column-gap: 10px;
        }

        .item {
            border: 1px solid #999;
            margin-bottom: 10px;
            break-inside: avoid;
        }

        img {
            width: 100%;
        }

        span {
            display: block;
            margin-left: 5px;
        }
    </style>

效果如下:

PixPin_2024-12-21_19-00-11.gif

这就是能实现瀑布流布局的一种布局方式,多栏布局。

2. grid布局 网格布局

我们再来聊一聊第二种能实现瀑布流布局的布局方式,grid布局,也叫网格布局。

网格布局是css中最强大的布局方式,它可以将页面划分成任意网格,随意去搭配。

所以我们先来看看网格布局有些什么特性。

<body>
    <div id="app">
        <div class="masonry">
            <div class="item" v-for="n in 9" :style="getStyle(n)">{{n}}</div>
        </div>
    </div>

    <script>
        const { createApp, ref } = Vue

        createApp({
            setup() {
                const heights = [150, 160, 170, 180, 140, 155, 165, 180, 175]
                const colors = [
                    "#ef3429",
                    "#f68f25",
                    "#4ba846",
                    "#0476c2",
                    "#c077af",
                    "#f8d29d",
                    "#b4a87f",
                    "#d0e4a8",
                    "#4dc7ec"
                ]
                const getStyle = (index) => {
                    return {
                        height: heights[index - 1] + "px",
                        backgroundColor: colors[index - 1]
                    }
                }
                return {
                    heights,
                    colors,
                    getStyle
                }
            }
        }).mount('#app')
    </script>
</body>

</html>

我在masonry循环遍历了几个item出来,heights代表每个item的高度,colors代表每个item的背景颜色。所以现在页面长这样:

PixPin_2024-12-21_19-46-44.png

我们来看看给它设置成网格布局会是什么样子:

<style>
        .masonry {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
        }
    </style>

我们将父容器masonry的display设置为grid,grid-template-columns能设置成几列排放,我们设置成3列。于是页面现在长这样:

PixPin_2024-12-21_19-51-48.png

此时页面就是网格布局了,并且我们设置成了3列排放,并且元素是从左到右、从上往下排列的。

当然我们还可以分成3行,grid-template-rows设置成几行分布。

<style>
        .masonry {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            grid-template-rows: 1fr 1fr 1fr;
        }
    </style>

PixPin_2024-12-21_19-56-00.png

你看我们用浏览器检查一下masonry容器,看虚线,是一个标准的九宫格。

我们还可以设置间距,用grid-gap,我们设置成10px。

<style>
        .masonry {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            grid-template-rows: 1fr 1fr 1fr;
            grid-gap: 10px;
        }
    </style>

PixPin_2024-12-21_19-58-35.png

这个间距是网格的间距,行和列都会产生间距。如果我们只希望有列间距就用column-gap。

<style>
        .masonry {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            grid-template-rows: 1fr 1fr 1fr;
            column-gap: 10px;
        }
    </style>

PixPin_2024-12-21_20-00-24.png

网格布局还有些有意思的属性。我们还可以设置子元素上边框的起始位置,如:

<style>
        .masonry {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            grid-template-rows: 1fr 1fr 1fr;
            column-gap: 10px;
        }
        
        .item:first-child {
            grid-row-start: 2;
        }
    </style>

我选中第一个item,设置它上边框的起始位置为2,它的上边框就会对齐第二行,它就会变成这样:

PixPin_2024-12-21_20-09-47.png

第一个item的上边框就会对齐网格的第二行。

我们还可以设置它下边框所在的位置:

<style>
        .masonry {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            grid-template-rows: 1fr 1fr 1fr;
            column-gap: 10px;
        }
        
        .item:first-child {
            grid-row-start: 1;
            grid-row-end: 3;
        }
    </style>

我们设置第一个item上边框在第一行,下边框在第三行,它就会变成这样:

PixPin_2024-12-21_20-14-47.png

它就会占据第一行到第三行之间的内容。

当然还可以设置左边框和右边框所在的位置:

<style>
        .masonry {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            grid-template-rows: 1fr 1fr 1fr;
            column-gap: 10px;
        }
        
        .item:first-child {
            grid-column-start: 1;
            grid-column-end: 3;
        }
    </style>

PixPin_2024-12-21_20-18-01.png

我们设置第一个item左边框在第一列,右边框在第三列,它就会占据第一列到第三轮之间的内容,因为我们只设置了高度没有设置宽度,所以它就会变成上图这样。

我们还可以这样写:

<style>
        .masonry {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            grid-template-rows: 1fr 1fr 1fr;
            column-gap: 10px;
        }
        
        .item:first-child {
            grid-row-start: auto;
            grid-row-end: span 3;
        }
    </style>

用span,它可以设置下边框跨越多少个网格,我们设置成了3,下边框就会跨越3个网格:

PixPin_2024-12-21_20-36-11.png

你看,第一个item是不是就占据了3个网格。

好,了解完网格布局的一些属性,我们就可以用它来实现瀑布流布局了。我们可以用网格布局将item设置为3列,然后用js获取到每一张图片的高度去计算每一个item所占的网格就能实现瀑布流布局了。

还是我们在多栏布局中用到的例子,这次我们就只放图片:

<body>
    <div id="app">
        <div class="masonry">
            <img v-for="item in images" :src="item.img" alt="">
        </div>
    </div>

    <script>
        const { createApp, ref } = Vue
        createApp({
            setup() {
                const images = ref(data)
                return {
                    images,
                }
            }
        }).mount('#app')
    </script>
</body>

现在图片已经被循环遍历展示到页面上了。我们将父容器masonry设置为网格布局,让它分3列展示。

<style>
        .masonry {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            column-gap: 5px;
            grid-auto-rows: auto;
        }

        img {
            width: 100%;
            height: 100%;
        }
    </style>

做完了准备工作,接下来,我们就得去获取每张图片的高度去计算它应该占据多少网格。

那我们得先获取到img的dom结构,vue提供了一种方法给我们获取dom结构。我们想要获取img标签,直接在它身上打一个ref标记,然后我们就可以在js中获取这个标记。

<body>
    <div id="app">
        <div class="masonry">
            <img ref="imgRefs" v-for="item in images" :src="item.img" alt="">
        </div>
    </div>

    <script>
        const { createApp, ref } = Vue
        createApp({
            setup() {
                const images = ref(data)
                const imgRefs = ref(null)
                
                return {
                    images,
                    imgRefs
                }
            }
        }).mount('#app')
    </script>
</body>

我们先将imgRefs设置为null,再将它抛出。这个imgRefs就帮我们获取到了页面上50个img标签。

那我们就去读img的高度。在这里我们要写一个生命周期函数onMounted,我们在onMounted的回调函数去读取高度。这个生命周期函数只有在页面渲染完毕后才会执行,避免我们获取到空的dom结构。

<body>
    <div id="app">
        <div class="masonry">
            <img ref="imgRefs" v-for="item in images" :src="item.img" alt="">
        </div>
    </div>

    <script>
        const { createApp, ref, onMounted } = Vue
        createApp({
            setup() {
                const images = ref(data)
                const imgRefs = ref(null)
                
                onMounted(() => {  // 在页面渲染完成后执行
                    imgRefs.value.forEach((item) => {
                        let src = item.getAttribute("src")
                        let image = new Image()  // 创建一个新的图片对象
                        image.src = src // 给图片对象的src属性赋值浏览器就会加载图片
                        let width = item.width
                    })
                })
                
                return {
                    images,
                    imgRefs
                }
            }
        }).mount('#app')
    </script>
</body>

然后我们在生命周期函数内先获取item的属性src,再创建一个新的图片对象,然后给图片对象的src属性赋值浏览器就会加载图片。

然后我们再写当新的图片加载完成后我们再去计算图片的高,得到它应该跨越几个网格。

<body>
    <div id="app">
        <div class="masonry">
            <img ref="imgRefs" v-for="item in images" :src="item.img" alt="">
        </div>
    </div>

    <script>
        const { createApp, ref, onMounted } = Vue
        createApp({
            setup() {
                const images = ref(data)
                const imgRefs = ref(null)
                
                onMounted(() => {  // 在页面渲染完成后执行
                    imgRefs.value.forEach((item) => {
                        let src = item.getAttribute("src")
                        let image = new Image()  // 创建一个新的图片对象
                        image.src = src // 给图片对象的src属性赋值浏览器就会加载图片
                        let width = item.width
                        // 防止页面上的img标签还没有渲染完成就执行下面的代码
                        image.onload = () => {
                            let w = image.width
                            let h = image.height
                            let height = Math.round(width * h / w)
                            // Math.round四舍五入
                            item.src = src
                            item.style.gridRowEnd = `span ${~~(height / 10)}`
                            // ~~ 向下取整
                        }
                    })
                })
                
                return {
                    images,
                    imgRefs
                }
            }
        }).mount('#app')
    </script>
</body>

我们写一个onload,当图片加载完成了我们再去计算高度。然后就将item.style.gridRowEnd设置为应该跨越的网格数。

这样我们就实现了瀑布流布局了,每张图片都占据了它该有的网格数。

PixPin_2024-12-21_21-26-19.gif

页面效果就是瀑布流了。

这就是利用网格布局来实现瀑布流布局的效果。

3. flex 布局

我们再来讲一下第三种能实现瀑布流布局的布局方式,flex布局,也叫弹性布局。

用弹性布局应该怎么写呢?

我们知道,当我们给父容器设置为了弹性容器,里面的子元素就会去到同一行。假如子元素有3个,我们再给子元素设置为flex: 1,父容器就会平均分为3等份,每一个子元素占一份。

所以我们应该这样写,我们人为的准备3列,然后将我们准备好的图片分为3列去展示到每一列。

<body>
    <div id="app">
        <div class="masonry">
            <div class="colmun">
                <img class="item" v-for="item in data1" :src="item.img" alt="">
            </div>
            <div class="colmun">
                <img class="item" v-for="item in data2" :src="item.img" alt="">
            </div>
            <div class="colmun">
                <img class="item" v-for="item in data3" :src="item.img" alt="">
            </div>
        </div>
    </div>

    <script>
        const { createApp, ref } = Vue

        createApp({
            setup() {
                const images = ref(data)
                const data1 = ref([])
                const data2 = ref([])
                const data3 = ref([])
                let i = 0
                while (i < data.length) {
                    data1.value.push(data[i++])
                    if (i < data.length) {
                        data2.value.push(data[i++])
                    }
                    if (i < data.length) {
                        data3.value.push(data[i++])
                    }
                }
                return {
                    data1,
                    data2,
                    data3
                }
            }
        }).mount('#app')

我们去循环遍历这个data数组,将第一个元素存放到data1中,将第二个元素存放到data2中,将第三个元素存放到data3中,然后第四个又放到data1中。如此循环下去,当循环结束后,data数组就会被平均的分为3等份。

然后我们再拿着data1展示到第一列,data2展示到第二列,data3展示到第三列。这样图片就会被分为3列展示。

我们再将父容器masonry设置为弹性容器,子容器colmun的flex设置为1,这样每一列就会平均占据父容器一等份。还要将子容器colmun设置为弹性容器,并改变它主轴的方向,为y轴,这样每一列中的元素就会在colmun中垂直弹性布局,就不会跑到同一行去。在这样就能实现一个瀑布流布局了。

<style>
        .masonry {
            display: flex;
        }

        .colmun {
            display: flex;
            flex-direction: column;
            flex: 1;
            padding: 0 2px;
        }

        .item {
            margin-bottom: 5px;
            width: 100%;
        }
    </style>

弹性布局的写法会更简单更好理解一点。

效果还是这样。

PixPin_2024-12-21_22-02-15.png

这样我们就实现了弹性布局实现瀑布流的效果。

4. 总结

本次我们一起学了三种能实现瀑布流布局的方法,多栏布局、网格布局和弹性布局。你学废了嘛。

如果对你有帮助的话不妨点个赞吧!