一种很受图片网站欢迎的版面布局方式,叫做 Masonry Layout 瀑布流布局。

排版方式特点

内容是由多个不同高度的方格组成,而版面切割为多个直行。这些方格在每一个直行中一个接着一个排列,所以看起来行与行之间会有一些不整齐的感觉。

实现方法

最容易最完美

直接加载别人写好的 JavaScript 套件,

  • Masonry.js
  • Isotope.js

    JavaScript 实现原理

    通过 JavaScript 计算合共有多少个方格,再计算每一个方格的宽度和高度,因应容器的宽度可以放置多少行等等要求。
    将全部方式的 position 都设定为 absolute,逐一计算出 top 和 left 去定位。
    由于所有方格的位置得了是计算出来的,所以还可以在容器宽度改变的时候,将方格动态移动并重新排列。

纯 CSS - CSS Column

准备

https://picsum.photos 服务,可以随机生成一些假图

关键属性

  • column-count
  • column-gap ```html <!DOCTYPE html>

    瀑布流布局 - 图1
    瀑布流布局 - 图2
    瀑布流布局 - 图3
    瀑布流布局 - 图4
    瀑布流布局 - 图5
    瀑布流布局 - 图6
    瀑布流布局 - 图7
    瀑布流布局 - 图8
    瀑布流布局 - 图9
    瀑布流布局 - 图10
    瀑布流布局 - 图11
    瀑布流布局 - 图12
    瀑布流布局 - 图13
    瀑布流布局 - 图14
    瀑布流布局 - 图15

  1. <a name="U2v7f"></a>
  2. ## 一些缺陷
  3. 用两个 CSS 属性就能生成瀑布流布局,但是这样会现在一些问题。
  4. 为每个 item 增加一个 counter 的 小标签:
  5. ```css
  6. .item {
  7. padding: 2px;
  8. position: relative;
  9. counter-increment: item-counter;
  10. }
  11. .item::after {
  12. position: absolute;
  13. display: block;
  14. top: 2px;
  15. left: 2px;
  16. width: 24px;
  17. height: 24px;
  18. text-align: center;
  19. line-height: 24px;
  20. background-color: rgba(0, 0, 0, 0.5);
  21. color: #fff;
  22. content: counter(item-counter);
  23. }

出来的效果是这样子
image.png
我们会发现它的顺序是由每一个直行由上而下,然后再到第二个直行由上而下排列的。
如果想列出的内容是根据时间由新至旧排列,这样的排列顺序就不合适。

纯 CSS - Flexbox

我们期望是由左至右,再由上至下排列。

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

  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Masonry</title>
    <style>
      body {
        margin: 4px;
      }

      .masonry {
        display: flex;
        flex-direction: column;
        flex-wrap: wrap;
        height: 1500px;
      }

      .item {
        position: relative;
        width: 25%;
        padding: 2px;
        box-sizing: border-box;
        counter-increment: item-counter;
      }


      .item img {
        display: block;
        width: 100%;
        height: auto;
      }

      .item::after {
        position: absolute;
        display: block;
        top: 2px;
        left: 2px;
        width: 24px;
        height: 24px;
        text-align: center;
        line-height: 24px;
        background-color: rgba(0, 0, 0, 0.5);
        color: #fff;
        content: counter(item-counter);
      }
    </style>
  </head>

  <body>
    <div class="masonry">
      <div class="item">
        <img src="https://picsum.photos/200/300?random=1" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/200/300?random=2" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/360/450?random=3" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/200/300?random=4" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/360/450?random=5" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/600/600?random=6" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/360/450?random=7" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/360/450?random=8" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/100/100?random=9" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/200/300?random=10" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/700/600?random=11" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/200/300?random=12" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/450/600?random=13" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/500/300?random=14" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/300/300?random=15" />
      </div>
    </div>
  </body>

</html>

image.png
这样发现与 CSS Column 一样是由上至下,再由左至右排列。
但是 Flexbox 有一个 order 的属性可以让其不跟随 HTML 结构的顺序来排列 。

