Skip to content

原生 JS 实现瀑布流效果 #2

@liuyib

Description

@liuyib

瀑布流,又叫瀑布流式布局。是一些图片网站常用的布局,主要的特点是:图片等宽,竖直方向上参差不齐。

静态效果如下:

index_show

要想实现这个效果,只用 CSS 是很难的,但不是不可以。纯 CSS 实现这里先不讨论。

开始之前,需要准备好一些图片,图片要求:宽度要相等,高度随意。

实现原理是:给每个图片设置绝对定位,然后动态改变这些图片的 lefttop 即可实现自动排列。这里重点是如何动态改变这些图片的 lefttop

计算列数

第一行图片是瀑布流的基础,也是实现的关键。所以从第一行开始说起:

计算图片列数:图片列数 = 可视区的宽度 / 图片的宽度:

first_line

再加上图片之间的间隙:图片列数 = 可视区宽度 / (图片宽度 + 间隙宽度):

first_line_gap

排列行

第一行图片 top 都是 0,所以只要动态设置每张图片的 left 就可以排列好:left = (imgWidth + gap) * i

第一行排好后,排列第二行:

  • 首先准备一个数组 arr 用于储存每一列的总高度。
  • 将第一行 图片的高度(通过 img.offsetHeight 获取)存入数组:arr[h1, h2, h3, h4, h5, h6]
  • 找出数组中高度最小的那一项,并记录它的索引 index。然后把第二行的第一张图片放到这张图片后面。
  • 第二行第一张图片位置为:
    left = arr[index].offsetLeft。(因为同一列中的图片 left 是一样的)
    top = arr数组中的最小高度 + 间隙大小

second_first

这时候会发现,后面所有的图片都叠在了第二行的第一张图片那里:

secone_chongdie

这是因为,在第二行加入第一张图片后,并没有更新数组中的数值。现在,只需要更新一下最小列的高度即可。

每次向高度最小的列后面添加一张图片后,更新一下数组中储存的这列的高度值,就实现了图片的自动排列,从而实现瀑布流效果。

全部代码如下:

HTML:

View Content
<div id="imgs-wrapper" class="imgs_wrapper">
  <img src="./imgs/1.jpg" alt="water_fall_img" />
  <img src="./imgs/2.jpg" alt="water_fall_img" />
  <img src="./imgs/3.jpg" alt="water_fall_img" />
  <img src="./imgs/4.jpg" alt="water_fall_img" />
  <img src="./imgs/5.jpg" alt="water_fall_img" />
  <img src="./imgs/6.jpg" alt="water_fall_img" />
  <img src="./imgs/7.jpg" alt="water_fall_img" />
  <img src="./imgs/8.jpg" alt="water_fall_img" />
  <img src="./imgs/9.jpg" alt="water_fall_img" />
  <img src="./imgs/10.jpg" alt="water_fall_img" />
  <img src="./imgs/11.jpg" alt="water_fall_img" />
  <img src="./imgs/12.jpg" alt="water_fall_img" />
  <img src="./imgs/13.jpg" alt="water_fall_img" />
  <img src="./imgs/14.jpg" alt="water_fall_img" />
  <img src="./imgs/15.jpg" alt="water_fall_img" />
  <img src="./imgs/16.jpg" alt="water_fall_img" />
  <img src="./imgs/17.jpg" alt="water_fall_img" />
  <img src="./imgs/18.jpg" alt="water_fall_img" />
  <img src="./imgs/19.jpg" alt="water_fall_img" />
  <img src="./imgs/20.jpg" alt="water_fall_img" />
</div>

CSS:

View Content
* {
  margin: 0;
  padding: 0;
}

body {
  background: #eee;
}

.imgs_wrapper img {
  position: absolute;
  width: 236px;
}

JavaScript:

View Content
window.onload = function() {
  var oWrapper = document.getElementById("imgs-wrapper");
  var aImgs = oWrapper.getElementsByTagName("img");

  waterFall(); // 初始化

  function waterFall() {
    var nGap = 10; // 图片间隙
    var nImgWidth = aImgs[0].offsetWidth; // 图片宽度
    var nClientWidth = getClient().width; // 可视区宽度
    var nColumns = parseInt(nClientWidth / (nImgWidth + nGap)); // 列数
    var aImgHeights = []; // 储存每列图片的总高度

    for (var i = 0; i < aImgs.length; i++) {
      var nImgHeight = aImgs[i].offsetHeight; // 当前图片的高度

      if (i < nColumns) {
        // 第一行图片
        aImgs[i].style.top = 0;
        aImgs[i].style.left = (nImgWidth + nGap) * i + "px";
        aImgHeights.push(nImgHeight); // 储存当前图片的高度
      } else {
        var nMinHeight = getMinNum(aImgHeights); // 获取数组中最小高度
        var nMinIndex = aImgHeights.indexOf(nMinHeight); // 最小高度的索引(即图片所在的列)
        var nImgLeft = aImgs[nMinIndex].offsetLeft;
        var nImgTop = aImgHeights[nMinIndex] + nGap;

        // 执行动画
        startMove(aImgs[i], {
          left: nImgLeft,
          top: nImgTop
        });

        // 添加新的图片后,更新数组中的最小高度
        aImgHeights[nMinIndex] = nMinHeight + nGap + nImgHeight;
      }
    }
  }

  /**
   * 获取数组中的最小数
   * @param {Array} arr 数字数组
   */
  function getMinNum(arr) {
    return Math.min.apply(Math, arr);
  }

  // 获取页面可视区的宽高
  function getClient() {
    return {
      width:
        window.innerWidth ||
        document.documentElement.clientWidth ||
        document.body.clientWidth ||
        0,
      height:
        window.innerHeight ||
        document.documentElement.clientHeight ||
        document.body.clientHeight ||
        0
    };
  }

  /**
   * 获取某个属性的值
   * @param {Object} elem 当前元素
   * @param {String} attr 属性
   */
  function getStyle(elem, attr) {
    if (window.getComputedStyle) {
      return window.getComputedStyle(elem, null)[attr];
    } else {
      return elem.currentStyle[attr];
    }
  }

  /**
   * 运动函数
   * @param {Object}   elem 当前元素
   * @param {Object}   json 要操作的属性 // 传透明度属性时,单位为100
   * @param {Function} fn   回调函数
   * @param {Number}   slow 动画的缓慢程度 // 数值越大,动画越迟缓。
   * @param {Number}   time 每次动画执行时间
   */
  function startMove(elem, json, fn = null, slow = 10, time = 15) {
    clearInterval(elem.timer);

    elem.timer = setInterval(function() {
      var bStop = true;

      for (var attr in json) {
        var iCur = 0;
        var target = json[attr];

        if (attr === "opacity") {
          iCur = getStyle(elem, attr) * 100 || 1;
        } else {
          iCur = parseInt(getStyle(elem, attr)) || 0;
        }

        var iSpeed = (target - iCur) / slow;
        iSpeed = iSpeed > 0 ? Math.ceil(iSpeed) : Math.floor(iSpeed);
        iCur += iSpeed;

        if (attr === "opacity") {
          elem.style.filter = "alpha(opacity=" + iCur + ")";
          elem.style[attr] = iCur / 100;
        } else if (attr === "zIndex") {
          elem.style.zIndex = target;
        } else {
          elem.style[attr] = iCur + "px";
        }

        iCur != target && (bStop = false);
      }

      if (bStop) {
        clearInterval(elem.timer);
        fn && fn();
      }
    }, time);
  }
};

这里用了一个运动函数 startMove,这个函数使用很简单,我们只需要把 操作的对象运动时要操作的属性与值 传给它。它就会自动完成这个动画。

效果如下:

progress_3 1

响应浏览器窗口大小

到这里我们知道了,只要执行 waterFall 方法,图片就会自动去找自己的位置。所以如果想要图片的位置随着窗口大小的改变而改变,只需要加上这些代码:

