瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次按照规则放入指定位置。
相信大家在日常生活中看到很多次这种不规则瀑布流,我最近在给别人写网站的时候也遇到了这个需求,觉得解决问题的过程还是挺有意思的,所以就写下这篇文章。
比如 堆糖 网站
网格布局 grid 布局解决了 flex 布局下多个元素最后一排会出现的问题
如果你用 grid 布局想实现这个这个效果是不行的,网格布局顾名思义他看起来就是像一个一个的表格,尽管你的元素高度不同,但是他的每一行起始位置还是相同的。
首先放上我的 vue 3 代码中的 HTML 部分
v-bind:style="{ backgroundColor: getBackgroundColor(), height: getHeight() }">
{{ index }}
其中用到了俩个方法也放下面了
const getBackgroundColor = () => {
return `rgb(${Math.trunc(Math.random() * 255)},${Math.trunc(Math.random() * 255)},${Math.trunc(Math.random() * 255)})`
}
const getHeight = () => {
return Math.trunc(Math.random() * 300 + 50) + 'px'
}
初始css
#bigBox {
width: 100%;
max-width: 1200px;
margin: 20px auto;
background-color: cyan;
border: 4px dashed gray;
.waterfallBox {
display: flex;
justify-content: center;
align-items: center;
color: white;
font-weight: bold;
font-size: 20px;
}
}
纯CSS实现
flex 实现
以下代码是利用了 order 属性来控制显示的方位,但是这个方法非常不稳定。
在flex 布局中, order属性值来调整 flex 元素在 flex 容器中的排序顺序
order 属性也是我接触到这个瀑布流才知道的一个好东西,有时候我们做自适应布局时,有可能电脑端和手机端布局不一致,电脑端是图片左文字右,手机端可能就变成 文字上图片下,这个时候就可以巧借 order 属性,控制显示的顺序。
代码
#bigBox {
display:flex;
flex-direction: column;
flex-wrap: wrap;
// 需要提前计算高度,否则会出问题
height: 600px;
.waterfallBox{
position: relative;
margin-bottom: 20px;
width: 30%;
&::after{
position: absolute;
}
}
.waterfallBox:nth-child(3n+1) {
order: 1;
}
.waterfallBox:nth-child(3n+2) {
order: 2;
}
.waterfallBox:nth-child(3n) {
order: 3;
}
}
运气好的时候:
运气不好的时候
你会发现顺序全部乱套了/(ㄒoㄒ)/~~
利用 column-count
CSS column-count 属性是一个非常有用的CSS特性,它允许开发者将元素的内容分割成指定数量的列,从而创建多栏布局。这个属性接受一个正整数作为值,该值指定了元素内容应该被划分的理想列数。如果同时设置了 column-width 属性且其值不为 auto,则 column-count 仅表示允许的最大列数。
这个东西广泛使用于 报纸的布局,因为文字是需要这样现排版的
代码:(非常 nice 只有几行代码)
#bigBox{
column-count: 3;
column-gap: 20px;
.waterfallBox{
margin-bottom: 20px;
/* 防止多列布局,分页媒体和多区域上下文中的意外中断 */
break-inside: avoid;
}
}
结果 (但是你发现了吗,这个地方的排放是以列来排放的) 前面有说过,广泛用于报纸文字的排版,报纸里面的文字也是这样一列一列来的
因为他会计算,然后来分配每个列所占用的高度去填充盒子
这个方法适用于:不在意元素位置的排列
我一直在这俩个方法中徘徊,因为我想省事,能不写 js 的地方就绝对不写,查阅了很多资料,明白了 css 实现真正的瀑布流是不可能的,多多少少都会有问题,在真实的情况下,还是需要 js 来写会更好些,因为真实情况是 这些瀑布流一般都是动态加载的,而这个也需要 js 代码支撑
但是由于翻阅了很多资料的缘故——也逐渐知道了如何用 js 代码实现这个
但是由于我急于交付项目(本来想自己写,但是总是出问题,感觉可能还是心态没放平,因为这个问题已经困扰我好几天了)
于是我使用了别人写好的 js 代码,这个是真的香,程序员大佬中有句话:能使用别人已经写好的,就尽量不要自己写。
我使用的是,结合 grid 布局(因为我这边还需要做一个响应式设计)
https://masonry.desandro.com/
还有另外一个,没用过,但是感觉也不错
http://macyjs.com/
过了很久以后,才开始写了这个博客,写的时候就还是想挑战自己,于是还是手写了 js 代码,感觉没有那么难了,可能是心态放平了
实现思路:
设置最外层的 box 为 相对定位,里面的 所有子元素都使用 绝对定位 (子绝父相),然后通过计算每次都选取最小的高度,来作为 top 值,不断刷新这个 最小高度的数组,left 值可以一开始就计算好,通过 减去 gap 的值来平均分剩下的空间作为 每个子元素所占用 的大小。
附上:
import { onMounted } from "vue";
const getBackgroundColor = () => {
return `rgb(${Math.trunc(Math.random() * 255)},${Math.trunc(Math.random() * 255)},${Math.trunc(Math.random() * 255)})`
}
const getHeight = () => {
return Math.trunc(Math.random() * 300 + 50) + 'px'
}
const columns = 3;
const gap = 20;
let minArray: number[] = []
let maxHeight: number = 0
// 获取最小元素的下标
const getMinIndex = () => {
let minHeight = Number.MAX_SAFE_INTEGER;
let pos = 0;
minArray.forEach((item, index) => {
pos = item < minHeight ? index : pos
minHeight = Math.min(item, minHeight)
})
return pos
}
// 3.自己手写
const init = () => {
const waterfallList = document.querySelectorAll('.waterfallBox') as NodeListOf
minArray = new Array(columns).fill(0)
// 计算除去gap,物品的宽度
const bigBoxNode =
const bigBox = bigBoxNode.clientWidth
const boxWidth = Math.trunc((bigBox - ((minArray.length - 1) * gap)) / columns)
console.log(boxWidth)
const left = minArray.map((item, index) => {
return index * (boxWidth + gap)
})
console.log(left)
waterfallList.forEach((item, index) => {
// console.log(item.clientHeight, item.clientWidth)
// 获取高度
let currentPos = getMinIndex()
console.log(currentPos)
item.style.width = boxWidth + 'px'
item.style.left = left[currentPos] + 'px'
item.style.top = minArray[currentPos] + (minArray[currentPos] === 0 ? 0 : gap) + 'px'
minArray[currentPos] += (minArray[currentPos] === 0 ? 0 : gap) + item.clientHeight;
maxHeight = Math.max(...minArray)
})
bigBoxNode.style.height = maxHeight + gap + 'px';
}
window.addEventListener('resize', init)
onMounted(() => {
init()
})
最后,想说的就完了,为什么要通过元素选择器来实现(我希望有些问题,我们能够脱离框架也能解决这个问题,而不是脱离了框架就不知道该如何使用)
附上完整代码:
v-bind:style="{ backgroundColor: getBackgroundColor(), height: getHeight() }">
{{ index }}
import { onMounted } from "vue";
const getBackgroundColor = () => {
return `rgb(${Math.trunc(Math.random() * 255)},${Math.trunc(Math.random() * 255)},${Math.trunc(Math.random() * 255)})`
}
const getHeight = () => {
return Math.trunc(Math.random() * 300 + 50) + 'px'
}
const columns = 3;
const gap = 20;
let minArray: number[] = []
let maxHeight: number = 0
// 获取最小元素的下标
const getMinIndex = () => {
let minHeight = Number.MAX_SAFE_INTEGER;
let pos = 0;
minArray.forEach((item, index) => {
pos = item < minHeight ? index : pos
minHeight = Math.min(item, minHeight)
})
return pos
}
// 3.自己手写
const init = () => {
const waterfallList = document.querySelectorAll('.waterfallBox') as NodeListOf
minArray = new Array(columns).fill(0)
// 计算除去gap,物品的宽度
const bigBoxNode =
const bigBox = bigBoxNode.clientWidth
const boxWidth = Math.trunc((bigBox - ((minArray.length - 1) * gap)) / columns)
console.log(boxWidth)
const left = minArray.map((item, index) => {
return index * (boxWidth + gap)
})
console.log(left)
waterfallList.forEach((item, index) => {
// console.log(item.clientHeight, item.clientWidth)
// 获取高度
let currentPos = getMinIndex()
console.log(currentPos)
item.style.width = boxWidth + 'px'
item.style.left = left[currentPos] + 'px'
item.style.top = minArray[currentPos] + (minArray[currentPos] === 0 ? 0 : gap) + 'px'
minArray[currentPos] += (minArray[currentPos] === 0 ? 0 : gap) + item.clientHeight;
maxHeight = Math.max(...minArray)
})
bigBoxNode.style.height = maxHeight + gap + 'px';
}
window.addEventListener('resize', init)
onMounted(() => {
init()
})
#bigBox {
width: 100%;
max-width: 1200px;
margin: 20px auto;
background-color: cyan;
border: 4px dashed gray;
.waterfallBox {
display: flex;
justify-content: center;
align-items: center;
color: white;
font-weight: bold;
font-size: 20px;
}
}
// 1. 使用 flex 布局
// #bigBox {
// display:flex;
// flex-direction: column;
// flex-wrap: wrap;
// // 需要提前计算高度,否则会出问题
// height: 600px;
// .waterfallBox{
// position: relative;
// margin-bottom: 20px;
// width: 30%;
// &::after{
// position: absolute;
// }
// }
// .waterfallBox:nth-child(3n+1) {
// order: 1;
// }
// .waterfallBox:nth-child(3n+2) {
// order: 2;
// }
// .waterfallBox:nth-child(3n) {
// order: 3;
// }
// }
// 2.使用 column
#bigBox{
column-count: 3;
column-gap: 20px;
.waterfallBox{
margin-bottom: 20px;
/* 防止多列布局,分页媒体和多区域上下文中的意外中断 */
break-inside: avoid;
}
}
// 3.自己写
// #bigBox {
// position: relative;
// .waterfallBox {
// position: absolute;
// left: 0;
// top: 0;
// }
// }
// 4. 插件
// http://macyjs.com/
// https://masonry.desandro.com/