🦘🦘深度解析动态计算+定位实现瀑布流布局

1. 瀑布流布局的定义

页面上给人一种参差不齐的多栏布局,其中元素大部分为图片,图片的宽度是统一固定的,但是由于高度不一样,第一行图片排满之后,新的图片会插入到第一排中高度最低的图片下面,并更新高度,如此循环,最终达到瀑布流式的效果。下面看两个瀑布流布局的常见例子:

小红书:

淘宝M端:

除了小红书和淘宝,很多的产品和网站都会用到瀑布流布局,利用瀑布流布局能够有效的吸引用户的眼球、有效的利用空间,作为前端开发者,都应该掌握瀑布流布局的实现思路。

其实实现瀑布流布局的方法非常多,使用纯 CSS 或者使用 JS 都能实现,我们看下具体的实现方案!

2. 具体的实现方案

column-count

column-count 表示分栏数目,column-gap 表示分栏的间距;这里表示分 2 栏,两栏之间的间距为 30px

js 复制代码
.container {
  padding: 10px;
  column-count: 3;
  column-gap: 30px;
}

这种方法存在一些缺点:排列顺序是先上下后左右,而用户是横排观看,因此无法将优先级较高的项排列在前。无限滚动时由于新元素的加入导致,第二列上面的元素会移动到左边一列的最下面导致出现抖动。

gird布局

这种方式也可以实现,而且不会出现 column-count 布局以及 flex 布局的缺点,由于我对 gird 布局不是很了解,因此这里不提供具体的讲解思路,有兴趣的小伙伴可以自行查阅相关的资料。

flex布局

将容器设置为 flex 布局,设置允许换行 + 平均分布所有项目之间的间距。

js 复制代码
.container { 
    display: flex; 
    flex-wrap: wrap; 
    justify-content: space-evenly; 
}

缺点:由于 flex 布局是一行一行的,无法实现卡片的等宽不等高,导致每行上下卡片会出现空隙

定位布局

计算每个 card 的高度,使用 absolute 定位动态计算 card 高度的偏移位置,这是一些商业瀑布流的常见做法。缺点:计算代价大,可能会造成大量重排。

这里着重讲解使用 JS + absolute 定位实现瀑布流布局!

3. 动态计算+定位实现

先看下最终的效果:

我这里使用 Vue3 实现该功能,主要可以分为三个步骤:

  1. 创建图片元素并加入页面中
  2. 根据容器的宽度计算有多少列,以及间隙的数量
  3. 设置每张图片的位置(关键)
  4. 监听窗口 resize 事件,重新设置每张图片的位置

首先定义好图片的宽度以及间隙的宽度,瀑布流布局一定是要固定图片的宽度的,高度不相等

js 复制代码
const imgWidth = 200; // 每张图片的宽度
const spaceWidth = 30; // 水平间隙的宽度
const spaceHeight = 30; // 垂直间隙的高度

创建图片元素加入页面

使用 glob 批量导入指定文件夹下的图片,如果是在真实项目的话,图片可能是由接口返回;不同的处理方式代码会不一样,所以下面的代码仅供参考。

js 复制代码
// 导入所有的图片
let imageObj = import.meta.glob('@/assets/img/*.*', { eager: true });
let imageList = Object.values(imageObj).map((v: any) => v.default);

给每张图片都设置为绝对定位,这里有一个关键点:在图片加载完成之后(通过 onload 事件判断),调用 setPositions 方法重置每张图片的位置,不然整个页面的布局会很混乱,达不到想要的效果。

js 复制代码
const createImgs = () => {
  for (let i = 0; i < imageList.length; i++) {
    let img = document.createElement('img');
    img.src = imageList[i];
    img.onload = setPositions;
    img.style.position = 'absolute';
    img.style.width = imgWidth + 'px';
    img.style.transition = '0.3s';
    ImgContainer.value.appendChild(img);
  }
};

计算列数和间隙

外部容器的宽度不是固定的,采用百分比布局:

js 复制代码
.container {
  position: relative;
  width: 60%;
  margin: 50px auto;
}

首先通过 clientWidth 拿到容器的宽度,同时计算出有多少列以及间隙的数量,然后再重新计算容器的宽度并修改,防止容器出现多余的间隙,影响美观。

js 复制代码
// 计算一共有多少列,间隙的数量
const cal = () => {
  let containerWidth = ImgContainer.value.clientWidth; // 容器的宽度
  let columns = Math.floor(containerWidth / imgWidth); // 计算列数
  let spaceNumber = columns - 1; // 间隙数量
  ImgContainer.value.style.width =
    columns * imgWidth + spaceNumber * spaceWidth + 'px'; // 重置容器的宽度
  return {
    sapce: spaceWidth,
    columns,
  };
};

设置每张图片的位置

这是最关键的一步,核心:每一轮找到高度最小的列,添加图片

准备好一个数组,其元素个数等于列数,初始元素的值均为0,例如 nextTopsi = j 表示第i + 1列下一张图片的纵坐标是 j,下面通过几张图来详细分析:

初始状态:

根据每列图片的高度以及间隙的高度(这里是30),更新 nextTops 数组中的元素:

开启下一轮,找出 nextTops 中的最小值,也就是第一列,因此第四张图片放到第一列:

再次更新 nextTops 数组中的元素,然后以此类推就可以完成整个页面的图片布局啦!

