Skip to content
鼓励作者:欢迎打赏犒劳

07-瀑布流布局

https://www.w3cschool.cn/article/48614293.html

https://blog.csdn.net/Jzsn_Paul/article/details/140169578

https://juejin.cn/post/7368855076130488339

https://developers.weixin.qq.com/community/develop/article/doc/00004a4ae7c7a8aaddca7dc4f56413

这个瀑布流布局真的坑很多很多,还是挺难的,网上绝大部分都有瑕疵,都不能用。

  • 支持下一页加载
  • 高度由里面的子元素(一般为图片)高撑开
  • 需要考虑图片加载慢的问题

绝对定位实现

完美实现!!!

html
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        *{margin: 0;padding: 0;}
        .main{
            border: 1px solid #c1c1c1;
            width: 900px;
            height: 100vh;
            position: relative;
            margin: 0 auto;
        }
        .item{
            width: 150px;
            float: left;
        }
        .auxiliary{ /* 很重要,用来设置子item的margin,曲线救国 */
            background: #4CAF50;
            margin: 5px;
        }
        .item-img {

        }
        img {  /* 一定要加这个,不然item-img的高会比img的高多几个像素 */
            display: block; /* 改为块级元素 */
            vertical-align: top; /* 如果仍然希望保留行内特性 */
        }
    </style>

    <script>
        function getData(curSize){
            let list = []
            for (let i = 0; i < curSize; i++) {
                list.push(`https://picsum.photos/200/${num(100, 300)}?v=` + Math.random())
            }
            return list;
        }
        let num = (min,max) => {
            return Math.floor(Math.random() * max) + min
        }
        function parseHTML(htmlString) {
            const parser = new DOMParser();
            const doc = parser.parseFromString(htmlString, 'text/html');
            return doc.body.firstChild; // 返回第一个子元素
        }
        function insertImgToPage(src){
            let app = document.getElementById("app");
            let htmlString = `<div class="item"><div class="auxiliary"><div class="item-img"><img style="width: 100%"  src="${src}" alt=""></div></div></div>`
            const domElement = parseHTML(htmlString);
            app.appendChild(domElement);
            return domElement;
        }
        let item_ok_list = [] //已经完成瀑布流布局的图片
        let g_width = 150 //全局宽度
        let column; //几列
        function preDeal(parent){
            let cParent =document.getElementById(parent);
            let p_width =cParent.offsetWidth;
            column =Math.floor(p_width/g_width);
        }
        window.onload = function (){
            preDeal('app')
        }

        function add(){
            // 1、获取数据
            let list = getData(10);
            // 2、获取图片url集合
            let images = list;
            // 3、判断那些图片加载OK,放到数组
            let completeImgList = [] //加载好的图片
            // 开始加载图片
            images.forEach(src => {
                const img = new Image();
                // 监听图片加载完成事件
                img.onload = function() {
                    console.log(src + ' loaded');
                    completeImgList.push(src);

                    // 4、将加载好的图片插入到页面
                    let item = insertImgToPage(src);

                    // 5、瀑布流布局
                    pubu('app',item)
                };
                img.onerror = function() {
                    console.error(src + ' failed to load');
                };
                img.src = src; // 开始加载图片
            });
        }
        let boxHeightArr =[];
        function pubu(parent,child){
            if (item_ok_list.length < column){
                boxHeightArr.push(child.offsetHeight);
            }else{
                let minHeight =Math.min(...boxHeightArr);
                let minIndex =boxHeightArr.indexOf(minHeight);
                child.style.position ='absolute';
                child.style.top =`${minHeight}px`;
                child.style.left =`${g_width * minIndex}px`;
                boxHeightArr[minIndex]+=child.offsetHeight;
            }
            item_ok_list.push(child)
        }
    </script>
</head>
<body>
<button onclick="add()"> add </button>
<div id="app" class="main">

</div>
</body>
</html>

Vue3的写法

vue
<template>
  <div>
    <button @click="addImages">Add Images</button>
    <div id="myPicture" class="main" ref="appRef"></div>
  </div>
</template>

<script setup>
import { onMounted, ref, nextTick } from 'vue';

const appRef = ref(null);

//加载OK的全部照片集合
const item_ok_list = ref([]);
//当前查看的图片
const currentPic = ref('');
// 每列的高集合
const boxHeightArr = ref([]);
//容器ID
const parentId = "myPicture"
//固定宽
const g_width = 150;
//几列
let column;


function getData(curSize) {
  let list = [];
  for (let i = 0; i < curSize; i++) {
    list.push(`https://picsum.photos/200/${num(100, 300)}?v=` + Math.random())
  }
  return list;
}
let num = (min,max) => {
  return Math.floor(Math.random() * max) + min
}

