js + 绝对定位实现瀑布流照片墙
瀑布流概念
瀑布流布局是错落式的布局方式。它有一个特点,整个布局以列为单位,下一个区块总是放在总高度最短的那一列的下面。
效果展示
技术栈
vue2 + element ui
原理
每个图片都设置为绝对定位,在每个图片加载好后,通过计算得到left,top,height
,并将其布局到页面中。
分析
首先需要分析一个卡片的组成,右边距 (图中红色部分),下边距(图中青色部分),以及时间显示区域,这对于计算非第一行的图片数据的left,top
至关重要。
计算每一列的宽度
因为列数固定,所以我们需要根据列数计算每列的宽度,那么我们就需要先拿到照片墙容器的宽度,因为是基于vue2,所以可以通过ref
拿到容器宽度。
let domWidth = this.$refs.photoWall.offsetWidth;
接下来就是计算了,由于我定义的列数是 3 列,右边距为 10,所以一共需要 4 个外边距。并且每个卡片还有 5 px的左右 padding ,3 列所以需要 30 px,所以只有减去这些额外的占用空间,剩下的才能平均分配给图片。
calculationWidth() {
let domWidth = this.$refs.photoWall.offsetWidth;
this.photoWidth = parseInt(
(domWidth - 30 - this.photoLeft * (this.waterFallPhotoCol + 1)) /
this.waterFallPhotoCol
);
},
记录每列的高度
因为瀑布流的特点就是下一张照片一定放在当前长度最短的那一列,什么意思呢,举个列子如下
假如这是当前 3 列的布局,那么下一张图片就应该放在中间这一列,放入后就需要更新中间一列所记录的长度。
所以我们首先要初始化一个数据用来记录每一列的长度。这里填充 5 的原因是我希望第一行卡片与容器上方保留 5 px的空隙。
//初始化偏移高度数组
this.deviationHeight = new Array(this.waterFallPhotoCol).fill(5);
计算每个卡片的left top height
这里需要根据宽度等比例的计算出图片高度。因为我在更新点前列的长度时吧下边距也加进去了,所以下一次再放图片时,top值就等于长度值。
imgPreloading() {
let len = this.carts.length;
let { photoWidth, photoLeft, deviationHeight, cartBottom } = this;
for (let i = 0; i < len; i++) {
let aImg = new Image();
aImg.src = this.carts[i].photoUrl;
aImg.onload = aImg.onerror = () => {
this.$set(
this.carts[i],
"photoHeight",
parseInt((this.photoWidth / aImg.width) * aImg.height)//等比例的计算图片高度
);
// 找到长度最短的那一列
let minIndex = deviationHeight.indexOf(
Math.min.apply(null, deviationHeight)
);
// 设置当前对象的top
this.$set(this.carts[i], "top", deviationHeight[minIndex]);
// 设置当前对象的left
this.$set(
this.carts[i],
"left",
minIndex == 0
? photoLeft
: minIndex * (photoLeft + photoWidth + 10) + photoLeft
);
// 更新当前最短列的长度,包含了下边距
deviationHeight[minIndex] += this.carts[i].photoHeight + cartBottom;
};
}
},
完整源码
<template>
<!-- 主体瀑布流区域,无限滚动 -->
<div class="photo-wall" ref="photoWall" infinite-scroll-distance="10">
<div
v-for="(cart, index) in carts"
:key="index"
:style="{
backgroundColor: specialcardBg,
top: cart.top + 'px',
left: cart.left + 'px',
}"
class="photo-wall-item"
>
<!-- 图片懒加载 -->
<el-image
:src="cart.photoUrl"
class="image"
:key="cart.photoUrl"
lazy
:style="{ width: photoWidth + 'px', height: cart.photoHeight + 'px' }"
>
<!-- 加载前占位 -->
<!-- <div slot="placeholder" class="image-slot">
<div
:style="{
height: cart.photoHeight + 'px',
width: photoWidth + 'px',
}">
<i class="el-icon-picture-outline"></i>
</div>
</div>-->
<!-- <div slot="error" class="image-slot">
<div
:style="{
height: cart.photoHeight + 'px',
width: photoWidth + 'px',
}"></div>
</div> -->
</el-image>
<div class="time">{{ cart.shootingTime }}</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "v-waterfall",
data() {
return {
//存放计算好的数据
carts: [],
//每一列的宽度
photoWidth: 0,
//多少列
waterFallPhotoCol: 3,
//左边距margin-left
photoLeft: 5,
//下边距margin-bottom + 卡片时间显示区域高度
cartBottom: 40,
//存放瀑布流各个列的高度
deviationHeight: [],
currentPage: 1,
//是否还有数据
noMore: false,
};
},
created() {
this.getPhotos();
},
computed: {
...mapGetters(["specialcardBg"]),
},
methods: {
async getPhotos() {
const res = await this.$api.getPhotos();
if (res.status == 200) {
this.carts = res.data;
}
this.calculationWidth();
},
//计算每个图片的宽度或者是列数
calculationWidth() {
let domWidth = this.$refs.photoWall.offsetWidth;
this.photoWidth = parseInt(
(domWidth - 30 - this.photoLeft * (this.waterFallPhotoCol + 1)) /
this.waterFallPhotoCol
);
//初始化偏移高度数组
this.deviationHeight = new Array(this.waterFallPhotoCol).fill(5);
this.imgPreloading();
},
//图片数据处理
imgPreloading() {
let len = this.carts.length;
let { photoWidth, photoLeft, deviationHeight, cartBottom } = this;
for (let i = 0; i < len; i++) {
let aImg = new Image();
aImg.src = this.carts[i].photoUrl;
aImg.onload = aImg.onerror = () => {
this.$set(
this.carts[i],
"photoHeight",
parseInt((this.photoWidth / aImg.width) * aImg.height)
);
let minIndex = deviationHeight.indexOf(
Math.min.apply(null, deviationHeight)
);
this.$set(this.carts[i], "top", deviationHeight[minIndex]);
this.$set(
this.carts[i],
"left",
minIndex == 0
? photoLeft
: minIndex * (photoLeft + photoWidth + 10) + photoLeft
);
deviationHeight[minIndex] += this.carts[i].photoHeight + cartBottom;
};
}
},
},
};
</script>
<style scoped lang="less">
.photo-wall {
overflow: scroll;
position: relative;
.photo-wall-item {
position: absolute;
padding: 5px;
box-sizing: border-box;
border-radius: 5px;
transition: 0.6s;
.time {
padding-right: 5px;
text-align: right;
font-size: 14px;
color: #999;
}
}
.photo-wall-item::after {
clear: both;
}
}
::-webkit-scrollbar {
/*滚动条整体样式*/
width: 0;
}
</style>
结语
到此瀑布流照片墙的功能就全部实现了,其实也没有很难,只要理解了原理就好了,其实也可用纯html css
实现瀑布流照片墙,但是达不到最完美的效果即无法将图片放到长度最短的那一列。如果想了解完整的项目可以去看看哦,已开源MyBlog。