js + 绝对定位实现瀑布流照片墙

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值