function insertImgToPage(src) {
  const htmlString = `
    <div class="item">
      <div class="auxiliary">
        <div class="item-img">
          <img style="width: 100%;" src="${src}" alt="" />
        </div>
      </div>
    </div>
  `;
  const domElement = parseHTML(htmlString);
  appRef.value.appendChild(domElement);
  return domElement;
}

function parseHTML(htmlString) {
  const parser = new DOMParser();
  const doc = parser.parseFromString(htmlString, 'text/html');
  return doc.body.firstChild;
}


function preDeal() {
  const cParent = document.getElementById(parentId);
  const p_width = cParent.offsetWidth;
  column = Math.floor(p_width / g_width);
}
onMounted(() => {
  preDeal()
});


function addImages() {
  const list = getData(10);

  list.forEach((src) => {
    const img = new Image();

    img.onload = () => {
      console.log(`${src} loaded`);

      const item = insertImgToPage(src);
      pubu(item);

      // 获取实际插入到 DOM 中的 img 元素,并绑定点击事件
      const imgElement = item.querySelector('img');
      bindClickEvent(imgElement);
    };

    img.onerror = () => {
      console.error(`${src} failed to load`);
    };

    img.src = src;
  });

}
function bindClickEvent(img) {
  // 绑定 click 事件
  img.addEventListener('click', function(event) {
     currentPic.value = event.target.src
  });
}

function pubu(child) {
  if (item_ok_list.value.length < column) {
    boxHeightArr.value.push(child.offsetHeight);
  } else {
    const minHeight = Math.min(...boxHeightArr.value);
    const minIndex = boxHeightArr.value.indexOf(minHeight);
    child.style.position = 'absolute';
    child.style.top = `${minHeight}px`;
    child.style.left = `${g_width * minIndex}px`;
    boxHeightArr.value[minIndex] += child.offsetHeight;
  }
  item_ok_list.value.push(child);
}
</script>

<style>
#myPicture {
  /*border: 1px solid #c1c1c1;*/
  /* background: #4CAF50;*/
  width: 900px;
  min-height: 600px;
  position: relative;
  margin: 0 auto;

  /*overflow: auto;*/
}
#myPicture .item {
  width: 150px;
  float: left;
}
#myPicture .auxiliary {
  background: #4CAF50;
  margin: 5px;
}
#myPicture .item-img {
  display: block;
  vertical-align: top;
}
#myPicture img {
  display: block;
  vertical-align: top;
}
/**隐藏滚动条*/
#myPicture::-webkit-scrollbar {
  width: 0.5em;
}

#myPicture::-webkit-scrollbar-track {
  background-color: transparent;
}

#myPicture::-webkit-scrollbar-thumb {
  background-color: transparent;
}
</style>

css+js实现

实现1

非常完美

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>瀑布流布局</title>
    <style>
        /* 重置样式 */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: Arial, sans-serif;
            background-color: #f8f8f8;
        }

        /* 瀑布流容器 */
        .waterfall {
            display: flex;
            flex-wrap: wrap;
            padding: 10px;
        }

        /* 每一列的样式 */
        .column {
            flex: 1;
            min-width: 45%; /* 两列布局,留一些间距 */
            margin: 0 5px;
        }

        /* 每个卡片项的样式 */
        .card {
            background-color: #fff;
            border-radius: 8px;
            margin-bottom: 10px;
            overflow: hidden;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }

        /* 图片容器 */
        .card img {
            width: 100%;
            display: block;
            border-radius: 8px 8px 0 0;
            object-fit: cover;
        }

        /* 骨架屏占位符 */
        .card .skeleton {
            width: 100%;
            height: 200px; /* 假设图片高度为200px */
            border-radius: 8px 8px 0 0;
            background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
            background-size: 200% 100%;
            animation: loading 1.5s infinite;
            z-index: 1;
        }

        /* 标题样式 */
        .card .title {
            padding: 10px;
            font-size: 14px;
            color: #333;
        }
        /* 骨架屏动画 */
        @keyframes loading {
            0% {
                background-position: 200% 0;
            }

            100% {
                background-position: -200% 0;
            }
        }
    </style>
</head>
<body>
<div class="waterfall">
    <!-- 第一列 -->
    <div class="column" id="column1"></div>
    <!-- 第二列 -->
    <div class="column" id="column2"></div>
</div>

