【必看】Grid 实现不定高度的响应式真瀑布流

2,645 阅读3分钟

最近在探索实现真瀑布流功能时,发现大部分纯CSS的方案都是假瀑布流,无法满足子元素不定高度的需求。然后我发现了一个使用Grid布局来实现假瀑布流的方案,于是在此基础上进行了改造,并结合少量JavaScript实现了真瀑布流。下面我将分享一下实现的基本原理以及一些关键技术细节。

实现效果

点击查看在线 Demo

image.png

b7kld-mao0j.gif

Demo 代码

<!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')
      // 获取当前列数
      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>

实现

基本原理

Grid布局类似于Excel表格,是一行一行排列的,无法让不定高度的项目依次堆叠在一起。因此,我们首先将Grid网格设置得非常密集,通过grid-auto-rows: 2px将每一行的网格设置为 2px 高度,然后通过JavaScript获取元素高度并设置元素的需占行数,通过grid-row-end: span 行数来实现堆叠。

如何保证排序是正确的

使用grid-row-end: span 行数进行布局时,浏览器会自动排序,我们只需告诉浏览器每个卡片占据多少格,而不需要固定它们的位置。浏览器会自动堆叠,实现真正的瀑布流效果。由于JavaScript的操作非常少,堆叠排布和元素宽度变化都由浏览器自动处理,因此性能损失很少。

元素间隔是如何实现的

由于我们的网格设置得非常小,一个瀑布流元素占据了一列的多个网格,所以无法直接使用gap来实现行间隔(列间隔仍然可以使用gap-column-gap实现)

我们首先尝试将行预期间隔加入到计算瀑布流元素高度的过程中,从而给瀑布流元素加上了一个下边距。然而,这种方法会导致最后一行的元素出现不应该有的下边距。由于瀑布流的特性,最后一行的元素并不确定,因此无法直接取消其下边距。

为了解决这一问题,我们转变了思路,尝试给瀑布流元素加上边距。通过使用align-items: end属性让内容靠下,我们成功实现了上边距。随后,通过JavaScript获取当前列数,我们成功地取消了每列第一个元素的上边距,从而实现了瀑布流元素间的间隔效果。通过这种方法,我们巧妙地解决了瀑布流元素间隔的问题,使得布局更加美观和整洁。

const masonry = document.querySelector('.masonry')
const items = masonry.querySelectorAll('.item')
// 获取当前列数
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}`;
})