瀑布流的原理以及实现

1,556 阅读2分钟

前言

浏览别人的网站,看到瀑布流的这种效果很好看,于是就想着自己实现一个,刚开始去写的时候发现其实还是有点难度的,搜索参考了一下别人的理解加自己的改造,自己也写了一个响应式的瀑布流

分析

瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。

xxx.png

  • 1、首先瀑布流所有的图片应该保持宽度一致,高度是由内容决定。 xxx.png

左浮动的话,我们可以看到第6个盒子直接就在第4个盒子旁边停下了,因为第4个高度最高,挡住了它左浮动的去路。第6个盒子是第2行的最后一个,所以第7个盒子只能在第3行排列了。当排到第12个盒子的时候,盒子会以第11个盒子的位置为基础左浮动(这就是第12个盒子为什么没有‘跳到’第9个盒子下面的原因),碰到第8个盒子后又被挡住了。

xxx.png

通过定位的方式是我们实现瀑布流的最基本的原理,只要我们动态的设置它的top值、left值,就能让它排列。

  • 2、定位后确定浏览器显示区域内,一行能放多少列图片盒子。

    • 获取页面的宽度
    • 获取图片盒子的宽度
    • 显示的列数 = 页面宽度/图片盒子宽度

计算出来列数,然后根据列数再反过来平分剩下的宽度,然后再原有的宽度基础上加上剩下的平分宽度就是盒子的总宽度,这样就平分了屏幕的宽度,做到自适应

  • 3、为了美观我们也可以加上一个空隙
  • 显示的列数 = 页面宽度/(图片盒子宽度+间隙);

图片之间也可也增加一个右边距,这样图片和图片之间就有间隙了,增加间隙计算相对定位的时候别忘记计算进去

xxx.png

  • 4、计算完列数和宽度之后,就开始填充数据了
    • 每一列当成一个数组,用来存储每一列当前的高度之和,
    • 然后循环遍历服务端请求来的数据,push到展示的数据里面
    • 计算每一次push进去的图片当前的top值和left值

全部代码

<template>
  <div class="v-waterfall-content">
    <div
      class="v-waterfall-item"
      v-for="(item, index) in waterfallList"
      :key="index"
      :style="{
        top: item.top + 'px',
        left: item.left + 'px',
        width: waterfallImgWidth + 'px',
        height: item.height + 'px',
      }"
    >
      <img :src="item.src" alt="" />
    </div>
  </div>
</template>

<script>
import { debaunce } from '../utils'
import imgArr from '../api/imgArr.js'
export default {
  name: "v-waterfall",
  data() {
    return {
      waterfallImgWidth: 250,//每一列的宽度
      waterfallImgCol: 0,//多少列
      waterfallImgRight: 10,//右边距
      waterfallImgBottom: 10,//下边距
      imgArr,//原始数据
      waterfallList: [],//存放计算好的数据
      waterfallDeviationHeight: [],//存放瀑布流各个列的高度
    }
  },
  mounted() {
    this.waterFall()
    // 窗口尺寸变化事件
    window.addEventListener("resize", debaunce(() => {
      this.waterfallImgWidth = 250
      this.waterfallList = []
      this.waterFall()
    }, 1000)),
      // 窗口滚动事件
      window.addEventListener("scroll", debaunce(() => {
        this.onScroll()
      }, 1000))
  },
  methods: {
    //动态计算多少列,每列的宽度
    waterFall() {
      let pageWidth = this.getClient().width - 10;//可以减去一个滚动条的宽度
      this.waterfallImgCol = parseInt(pageWidth / (this.waterfallImgWidth + this.waterfallImgRight));
      this.waterfallImgWidth = parseInt(pageWidth % (this.waterfallImgWidth + this.waterfallImgRight) / this.waterfallImgCol) + this.waterfallImgWidth
      //初始化偏移高度数组
      this.waterfallDeviationHeight = new Array(this.waterfallImgCol);
      for (let i = 0; i < this.waterfallDeviationHeight.length; i++) {
        this.waterfallDeviationHeight[i] = 0;
      }
      this.imgPreloading()


    },
    //图片预加载
    async imgPreloading() {
      for (let i = 0; i < this.imgArr.length; i++) {
        await new Promise((resolve) => {
          const aImg = new Image();
          aImg.src = this.imgArr[i]
          aImg.onload = () => {
            const imgData = {}
            //根据设定的列宽度求出图片的高度
            imgData.height = this.waterfallImgWidth / aImg.width * aImg.height
            imgData.src = this.imgArr[i]
            this.waterfallList.push(imgData)
            //调用图片位置计算方法
            this.rankImg(imgData);
            resolve()
          }
        })
      }
    },
    //计算图片偏移量
    rankImg(imgData) {
      let { waterfallImgWidth, waterfallImgRight, waterfallImgBottom, waterfallDeviationHeight } = this;
      //找出当前最短列的索引
      let minIndex = this.waterfallDeviationHeight.indexOf(Math.min.apply(null, this.waterfallDeviationHeight))
      //获取最短列底部高度,既下一张图片的顶部高度
      imgData.top = waterfallDeviationHeight[minIndex];
      //计算左侧偏移,最短列索引*(右边距+列宽度)
      imgData.left = minIndex * (waterfallImgRight + waterfallImgWidth);
      //改变当前列高度
      waterfallDeviationHeight[minIndex] += imgData.height + waterfallImgBottom;

    },
    // clientWidth 处理兼容性
    getClient() {
      return {
        width: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth,
        height: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
      }
    },
    // 触底事件
    onScroll() {
      // 滚动高度(document.documentElement.scrollTop)
      // 可视区域/屏幕高度(document.documentElement.clientHeight)
      // 页面高度(document.documentElement.scrollHeight)
      //If(滚动高度 + 可视区域  >= 页面高度){ do something函数}  
      // 是否滚动到底部
      const IS_BOTTOM = document.documentElement.scrollTop || document.body.scrollTop + document.documentElement.clientHeight >= document.documentElement.scrollHeight - 100
      if (IS_BOTTOM) {
        console.log('到底了')
        this.imgArr = imgArr.slice(Math.round(Math.random() * 10), Math.round(Math.random() * 10) + 20)
        this.imgPreloading()
      }
    }
  },
  destroyed() {
    // 离开页面取消监听
    window.removeEventListener('scroll', this.onScroll, false)
    window.removeEventListener('resize', this.waterFall, false)
  }
}
</script>
<style >
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.v-waterfall-content {
  position: relative;
}
.v-waterfall-item {
  position: absolute;
}
.v-waterfall-item img {
  width: 100%;
}
</style>

源码