<script>
    // 模拟数据
    const data = [
        { image: "https://picsum.photos/300/300", title: "标题1" },
        { image: "https://picsum.photos/300/100", title: "标题2" },
        { image: "https://picsum.photos/300/500", title: "标题3" },
        { image: "https://picsum.photos/300/200", title: "标题4" },
        { image: "https://picsum.photos/300/300", title: "标题5" },
        { image: "https://picsum.photos/300/400", title: "标题6" },
        { image: "https://picsum.photos/300/500", title: "标题3" },
        { image: "https://picsum.photos/300/200", title: "标题4" },
        { image: "https://picsum.photos/300/300", title: "标题5" },
        { image: "https://picsum.photos/300/400", title: "标题6" },
    ];

    // 获取列元素
    const column1 = document.getElementById("column1");
    const column2 = document.getElementById("column2");

    // 动态加载卡片
    data.forEach((item, index) => {
        // 创建卡片
        const card = document.createElement("div");
        card.className = "card";

        // 图片部分(骨架屏占位符)
        const imageContainer = document.createElement("div");
        imageContainer.className = "skeleton";
        card.appendChild(imageContainer);

        // 标题部分
        const title = document.createElement("div");
        title.className = "title";
        title.textContent = item.title;
        card.appendChild(title);

        // 动态加载图片
        const img = new Image();
        img.src = item.image;
        img.onload = () => {
            // 图片加载完成后替换骨架屏
            imageContainer.replaceWith(img);
        };

        // 根据索引决定添加到哪一列
        if (index % 2 === 0) {
            column1.appendChild(card);
        } else {
            column2.appendChild(card);
        }
    });
</script>
</body>
</html>

实现2

html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <!-- 适配移动端 -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>小红书瀑布流布局</title>
    <style>
        /* 全局样式,去除默认的内外边距 */
        body {
            margin: 0;
            padding: 0;
            font-family: Arial, sans-serif;
            background-color: #f4f4f4;
        }

        /* 瀑布流容器样式 */
        .waterfall-container {
            /* 列数为 2,实现一行两列布局 */
            column-count: 2;
            /* 列之间的间距为 10px */
            column-gap: 10px;
            padding: 10px;
        }

        /* 卡片样式 */
        .card {
            /* 防止卡片跨列显示 */
            break-inside: avoid;
            background-color: #fff;
            border-radius: 8px;
            /* 卡片阴影效果 */
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            margin-bottom: 10px;
            overflow: hidden;
            position: relative;
        }

        /* 图片样式 */
        .card img {
            width: 100%;
            height: auto;
            display: block;
        }

        /* 标题样式 */
        .card-title {
            padding: 10px;
            font-size: 14px;
            line-height: 1.4;
        }

        /* 骨架屏样式 */
        .skeleton::before {
            content: "";
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
            background-size: 200% 100%;
            animation: loading 1.5s infinite;
            z-index: 1;
        }

        /* 骨架屏动画 */
        @keyframes loading {
            0% {
                background-position: 200% 0;
            }

            100% {
                background-position: -200% 0;
            }
        }
    </style>
</head>

<body>
<!-- 瀑布流容器 -->
<div class="waterfall-container">
    <!-- 卡片示例 -->
    <div class="card skeleton">
        < img src="https://picsum.photos/800/400" alt="Image 1" onload="this.parentNode.classList.remove('skeleton')">
        <div class="card-title">这是第一张图片的标题</div>
    </div>
    <div class="card skeleton">
        < img src="https://picsum.photos/300/350" alt="Image 2" onload="this.parentNode.classList.remove('skeleton')">
        <div class="card-title">这是第二张图片的标题</div>
    </div>
    <div class="card skeleton">
        < img src="https://picsum.photos/300/450" alt="Image 3" onload="this.parentNode.classList.remove('skeleton')">
        <div class="card-title">这是第三张图片的标题</div>
    </div>
    <div class="card skeleton">
        < img src="https://picsum.photos/300/300" alt="Image 4" onload="this.parentNode.classList.remove('skeleton')">
        <div class="card-title">这是第四张图片的标题</div>
    </div>
    <div class="card skeleton">
        < img src="https://picsum.photos/300/800" alt="Image 4" onload="this.parentNode.classList.remove('skeleton')">
        <div class="card-title">这是第五张图片的标题</div>
    </div>
    <div class="card skeleton">
        < img src="https://picsum.photos/300/200" alt="Image 4" onload="this.parentNode.classList.remove('skeleton')">
        <div class="card-title">这是第六张图片的标题</div>
    </div>
</div>
</body>

</html>

grid实现

实现一

先看一个小demo,看似看不错,其实有很大的问题,这个高度不是动态的,是写死的。我们想要的是高度是由里面的子元素的高撑开的。

html
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .main {
            background: #e4e4e4;
            display: grid;
            width: 300px;
            grid-template-columns: repeat(2, 1fr);   /*指定两列,自动宽度*/
            grid-gap: 1px;  /*横向,纵向间隔*/
            grid-auto-flow: row dense;  /*是否自动补齐空白*/
            grid-auto-rows: 20px;  /*base高度,grid-row基于此运算*/
        }
        .main .item {
            width: 100%;
            background: #222;
            color: #ddd;
        }

        .main .item:nth-of-type(3n+1) {
            grid-row: auto / span 5;
        }

        .main .item:nth-of-type(3n+2) {
            grid-row: auto / span 6;
        }

        .main .item:nth-of-type(3n+3) {
            grid-row: auto / span 8;
        }
    </style>

