基于Vue3实现图片瀑布流排序
前言
今天来介绍一下关于图片瀑布流布局的实现,关于瀑布流,相信大家都很熟悉了:
如上图所示,就是瀑布流的布局了。
本文开发环境:windows10 + vue3 + vite + tailwindcss
实现思路
看过了效果,我们来聊聊如何实现。
首先,我们要确认一个排序的原则,瀑布流的第一行肯定是顺序排列的,之后从第二行开始,最先插入图片的是目前高度最小的列,再依次排序下去。
确认了这一点后,接下来就是创建一个瀑布流组件waterfall.vue,确认应该设置的参数:
1. 接收的props
- data
首先肯定是图片列表,其中保存图片地址 - nodeKey
唯一key,用于循环创建图片时指定key - columns
希望展示的图片列数 - picturePreLoad
图片是否预加载,当你的数据中不包括图片高度时,应该设置该项为true,这样可以等待图片加载后再渲染布局 - rowSpacing
行间距 - columnSpacing
列间距
const props = defineProps({
data: {
type: Array,
required: true
},
// 唯一标识key
nodeKey: {
type: String
},
columns: {
type: Number,
default: 3
},
picturePreLoad: {
type: Boolean,
default: true
},
rowSpacing: {
type: Number,
default: 20
},
colSpacing: {
type: Number,
default: 20
}
})
2. 组件样式
我这边是基于vue3+tailwindcss实现的,话说tailwindcss真的好用,不过即使不用这个问题也不大,因为样式很简单,我们希望图片是横向排列的,而且根据瀑布流的排列逻辑,应该很容易想到相对定位+绝对定位的方式。
<template>
<div class="relative m-auto bg-white" ref="containerRef" :style="{ height: containerHeight + 'px' }">
<template v-if="columnWidth && data.length > 0">
<div v-for="(item, index) in data" :key="nodeKey ? item[nodeKey] : index"
class=" m-waterfall-item absolute bg-white" :style="{
width: columnWidth + 'px',
left: item._style?.left + 'px',
top: item._style?.top + 'px'
}">
<slot :item="item" :width="columnWidth"></slot>
</div>
</template>
<template v-else>加载中</template>
</div>
</template>
3. 设置基本变量
从上面的视图代码中我们用到了几个变量
1、containerHeight:外部容器的高度;
2、columnWidth:每列的宽度;
3、item._style?.left:每个item与左边的距离;
3、item._style?.top:每个item与顶部的距离。
那么在代码中这样体现
// 总高度
const containerHeight = ref(0)
// 每一列的宽度
const columnWidth = ref(0)
// 容器实例
const containerRef = ref(null)
// 容器总宽度,设置它是为了计算每列的宽度
const containerWidth = ref(0)
// 容器与左边的距离,后续计算定位时使用
const containerLeft = ref(0)
4. 计算每列的宽度
根据上述的数据来计算容器总宽度,进而计算出每列的宽度
// 计算容器宽度以及左边距
const calcContainerWidth = () => {
const { paddingLeft, paddingRight } = getComputedStyle(containerRef.value)
containerLeft.value = parseFloat(paddingLeft)
containerWidth.value = containerRef.value.offsetWidth - parseFloat(paddingLeft) - parseFloat(paddingRight)
}
// 计算列宽
const calcColumnWidth = () => {
// 先计算容器宽度
calcContainerWidth()
// 每列宽度 = (容器宽度 - 所有列边距) / 4
columnWidth.value = (containerWidth.value - props.colSpacing * (props.columns - 1)) / props.columns
}
onMounted(() => {
// 计算列宽
useColumnWidth()
console.log(columnWidth.value)
})
5. 设置每列的高度
我们还需要记录每列的高度,最后容器的高度就是这些列中最大的那个高度
// 每列的高度,对象key:列数,value:高度
const columnHeightObj = ref({})
// 初始化每列高度
const initColumnHeightObj = () => {
for (let i = 0; i < props.columns; i++) {
columnHeightObj.value[i] = 0
}
// console.log('columnHeightObj.value', columnHeightObj.value);
}
6. 获取瀑布流各个item的高度
从上一步我们可以获取到每列的宽度,那么接下来就是获取瀑布流各个item的高度了,在这一块又需要分为俩种情况:
6.1 图片预加载
当picturePreLoad传入true时,需要等待图片预加载完成,我们再进行排列,那么,首先就得实现图片的预加载,这一块详见代码吧,主要就是利用Promise.all来实现的:
// 获取所有图片的高度
let itemHeights = []
/**
* 当需要等待图片加载时
*/
const waitImgComplete = () => {
// 每次重新计算item高度时,先清空之前的数据,以免影响
itemHeights = []
// 首先获取全部item的节点
const itemElments = [...document.getElementsByClassName('m-waterfall-item')]
// 获取所有图片的dom信息
const imgElements = getImgElements(itemElments)
// 获取图片列表
const imgList = getAllImgs(imgElements)
// console.log(imgList);
// 等待图片加载
onImgAllComplete(imgList).then(() => {
imgElements.forEach((item) => {
itemHeights.push(item.offsetHeight)
})
// 渲染item位置
useItemLocation()
})
}
// 关于处理图片预加载的代码可以单独放在utils.js文件中
// utils.js
/**
* 获取所有包含img的节点
*/
export function getImgElements(itemElements) {
const imgElements = []
itemElements.forEach((el) => {
imgElements.push(...el.getElementsByTagName('img'))
})
return imgElements
}
/**
* 从img节点中获取所有img
*/
export function getAllImgs(imgElements) {
const imgs = []
imgElements.forEach(el => {
imgs.push(el.src)
})
return imgs
}
/**
* 等待所有图片加载完成
*/
export function onImgAllComplete(allImgs) {
const promises = []
allImgs.forEach((img, index) => {
promises[index] = new Promise(resolve => {
const imgObj = new Image()
imgObj.src = img
imgObj.onload = () => {
resolve({
img,
index
})
}
})
})
return Promise.all(promises)
}
如此,我们就可以得到一个装有瀑布流各个元素的高度的数组了。
6.2 图片不预加载
这种就是不等待图片加载,直接开始渲染瀑布流,很显然,当第一次加载图片时,图片尚未加载完毕就开始渲染瀑布流,肯定会导致样式变形,当然,这种情况其实也很好解决,让后端传图片数据时带上图片高度就是了,但是这个问题先不进行处理,这篇文章主要还是针对第一种方式。
/**
* 图片不预加载,会有样式问题
*/
const useItemHeight = () => {
// 首先清空旧数据
itemHeights = []
// 首先获取全部item的节点
const itemElments = [...document.getElementsByClassName('m-waterfall-item')]
itemElments.forEach(el => {
itemHeights.push(el.offsetHeight)
})
// 渲染item位置
useItemLocation()
}
7. 计算各个item定位
7.1 首先定义一个渲染的方法
/**
* 渲染item的位置
* 1、根据瀑布流的排列,我们知道,优先将图片插入到高度最小的列,所以_style的left值就是先找到高度最小的列,然后获取其列数
* 2、根据列数及列间距计算出与左边的距离,而与顶部的距离其实就是当前列的最小高度;
* 3、设置完left值和top值后,要对当前最小的高度进行更新,就是加上刚刚插入的图片的高度,再进行接下来的对比
*/
const useItemLocation = () => {
props.data.forEach((item, index) => {
if (item._style) return
item._style = {}
// 计算与左边的距离
item._style.left = getLeft()
// 计算与顶部的巨鹿
item._style.top = getTop()
// 对当前最小高度的列进行高度的更新
increaseMinColumnHeight(index)
})
// 设置容器总高度
containerHeight.value = getMaxHeight(columnHeightObj.value)
}
7.2 计算各个item与容器左边的距离
从我们一开始得分析可知,当第一行按序排列后,从第二行开始,首先要找到高度最小的列,之后将图片插入该列,那么目的就很明确了,首先获取高度最小的列,然后根据获取到的列数、每列宽度以及列间距计算出与容器左边的距离。
/** 计算与左边的距离 */
const getLeft = () => {
let left = 0
// 首先获取高度最小的列的索引
const minHeightColumn = getMinHeightColumn(columnHeightObj.value)
// 与左边的距离 = (每列的宽度 + 列边距)* minHeightColumn
left = (columnWidth.value + props.colSpacing) * minHeightColumn + containerLeft.value
return left
}
// utils.js
/**
* 获取列中最小的高度
* @param {包含列高度的对象} columnHeightObj
*/
export const getMinHeight = (columnHeightObj) => {
const heights = Object.values(columnHeightObj)
return Math.min(...heights)
}
/**
* 获取当前最小高度的列数
*/
export const getMinHeightColumn = (columnHeightObj) => {
const minHeight = getMinHeight(columnHeightObj)
return Object.keys(columnHeightObj).find(key => columnHeightObj[key] === minHeight)
}
7.3 计算item和顶部的距离
当前item和顶部的距离其实就是最小的列的高度,这个应该很好理解,因为我们就是按照当前高度最小的列来排的。
const getTop = () => {
// 其实高度最小的列的高度
const minHeight = getMinHeight(columnHeightObj.value)
return minHeight
}
7.4 高度自增
当设置完当前item的left和top位置后,就需要更新当前列的高度了,其实就是加上item的高度以及行间距嘛
const increaseMinColumnHeight = (index) => {
// 首先获取高度最小的列的索引
const minHeightColumn = getMinHeightColumn(columnHeightObj.value)
columnHeightObj.value[minHeightColumn] += itemHeights[index] + props.rowSpacing
}
7.5 触发渲染
其实做完上面这些步骤,整个瀑布流的功能就基本完毕了,那么剩下的就是什么时候触发渲染方法,很容易想到哈,就是传入的data数据变化时,还要注意根据picturePreLoad的值来判断是否等待图片加载完成:
watch(() => props.data, (newVal) => {
// 重置数据源
const resetColumnHeight = newVal.every((item) => !item._style)
if (resetColumnHeight) {
// 构建高度记录容器
initColumnHeightObj()
}
nextTick(() => {
if (props.picturePreLoad) {
// 预加载
waitImgComplete()
} else {
// 不预加载
useItemHeight()
}
})
}, {
immediate: true,
deep: true
})
总结
综上所述,就是一个简单的瀑布流组件的开发思路了,我们主要要抓住几点:
- 瀑布流的排列规则:瀑布流的第一行肯定是顺序排列的,之后从第二行开始,最先插入图片的是目前高度最小的列,再依次排序下去;
- 根据你想怎样使用瀑布流组件来觉得传入怎么样的数据,本例中是希望用瀑布流组件包裹内部组件,所以传入的props就有data、nodeKey、colums、picturePreLoad等;
- 接下来就是确定容器布局以及计算各个item的位置,布局很简单,容器相对定位,内部item绝对定位,而关于计算,就是根据瀑布流的排列规则,优先排列高度最小的列,那么当前item的位置就容易确定了,left值就是高度最小列的列数 * (列宽+列间距)+外部容器的paddiingLeft,item的top值就是当前高度最小列的高度值;
- 当然,该组件还有待完善,就比如图片不预加载时的处理,同时还可以进行图片懒加载的处理等逻辑,这些以后有时间再写吧。
更多推荐
所有评论(0)