window.addEventListener("resize", waterFall);

效果如下:

20190120160233

但这样是有问题的,当浏览器窗口触发 resize 事件时,直接执行 waterFall 方法会造成性能问题(因为浏览器的 resize 事件会在短时间内触发很多次),所以需要使用防抖函数进行改进。这里涉及了 JS 中的节流、防抖的概念,具体实现如下:

/**
 * 防抖函数
 * @param {Object} func 要执行的函数
 * @param {Number} wait 间隔时间
 */
function debounce(func, wait) {
  var timer;

  return function() {
    var context = this;
    var args = arguments;

    clearTimeout(timer);
    timer = setTimeout(function() {
      func.apply(context, args);
    }, wait);
  };
}

然后修改代码:

- window.addEventListener('resize', waterFall);
+ window.addEventListener('resize', debounce(waterFall, 1000));

效果如下:

tz7c9-f30yc

这里为了演示效果,我将防抖时间设置为 1 秒,实际使用时设置为 300 毫秒左右即可。

一个很好的线上例子:Pinterest 是一个瀑布流图片网站,同样也对响应浏览器窗口大小进行了防抖处理。

节流、防抖函数参考文章:JavaScript 专题之跟着 underscore 学节流

懒加载效果

这个例子中,我用了 20 张图片来举例,实际中的图片都是成百上千张,不可能一下子全部加载。所以就需要使用 懒加载,即:当页面显示最后一张图片时,再去加载一批新的图片。

那么如何判断页面有没有显示最后一张图片呢?

这里通过最后一张图片的 offsetTop 值来判断,当 页面滚动的距离 + 页面可视区的高度 > 最后一张图片的offsetTop 时,证明页面中显示了最后一张图片:

lazy_loading

代码如下:

// 模拟获取到的 Ajax 数据
var newImgs = [
  './imgs/1.jpg',
  './imgs/2.jpg',
  ......
  './imgs/20.jpg'
];

window.onscroll = function() {
  if (getClient().height + getScrollTop() >= aImgs[aImgs.length - 1].offsetTop) {
    newImgs.forEach(function(item) {
      var oImg = document.createElement('img');
      oImg.src = item;
      oImg.alt = "new_img";
      oWrapper.appendChild(oImg);
    });
  }

  // 延迟加载图片,否则图片还没显示出来时,获取不到图片的高度
  setTimeout(function() {
    waterFall();
  }, 1000);
};

function getScrollTop() {
  return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
}

上面的代码还有一点问题:鼠标滚轮滚动时 onscroll 事件会被多次触发。试想一下,这里的代码如果多次被触发会怎样?页面中会瞬间加载很多很多图片(假如 onscroll 事件触发一次会加载 20 张图片,鼠标滚动一下触发了 10 次 onscroll 事件,那么一下就会加载 200 张图片,这样是很糟糕的)这个问题可以用上面提到的防抖函数进行处理。不过这里只是用布尔变量简单处理一下:

// 模拟获取到的 Ajax 数据
var newImgs = [
  './imgs/1.jpg',
  './imgs/2.jpg',
  ......
  './imgs/20.jpg'
];

+ var bFlag = false;

window.onscroll = function() {
+ if (bFlag) { return; }
  if (getClient().height + getScrollTop() >= aImgs[aImgs.length - 1].offsetTop) {
+   bFlag = true;

    newImgs.forEach(function(item) {
      var oImg = document.createElement('img');
      oImg.src = item;
      oImg.alt = "new_img";
      oWrapper.appendChild(oImg);
    });
  }

  // 延迟加载图片,否则图片还没显示出来时,获取不到图片的高度
  setTimeout(function() {
    waterFall();
  }, 1000);
};

function getScrollTop() {
  return window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0
}

至此,瀑布流效果已经基本实现。还有一些不足,可以自行尝试改进或者使用 Ajax 获取数据来模拟真实的场景。

Demo 体验地址:https://liuyib.github.io/blog/demo/blog/water-fall/

以上 🚀

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions