各种方法实现瀑布流
瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次按照规则放入指定位置。
相信大家在日常生活中看到很多次这种不规则瀑布流,我最近在给别人写网站的时候也遇到了这个需求,觉得解决问题的过程还是挺有意思的,所以就写下这篇文章。
比如 堆糖 网站
网格布局 grid 布局解决了 flex 布局下多个元素最后一排会出现的问题
如果你用 grid 布局想实现这个这个效果是不行的,网格布局顾名思义他看起来就是像一个一个的表格,尽管你的元素高度不同,但是他的每一行起始位置还是相同的。
- 首先放上我的 vue 3 代码中的 HTML 部分
<template><div id="bigBox"><div class="waterfallBox" v-for="(item, index) in 20"v-bind:style="{ backgroundColor: getBackgroundColor(), height: getHeight() }">{{ index }}</div></div>
</template>
- 其中用到了俩个方法也放下面了
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 的值来平均分剩下的空间作为 每个子元素所占用 的大小。
附上:
<script lang="ts" setup>
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 : posminHeight = Math.min(item, minHeight)})return pos
}// 3.自己手写
const init = () => {const waterfallList = document.querySelectorAll('.waterfallBox') as NodeListOf<HTMLElement>;minArray = new Array(columns).fill(0)// 计算除去gap,物品的宽度const bigBoxNode = <HTMLDivElement>document.querySelector('#bigBox')const bigBox = bigBoxNode.clientWidthconst 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()
})
</script>
最后,想说的就完了,为什么要通过元素选择器来实现(我希望有些问题,我们能够脱离框架也能解决这个问题,而不是脱离了框架就不知道该如何使用)
附上完整代码:
<template><div id="bigBox"><div class="waterfallBox" v-for="(item, index) in 10"v-bind:style="{ backgroundColor: getBackgroundColor(), height: getHeight() }">{{ index }}</div></div>
</template><script lang="ts" setup>
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 : posminHeight = Math.min(item, minHeight)})return pos
}// 3.自己手写
const init = () => {const waterfallList = document.querySelectorAll('.waterfallBox') as NodeListOf<HTMLElement>;minArray = new Array(columns).fill(0)// 计算除去gap,物品的宽度const bigBoxNode = <HTMLDivElement>document.querySelector('#bigBox')const bigBox = bigBoxNode.clientWidthconst 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()
})
</script><style lang="scss" scoped>
#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/
</style>