.item:nth-child(4n+1) {
  order: 1;
}
.item:nth-child(4n+2) {
  order: 2;
}
.item:nth-child(4n+3) {
  order: 3;
}
.item:nth-child(4n) {
  order: 4;
}

image.png

一些缺陷

要设定 Flex 容器的高度,需要知道内容加起来的高度是多少,还要考虑分多少直行。来为 Flex 容器计算一个合理的高度。

JavaScript

纯 CSS 的瀑布流都会现在一些缺陷,不够完美。借助 JavaScript 使瀑布流布局变得完善。
以每次以直行高度最小的来增加。当需要想由左至右,由上至下的布局,也就修改一下其逻辑就可以了。大逻辑对每张图片的 item 的 top 和 left 的设置。

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

  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Masonry</title>
    <style>
      body {
        margin: 4px;
      }

      .masonry {
        position: relative;
        width: 1200px;
        margin: 0 auto;
      }

      .item {
        position: absolute;
        counter-increment: item-counter;
      }

      .item img {
        display: block;
        width: 100%;
        height: auto;
      }

      .item::after {
        position: absolute;
        display: block;
        top: 2px;
        left: 2px;
        width: 24px;
        height: 24px;
        text-align: center;
        line-height: 24px;
        background-color: rgba(0, 0, 0, 0.5);
        color: #fff;
        content: counter(item-counter);
      }
    </style>
  </head>

  <body>
    <div class="masonry">
      <div class="item">
        <img src="https://picsum.photos/200/300?random=1" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/200/300?random=2" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/360/450?random=3" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/200/300?random=4" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/360/450?random=5" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/600/600?random=6" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/360/450?random=7" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/360/450?random=8" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/100/100?random=9" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/200/300?random=10" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/700/600?random=11" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/200/300?random=12" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/450/600?random=13" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/500/300?random=14" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/300/300?random=15" />
      </div>
    </div>
  </body>

</html>

制作一个瀑布流的插件,使用的属性与 CSS Column 相似

new Waterfall({
    el: '.masonry',
  column: 5, // column-count
  gap: 10, // column-gap
})

定义插件

; (function () {
  var Waterfall = function (opt) {
    this.el = document.querySelector(opt.el);
    this.oItems = this.el.querySelectorAll('div');
    this.column = opt.column;
    this.gap = opt.gap;
    // 计算出每一张图片的宽度
    this.itemWidth = (this.el.offsetWidth - (this.column - 1) * this.gap) / this.column;
    this.heightArr = [];

    this.init();
  }

  Waterfall.prototype.init = function () {
    this.render();
  }

  Waterfall.prototype.render = function () {
    var item = null,
        minIdx = -1;
    for (var i = 0; i < this.oItems.length; i++) {
      item = this.oItems[i];

      item.style.width = this.itemWidth + 'px';

      if (i < this.column) { // 第一排 高度都为 0
        item.style.top = '0px';
        item.style.left = i * (this.itemWidth + this.gap) + "px";
        var oImg = item.querySelector('img');
        if (oImg.complete) {
          // 循环后把对应的高度放置 heightArr 中
          this.heightArr.push(item.offsetHeight);
        }
      } else { // 第二排每一个都会找最度最小的直行加入
        minIdx = getMinIdx(this.heightArr);
        item.style.left = this.oItems[minIdx].offsetLeft + 'px';
        item.style.top = (this.heightArr[minIdx] + this.gap) + 'px';

        this.heightArr[minIdx] += item.offsetHeight + this.gap;
      }
    }

    function getMinIdx(arr) {
      return arr.indexOf(Math.min.apply(null, arr));
    }
  }

  window.Waterfall = Waterfall;
})();

图片加载与图片高度

由于图片是网络加载,高度要图片加载完成后才知道。所以加入一点延迟等待图片完全加载后再进行瀑布布局的重排。

