如何用绝对定位实现瀑布流

@温叶博客  August 7, 2023

前言

瀑布流是一种布局方式,实现瀑布流有很多种方法,例如flex,grid布局,但或多或少有这样那样的问题。所以这里介绍的是最传统也是兼容性最好的方法,绝对定位布局。

实现思路

首先我们来看看瀑布流长啥样

瀑布流

首先基于瀑布流的特点,我们很容易就能定义出这几个变量

data() {
  return {
    containerWidth: 1230, // 容器的长度
    colCount: 4, // 每列的个数
    colGap: 10, // 每列的空隙
  };
}

然后为了区分元素,这里给元素进行编号(从1开始),那么行编号和列编号也可以计算得出

data() {
  return {
    num: 1,
    // 省略...
  }
},

// 编号
const num  = this.num++;
// 当前行编号: 从1开始
const rowNum = Math.ceil(num / this.colCount);
// 当前列编号: 从1开始
const colNum = num % this.colCount || this.colCount;

瀑布流的特点就是先排满一行,但是因为元素有高有矮,所以底部显得参差不齐。在开启下一行的渲染时候,元素应该在当前高度中最短的列中进行生成,如上图所示,编号5的元素排在编号1的下面,因为当前编号1所在的列最矮,这个应该很容易理解。

那这个是怎么实现的呢,其实很简单,就是用一个数组记录当前列的高度,下次渲染元素就渲染在最矮列下面,最高列的高度则是瀑布流的整体高度。

created() {
  // 初始化第0列的高度
  this.currheights = new Array(this.colCount + 1).fill(0);
},
data() {
  return {
    // 省略...
    currheights: [] // 缓存当前每列的高度
  };
},
computed: {
  // 当前列的最大高度
  maxCurrHeight() {
    return Math.max(...this.currheights);
  },
  // 当前最矮列的编号
  minShortColNum() {
    let num = 1;
    for (let i = 2; i <= this.currheights.length; i++) {
      if (this.currheights[num] > this.currheights[i]) {
        num = i;
      }
    }
    return num;
  }
}

对于渲染一个元素来说,我们需要知道的无非是width, height, top, left,因为这里介绍的是不定高度的做法,所以height是渲染完元素才能知道,这也就导致实际一个列表,我们只能一个个渲染,即拿到上一个高度,根据当前列高度再渲染下一个元素的位置。

容器长度,每列个数和间距大小都是确定的,那么width自然也是确定的,width = (容器长度 - (每列个数 - 1) * 间距) / 每列个数,这个很好理解;难点在于top和left怎么计算呢?

left其实就是当前最矮列距离左边的距离,因为我们优先填满最矮的那列嘛

const left = (this.minShortColNum - 1) * (width + this.colGap);

top则需要分情况讨论,如果是第一行则为0,否则就是当前列(最矮列)的高度 + 边距。

const currColHeihgt = rowNum === 1 ? 0 : this.currheights[this.minShortColNum];
const gap = rowNum === 1 ? 0 : this.colGap;
const top = currColHeihgt + gap;

完成例子

这里给出上面瀑布流截图的具体实现例子,在线访问

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
    />
    <title>瀑布流-定位实现</title>
    <style>
      .m-list {
        position: relative;
        background-color: #ccc;
      }
      .m-item {
        position: absolute;
        background-color: darkcyan;
        display: inline-flex;
        justify-content: center;
        align-items: center;
        vertical-align: top;
        font-size: 20px;
        word-break: break-all;
      }
      .m-btn {
        margin-top: 20px;
        margin-left: 600px;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <p>{{currheights}}</p>
      <div class="m-list" :style="{width: `${containerWidth}px`, height: `${maxCurrHeight}px`}">
        <div
          class="m-item"
          v-for="item in items"
          :key="item.num"
          :style="{width: `${item.width}px`, top: `${item.top}px`,left: `${item.left}px`}"
          :ref="`item${item.num}`"
          v-html="item.content"
        ></div>
      </div>
      <p class="m-btn"><button @click="requestItems">加载更多</button></p>
    </div>
  </body>
  <script src="../lib/vue.js"></script>
  <script type="text/javascript">
    new Vue({
      el: '#app',
      data() {
        return {
          num: 1, // 编号
          pageNum: 1,
          pageSize: 10, // 每次请求两行
          items: [],

          containerWidth: 1230, // 容器的长度
          colCount: 4, // 每列的个数
          colGap: 10, // 每列的空隙
          
          currheights: [] // 缓存当前每列的高度
        };
      },
      computed: {
        // 当前列的最大高度
        maxCurrHeight() {
          return Math.max(...this.currheights);
        },
        // 当前最矮列的编号
        minShortColNum() {
          let num = 1;
          for (let i = 2; i <= this.currheights.length; i++) {
            if (this.currheights[num] > this.currheights[i]) {
              num = i;
            }
          }
          return num;
        }
      },
      created() {
        // 初始化第0列的高度
        this.currheights = new Array(this.colCount + 1).fill(0);
      },
      mounted() {
        this.requestItems();
      },
      methods: {
        // 请求下一页数据
        async requestItems() {
          let list = [];
          for (let i = 0; i < this.pageSize; i++) {
            // 编号: 从1开始
            const num = this.num++;
            // 当前行编号: 从1开始
            const rowNum = Math.ceil(num / this.colCount);
            // 当前列编号: 从1开始
            const colNum = num % this.colCount || this.colCount;

            // 长度: (总长度-间隔长度)/列数
            const width = (this.containerWidth - (this.colCount - 1) * this.colGap) / this.colCount;

            // top: 当前行+间隔 (第一行不需要上一行的高度和间隔)
            const currColHeihgt = rowNum === 1 ? 0 : this.currheights[this.minShortColNum];
            const gap = rowNum === 1 ? 0 : this.colGap;
            const top = currColHeihgt + gap;

            // left: (编号 - 1) * (长度 + 间隔)
            const left = (this.minShortColNum - 1) * (width + this.colGap);

            // 渲染节点
            this.items.push({
              num,
              rowNum,
              colNum,
              width,
              left,
              top,
              content:
                `编号:${num}<br/> 行编号:${rowNum}<br/> 列编号${colNum}<br/>` +
                '<br/>'.repeat(Math.floor(Math.random() * 5))
            });

            // 渲染完缓存当前高度
            await this.$nextTick();
            const height = this.$refs[`item${num}`][0].getBoundingClientRect().height;
            this.currheights.splice(this.minShortColNum, 1, Math.floor(top + height)); // 缓存当前行高度
          }
          // 下一页
          this.pageNum++;
        }
      }
    });
  </script>
</html>

添加新评论