今天我们来讲一讲css中一种很实用的布局方式——瀑布流布局。
什么是瀑布流布局呢?我们可以随便打开一个购物网站或社交网站,它们的布局一般都是瀑布流布局,比如小红书:
你会发现它的布局是两列排列,但其中的图片高度都不一样,使得每一块内容的布局都错落有致,并不是每一行的内容高度都相同,也就是并不是排列整齐的。
为什么要这样设计呢?
这其实是有心理学暗示的。有人统计过,如果页面布局是整齐划一、两边对称的话,用户很容易刷着刷着就感觉很疲劳;而页面要是高度不一、错落有致排列的话,用户投入的时间更多。因为当用户刷内容的时候,可能此时已经疲劳了,但看到右下角露出了半张图片挺有意思,手指不自觉地往下划了划,然后左下角又露出了半张图片,又觉得挺有意思,又划了划。这样,用户在不知不觉中就将时间留给了我们的产品。
这就是瀑布流布局的好处。
瀑布流
优点:
- 用户体验不被打断
- 空间合理利用
适用场景
- 以图片为主的页面
- 用户的目的性不强
所以我们一起来学一下如何实现瀑布流布局。
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>
此时页面没有经过布局长这样,合情合理:
那我们想让它变成瀑布流布局,就可以用到多栏布局了。
我们是把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列来展示,从第一列按顺序从上往下排,放不下的就从第二列开始排。
所以效果就会是这样:
这样是不是就是我们想要的瀑布流布局,但这样还有个小问题:
你看第一列最后一个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,不让它自适应,它就不会断开了。
这样不管我们怎么调整屏幕的宽度,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>
效果如下:
这就是能实现瀑布流布局的一种布局方式,多栏布局。
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的背景颜色。所以现在页面长这样:
我们来看看给它设置成网格布局会是什么样子:
<style>
.masonry {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
</style>
我们将父容器masonry的display设置为grid,grid-template-columns
能设置成几列排放,我们设置成3列。于是页面现在长这样:
此时页面就是网格布局了,并且我们设置成了3列排放,并且元素是从左到右、从上往下排列的。
当然我们还可以分成3行,grid-template-rows
设置成几行分布。
<style>
.masonry {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 1fr 1fr 1fr;
}
</style>
你看我们用浏览器检查一下masonry容器,看虚线,是一个标准的九宫格。
我们还可以设置间距,用grid-gap,我们设置成10px。
<style>
.masonry {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 1fr 1fr 1fr;
grid-gap: 10px;
}
</style>
这个间距是网格的间距,行和列都会产生间距。如果我们只希望有列间距就用column-gap。
<style>
.masonry {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 1fr 1fr 1fr;
column-gap: 10px;
}
</style>
网格布局还有些有意思的属性。我们还可以设置子元素上边框的起始位置,如:
<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,它的上边框就会对齐第二行,它就会变成这样:
第一个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上边框在第一行,下边框在第三行,它就会变成这样:
它就会占据第一行到第三行之间的内容。
当然还可以设置左边框和右边框所在的位置:
<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>
我们设置第一个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个网格:
你看,第一个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设置为应该跨越的网格数。
这样我们就实现了瀑布流布局了,每张图片都占据了它该有的网格数。
页面效果就是瀑布流了。
这就是利用网格布局来实现瀑布流布局的效果。
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>
弹性布局的写法会更简单更好理解一点。
效果还是这样。
这样我们就实现了弹性布局实现瀑布流的效果。
4. 总结
本次我们一起学了三种能实现瀑布流布局的方法,多栏布局、网格布局和弹性布局。你学废了嘛。
如果对你有帮助的话不妨点个赞吧!