</head>
<body>
<div class="main">
    <div class="item">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
    <div class="item">4</div>
    <div class="item">5</div>
    <div class="item">6</div>
    <div class="item">7</div>
    <div class="item">8</div>
</div>
</body>
</html>

实现二

html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Demo</title>
    <style>
        body {
            margin: 0;
        }
        .masonry {
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            grid-gap: 0 60px;
            grid-auto-rows: 2px;
            align-items: end;
        }
        .item {
            background: #f8f8fa;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        @media (min-width: 1280px) and (max-width: 1920px) {
            .masonry {
                grid-template-columns: repeat(3, 1fr);
            }
        }
        @media (min-width: 768px) and (max-width: 1280px) {
            .masonry {
                grid-template-columns: repeat(2, 1fr);
            }
        }
        @media (max-width: 768px) {
            .masonry {
                grid-template-columns: repeat(1, 1fr);
            }
        }
    </style>
</head>
<body>
<div class="masonry">
    <div class="item">item1</div>
    <div class="item">item2</div>
    <div class="item">item3</div>
    <div class="item">item4</div>
    <div class="item">item5</div>
    <div class="item">item6</div>
    <div class="item">item7</div>
    <div class="item">item8</div>
    <div class="item">item9</div>
    <div class="item">item10</div>
    <div class="item">item11</div>
    <div class="item">item12</div>
    <div class="item">item13</div>
    <div class="item">item14</div>
    <div class="item">item15</div>
    <div class="item">item16</div>
    <div class="item">item17</div>
    <div class="item">item18</div>
    <div class="item">item19</div>
    <div class="item">item20</div>
    <div class="item">item21</div>
    <div class="item">item22</div>
    <div class="item">item23</div>
    <div class="item">item24</div>
    <div class="item">item25</div>
</div>

<script>
    // 给每个元素模拟随机高度
    window.addEventListener('load', () => {
        document.querySelectorAll('.masonry > .item').forEach(item => {
            item.style.height = `${Math.floor(Math.random() * 200) + 100}px`
        })
    })

    const calcRows = () => {
        const masonry = document.querySelector('.masonry')
        const items = masonry.querySelectorAll('.item')
        // 获取当前列数  这个API可以直接使用,grid专门提供的
        const cols = getComputedStyle(masonry).gridTemplateColumns.split(" ").length;
        items.forEach((item, index) => {
            // 给需要上下间隔的元素增加上间隔(每列第一个元素无需上间隔)
            const gapRows = index >= cols ? 8 : 0;
            // 根据元素高度设置元素的需占行数
            const rows = Math.ceil(item.clientHeight / 2) + gapRows;
            item.style.gridRowEnd = `span ${rows}`;
        })
    }
    window.addEventListener('resize', calcRows)
    window.addEventListener('load', calcRows)
</script>
</body>
</html>

实现无限滚动

主要是利用了IntersectionObserver这个API的能力,检测目标dom是否出现在容器的视口中

描述: 定义了在目标元素与根相交时,触发回调的可见性阈值,范围在 0 到 1 之间。

threshold:

  • 0 表示只要有任何部分可见,回调就会触发;
  • 1 表示目标元素全部可见时才会触发。
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Intersection Observer Demo</title>
    <style>
        #container {
            height: 200px;
            overflow: auto;
            border: 1px solid #000;
        }
        #target {
            height: 100px;
            background-color: lightblue;
            margin-top: 300px; /* initial position is outside of the viewport */
        }
        .spacer {
            height: 500px; /* create space to scroll */
        }
    </style>
</head>
<body>
    <div id="container">
        <div class="spacer"></div>
        <div id="target">Target Element</div>
    </div>

    <script>
        const target = document.getElementById('target');
        const container = document.getElementById('container');

        const options = {
            root: container, // 观察的上下文元素
            rootMargin: '0px',
            threshold: 0.1 // 目标元素在根元素中可见的百分比时触发
        };

        const callback = (entries, observer) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    // 元素出现在容器内,触发事件
                    console.log('Target is inside the container!');
                    // 这里可以执行更多的逻辑,比如调用其他函数等
                    
                    // 如果你只希望触发一次事件,可以取消观察
                    observer.unobserve(entry.target);
                }
            });
        };

        const observer = new IntersectionObserver(callback, options);
        observer.observe(target); // 开始观察目标元素
    </script>
</body>
</html>

如有转载或 CV 的请标注本站原文地址