js 复制代码
// 设置每张图片的位置
const setPositions = () => {
  ImgContainer.value.style.width = '60%'; // 重置容器的宽度
  let info = cal(); // 得到列数和间隙空间
  let nextTops = new Array(info.columns); // 该数组的长度为列数,每一项表示该列的下一个图片的纵坐标
  nextTops.fill(0); // 数组每一项填充为0
  for (let i = 0; i < ImgContainer.value.children.length; i++) {
    let img = ImgContainer.value.children[i];
    // 找到nextTops中的最小值作为当前图片的纵坐标
    let minTop = Math.min.apply(null, nextTops);
    img.style.top = minTop + 'px';
    // 重新设置数组这一项的下一个top值
    let index = nextTops.indexOf(minTop); // 得到使用的是第几列的top值
    nextTops[index] += img.height + spaceHeight;
    let left = index * info.sapce + index * imgWidth;
    img.style.left = left + 'px';
  }
  let max = Math.max.apply(null, nextTops); // 求最大值
  ImgContainer.value.style.height = max + 'px'; // 3. 设置容器的高度
};

监听窗口大小改变

当窗口大小改变后,容器的宽度会改变,重新设置每张图片的位置即可,同时由于 resize 事件频繁触发会浪费性能,因此采用防抖的方式优化。

js 复制代码
const headleResize = _.debounce(setPositions, 300);

onMounted(() => {
  createImgs();
  window.addEventListener('resize', headleResize);
});

onUnmounted(() => {
  window.removeEventListener('resize', headleResize);
});

到这里,基本就完成瀑布流布局了,当然这里只是提供一种思路,具体的实现细节还要根据不同的项目需求去做一些处理,还有很多小问题是需要处理的,以下是完整的代码:

js 复制代码
<template>
  <div ref="ImgContainer" class="container"></div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import * as _ from 'lodash';

// 导入所有的图片
let imageObj = import.meta.glob('@/assets/img/*.*', { eager: true });
let imageList = Object.values(imageObj).map((v: any) => v.default);

const imgWidth = 200; // 每张图片的宽度
const spaceWidth = 30; // 水平间隙的宽度
const spaceHeight = 30; // 垂直间隙的高度
const ImgContainer = ref();

// 1. 加入图片元素
const createImgs = () => {
  for (let i = 0; i < imageList.length; i++) {
    let img = document.createElement('img');
    img.src = imageList[i];
    img.onload = setPositions;
    img.style.position = 'absolute';
    img.style.width = 200 + 'px';
    img.style.transition = '0.3s';
    ImgContainer.value.appendChild(img);
  }
};

// 设置每张图片的位置
const setPositions = () => {
  ImgContainer.value.style.width = '60%'; // 重置容器的宽度
  let info = cal(); // 得到列数和间隙空间
  let nextTops = new Array(info.columns); // 该数组的长度为列数,每一项表示该列的下一个图片的纵坐标
  nextTops.fill(0); // 数组每一项填充为0
  for (let i = 0; i < ImgContainer.value.children.length; i++) {
    let img = ImgContainer.value.children[i];
    // 找到nextTops中的最小值作为当前图片的纵坐标
    let minTop = Math.min.apply(null, nextTops);
    img.style.top = minTop + 'px';
    // 重新设置数组这一项的下一个top值
    let index = nextTops.indexOf(minTop); // 得到使用的是第几列的top值
    nextTops[index] += img.height + spaceHeight;
    let left = index * info.sapce + index * imgWidth;
    img.style.left = left + 'px';
  }
  let max = Math.max.apply(null, nextTops); // 求最大值
  ImgContainer.value.style.height = max + 'px'; // 3. 设置容器的高度
};

// 计算一共有多少列,间隙的数量
const cal = () => {
  let containerWidth = ImgContainer.value.clientWidth; // 容器的宽度
  let columns = Math.floor(containerWidth / imgWidth); // 计算列数
  let spaceNumber = columns - 1; // 间隙数量
  ImgContainer.value.style.width =
    columns * imgWidth + spaceNumber * spaceWidth + 'px'; // 重置容器的宽度
  return {
    sapce: spaceWidth,
    columns,
  };
};

const headleResize = _.debounce(setPositions, 300);

onMounted(() => {
  createImgs();
  window.addEventListener('resize', headleResize);
});

onUnmounted(() => {
  window.removeEventListener('resize', headleResize);
});
</script>

<style lang="scss" scoped>
.container {
  position: relative;
  width: 60%;
  margin: 50px auto;
}
</style>
相关推荐
用户461169657283 分钟前
网站 HTTPS 不稳定?证书排查指南
前端
n876515 分钟前
Playwright Java WEB UI 自动化实现框架与逻辑
java·前端·ui
keyipatience16 分钟前
TypeScript学习(1)
前端·javascript·学习·typescript·html·css3
xifangge20251 小时前
基于 JavaScript 实现 CNC 数控切削参数与螺纹公差纯前端核算引擎
开发语言·前端·javascript
wangruofeng1 小时前
2026 Web 技术选型指南,AI 时代如何选择适合项目的技术栈
前端·aigc·ai编程
honkun61 小时前
vue 表格组件 vxe-table 修改滚动条样式,修改滚动条样式,统一宽度、颜色与自动隐藏
前端·javascript·vue.js
爱勇宝2 小时前
研究数字孪生的这半年,做了个 Demo,也有了一些新想法
前端·程序员
不可能片场2 小时前
工程实践 一个缓存把选片从十分钟压到一分钟
前端·electron
前端小万2 小时前
探索副业五个月,最终以放弃告终
前端·后端·面试
FRing032 小时前
JavaScript入门到精通二十四(正则表达式、元字符、修饰符)
开发语言·前端·javascript·正则表达式