还有一种方案是,由后端提供图片的 URL 和图片的宽高的对象回来 [{src: ‘xxxxx/1.jpg’, width: 300, height: 400}],然后动态生成 img。这样就不用等待网络把加载完的图片回来。

function isImgLoaded(callback) {
  const checkLoaded = () => {
    const imgs = [].slice.call(document.querySelectorAll('img'));
    return imgs.every((img) => {
      return img.complete === true;
    });
  }
  const t = setInterval(() => {
    if (checkLoaded()) {
      clearInterval(t);
      callback && callback();
    } else {
    }
  }, 500);
}

最终实现

再加点动画让,图片加载、布局重排后显示没有那么突兀感。

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

  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Masonry</title>
    <style>
      .masonry {
        position: relative;
        width: 1200px;
        margin: 0 auto;
        opacity: 0;
        transition: opacity 1.2s;
      }

      .item {
        position: absolute;
        counter-increment: item-counter;
      }

      .item img {
        display: block;
        width: 100%;
        height: auto;
      }

      .item::after {
        position: absolute;
        display: block;
        top: 2px;
        left: 2px;
        width: 24px;
        height: 24px;
        text-align: center;
        line-height: 24px;
        background-color: rgba(0, 0, 0, 0.5);
        color: #fff;
        content: counter(item-counter);
      }
    </style>
  </head>

  <body>
    <div class="masonry">
      <div class="item">
        <img src="https://picsum.photos/100/100?random=1" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/250/200?random=2" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/360/450?random=3" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/300/300?random=4" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/320/420?random=5" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/600/600?random=6" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/360/450?random=7" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/360/450?random=8" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/100/100?random=9" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/200/300?random=10" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/700/600?random=11" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/200/300?random=12" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/450/600?random=13" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/500/300?random=14" />
      </div>
      <div class="item">
        <img src="https://picsum.photos/300/300?random=15" />
      </div>
    </div>

    <script>
      function isImgLoaded(callback) {
        const checkLoaded = () => {
          const imgs = [].slice.call(document.querySelectorAll('.masonry img'));
          return imgs.every((img) => {
            return img.complete === true;
          });
        }
        const t = setInterval(() => {
          if (checkLoaded()) {
            clearInterval(t);
            callback && callback();
          } else {
          }
        }, 500);
      }


      ; (function () {
        var Waterfall = function (opt) {
          this.el = document.querySelector(opt.el);
          this.oItems = this.el.querySelectorAll('div');
          this.column = opt.column;
          this.gap = opt.gap;
          // 计算出每一张图片的宽度
          this.itemWidth = (this.el.offsetWidth - (this.column - 1) * this.gap) / this.column;
          this.heightArr = [];

          this.init();
        }

        Waterfall.prototype.init = function () {
          this.render();
        }

        Waterfall.prototype.render = function () {
          var item = null,
              minIdx = -1;
          for (var i = 0; i < this.oItems.length; i++) {
            item = this.oItems[i];

            item.style.width = this.itemWidth + 'px';

            if (i < this.column) { // 第一排 高度都为 0
              item.style.top = '0px';
              item.style.left = i * (this.itemWidth + this.gap) + "px";

              // 循环后把对应的高度放置 heightArr 中
              this.heightArr.push(item.offsetHeight);            
            } else { // 第二排每一个都会找最度最小的直行加入
              minIdx = getMinIdx(this.heightArr);
              item.style.left = this.oItems[minIdx].offsetLeft + 'px';
              item.style.top = (this.heightArr[minIdx] + this.gap) + 'px';

              this.heightArr[minIdx] += item.offsetHeight + this.gap;
            }
          }

          function getMinIdx(arr) {
            return arr.indexOf(Math.min.apply(null, arr));
          }

          this.el.style.opacity = '1';
        }

        window.Waterfall = Waterfall;
      })();

      isImgLoaded(() => {
        new Waterfall({
          el: '.masonry',
          column: 5, // column-count
          gap: 3, // column-gap
        })
      });
    </script>
  </body>

</html>

image.png
点击查看【codepen】