导言
在项目中需要对大量图片进行展示时,瀑布流布局因其错落有致的特点是大部分人的最佳选择之一。
下面说一说,瀑布流布局的核心
瀑布流布局会先限制图片展示的宽高度,一般是对宽度加以限制,然后根据图片的原大小,设置对应比例的高宽度,使图片按比例缩小或放大展示,不至于别扭。
如下图宽度是一定
下图是高度一定
一般来说,我们会选择宽度一定来设置,设置宽度一定也有两种方式,
1:直接给定宽度,如200px,300px,500px等
2:给定每行排列的个数,根据浏览器或父元素的宽度,自动计算宽度
最后采用绝对定位,对每张图片位置进行计算与布局
完整实现代码
我这里实现是限制一行放置图片的数量进而设置宽度实现的瀑布流
<!-- ---------html部分------------ -->
<template>
<div id="waterfall" ref="waterfall">
<div v-for="(item, index) in showImages" class="waterfallItem" :key="index"
:style="{
width:item.width+'px',
height: item.height+'px',
}">
<img alt="" :data-src="item.url">
</div>
</div>
</template>
<!-- ---------js部分------------ -->
<script>
import { debounce } from '../network/debounce'
export default {
name: 'waterfall',
props: {
// 定义图片之间的间距
imgGrap:{
type:Number,
default:10
},
// 设置一行排列的图片数量
columns:{
type:Number,
default:5
},
// 传入的图片数组,每个子元素至少包含url(图片请求地址)
imgLists:{
type:Array,
default:[{
"url": "https://w.wallhaven.cc/full/6o/wallhaven-6oxem6.jpg",
},
{
"url": "https://w.wallhaven.cc/full/wq/wallhaven-wq9x27.jpg",
},
{
"url": "https://w.wallhaven.cc/full/wq/wallhaven-wq9lk7.jpg",
},
{
"url": "https://w.wallhaven.cc/full/wq/wallhaven-wq9ddx.jpg",
},
{
"url": "https://w.wallhaven.cc/full/e7/wallhaven-e7kelw.jpg",
},
{
"url": "https://w.wallhaven.cc/full/rd/wallhaven-rd88g1.jpg",
},
{
"url": "https://w.wallhaven.cc/full/m9/wallhaven-m97128.jpg",
},
{
"url": "https://w.wallhaven.cc/full/28/wallhaven-2873ey.jpg",
},
{
"url": "https://w.wallhaven.cc/full/8o/wallhaven-8oq17j.png",
},
{
"url": "https://w.wallhaven.cc/full/y8/wallhaven-y8dzpk.jpg",
},
{
"url": "https://w.wallhaven.cc/full/6o/wallhaven-6o8e6q.jpg",
},
{
"url": "https://w.wallhaven.cc/full/72/wallhaven-728rey.jpg",
},
{
"url": "https://w.wallhaven.cc/full/v9/wallhaven-v98gq3.jpg",
},
{
"url": "https://w.wallhaven.cc/full/3z/wallhaven-3zm3d3.jpg",
},
{
"url": "https://w.wallhaven.cc/full/28/wallhaven-28mj69.png",
},
{
"url": "https://w.wallhaven.cc/full/l3/wallhaven-l3lrqy.jpg",
},
{
"url": "https://w.wallhaven.cc/full/6o/wallhaven-6ox1gx.jpg",
},
{
"url": "https://w.wallhaven.cc/full/k7/wallhaven-k7yw91.jpg",
},
{
"url": "https://w.wallhaven.cc/full/k7/wallhaven-k7yxqm.jpg",
},
{
"url": "https://w.wallhaven.cc/full/j3/wallhaven-j329gq.jpg",
},
{
"url": "https://w.wallhaven.cc/full/g7/wallhaven-g7y3ol.jpg",
},
{
"url": "https://w.wallhaven.cc/full/m9/wallhaven-m9jve9.png",
},
{
"url": "https://w.wallhaven.cc/full/y8/wallhaven-y8d3pd.png",
},
{
"url": "https://w.wallhaven.cc/full/y8/wallhaven-y81go7.jpg",
},
{
"url": "https://w.wallhaven.cc/full/6o/wallhaven-6o81p7.png",
},
{
"url": "https://w.wallhaven.cc/full/6o/wallhaven-6oxjv6.jpg",
},
{
"url": "https://w.wallhaven.cc/full/o3/wallhaven-o3pr99.png",
},
{
"url": "https://w.wallhaven.cc/full/1k/wallhaven-1k6m6g.jpg",
},
{
"url": "https://w.wallhaven.cc/full/28/wallhaven-28mqog.jpg",
},
{
"url": "https://w.wallhaven.cc/full/pk/wallhaven-pk6l1m.jpg",
},
{
"url": "https://w.wallhaven.cc/full/rd/wallhaven-rd86vm.jpg",
},
{
"url": "https://w.wallhaven.cc/full/72/wallhaven-72zxk3.jpg",
},
{
"url": "https://w.wallhaven.cc/full/q2/wallhaven-q2x3w7.jpg",
},
{
"url": "https://w.wallhaven.cc/full/l3/wallhaven-l3ljq2.jpg",
},
{
"url": "https://w.wallhaven.cc/full/x8/wallhaven-x8rvjd.jpg",
},
{
"url": "https://w.wallhaven.cc/full/72/wallhaven-728rm3.jpg",
},
{
"url": "https://w.wallhaven.cc/full/o3/wallhaven-o3poe7.jpg",
},
{
"url": "https://w.wallhaven.cc/full/o3/wallhaven-o3ogml.jpg",
},
{
"url": "https://w.wallhaven.cc/full/wq/wallhaven-wq99px.jpg",
},
{
"url": "https://w.wallhaven.cc/full/q2/wallhaven-q278r7.jpg",
},
{
"url": "https://w.wallhaven.cc/full/dp/wallhaven-dprdmo.jpg",
},
{
"url": "https://w.wallhaven.cc/full/pk/wallhaven-pke5qj.png",
}]
}
},
data() {
return {
imgWidth: 0, //表示每个图片的宽度
columnHeights: [],//存储每列的最后放置完图片的位置
boxWidth: 0, //表示每个图片盒子的宽度,即包含图片宽度与边距
positionTop: 0, //每张图片放置的top位置
showImages:[], //预加载之后需要展示的图片
imgBoxs:null, //用于放置所有装有img的div盒子
observer:null, //观察函数,用于懒加载
debounceResize: null, //防抖函数,用于窗口改变之后,重新布局
}
},
methods: {
// 观察每张图片,出现在用户视野就开始图片加载
observerImages(images){
images.forEach(item=>{
this.observer.observe(item)
})
},
// IntersectionObserver内部的回调函数
callback(entries){
entries.forEach(entry=>{
if(entry.isIntersecting){ //判断此dom是否与浏览器的视口是否相交
const targetDiv=entry.target; //获取目标dom
const image=targetDiv.childNodes[0];
const data_src=image.getAttribute('data-src');//获取img中的data-src属性
image.setAttribute('src',data_src); //开始加载
this.observer.unobserve(targetDiv); //观察完毕之后就不再重复观察
}
})
},
//计算或寻找每列中高度最小的位置,用于设置下一张图片的top位置
getMinHeight() {
this.positionTop = Math.min(...this.columnHeights)
return this.columnHeights.indexOf(this.positionTop)
},
// 初始化基本数据
initData() {
this.imgWidth = this.$refs.waterfall.offsetWidth / this.columns - this.imgGrap;
this.boxWidth = this.imgWidth + this.imgGrap;
this.columnHeights = new Array(this.columns).fill(0);
},
// 预加载图片
preLoad(images) {
const imgs = []
images.forEach(item => {
imgs.push(this.loadImage(item.url)) ;
})
// Promise.all检查所有图片是否加载完成
return Promise.all(imgs)
},
//封装图片加载函数
loadImage(src) {
// 因为new的关键字,Promise中this指向改变,所有记录下vuecomponent,用于获取imgWidth
const that=this
return new Promise(function (resolve, reject) {
const img = new Image();
img.onerror=img.onload = () =>{//加载完成或失败执行resolve函数
resolve({
url:src,
width:that.imgWidth,
height:img.width==0?0:that.imgWidth*img.height/img.width,//避免除0情况
});
}
img.src = src;
})
},
//开始瀑布流布局
waterfallLayOut(){
for(let i=0;i<this.imgBoxs.length;i++){
const minHeightIndex = this.getMinHeight(); //获取所有列中高度最小的索引
this.imgBoxs[i].style.top= this.positionTop + 'px' //设置每个imgBox的相对定位位置
this.imgBoxs[i].style.left = minHeightIndex*this.boxWidth+'px'
this.columnHeights[minHeightIndex] += this.showImages[i].height + this.imgGrap; //更新相应列的高度
}
},
//当窗口改变,重新计算数据与布局
reLayout(){
const oldWidth=this.imgWidth; //获取之前每个图片的宽度
this.initData(); //重新初始化数据
if(this.imgWidth===oldWidth) return //如果宽度没有改变,就不必重新布局
for(let item of this.showImages){ //重新计算图片宽度与高度
item.height=this.imgWidth*item.height/item.width;
item.width=this.imgWidth;
}
this.waterfallLayOut() //计算完图片高度与宽度之后,再次布局
}
}
,
mounted() {
this.debounceResize = debounce(this.reLayout)
//初始化观察函数
this.observer=new IntersectionObserver(this.callback)
this.initData() //初始化化数据
// console.log(this.imgWidth)
//所有图片预加载完成,将结果更新至展示数组中
this.preLoad(this.imgLists).then(res=>{
this.showImages.push(...res)
this.$nextTick(()=>{
//获取所有装右图片的div盒子
this.imgBoxs=this.$el.getElementsByClassName('waterfallItem')
//依次观察每个div盒子,出现在用户视野处就加载图片
this.observerImages(Array.from(this.imgBoxs))
//预加载完图片后,就可以瀑布流布局
this.waterfallLayOut()
//监听浏览器窗口大小是否改变,用于重新布局
window.addEventListener('resize', this.debounceResize)
})
})
},
beforeDestroy(){
window.removeEventListener('resize',this.debounceResize)
}
}
</script>
<!-- ---------css部分------------ -->
<style scoped>
#waterfall {
width: 100%;
height: 100%;
position: relative;
}
.waterfallItem {
position: absolute;
border: 1px solid rgb(24, 24, 24);
animation: show-img .5s ease;
}
.waterfallItem img {
width: 100%;
height: 100%;
}
@keyframes show-img {
0% {
transform: scale(0.5);
}
100% {
transform: scale(1);
}
}
</style>
这里引入了一个防抖函数,您可以将下面代码放入methods中,调试一下
或者将下面代码放入到某个文件中,改变一下上图对应的路径
export function debounce(fn, delay = 500) {
let timer = null;
return function (...args) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
代码说明
代码中的懒加载是借助 IntersectionObserver 自行实现的,其实也可以借助vue中懒加载插件,添加v-lazy实现,不知道的小伙伴可以自行查阅资料,这里就不做使用介绍了
同时我个人感觉添加上懒加载没有太多必要,只是节约了一点的性能
注意
如果有小伙伴直接使用我给出的图片地址,需要耐心等待一下,因为地址请求是国外的图片资源,所以预加载时间较长