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>

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 的请标注本站原文地址