阅读 1958

瀑布流的原理以及实现

如何实现瀑布流

在最近的业务中,需要实现以下功能 :图片呈现两列排行,顺序从上到下,从左到右。具体如下:

懒人最先想到的使用市面上现有的库,在网上找到react-masonry-component这个库用于实现瀑布流。实际使用后发现效果有些差,为什么呢?因为可以明显数据加载完后,所有的图片都堆叠在一起了,然后监听到图片的onload事件以后,页面开始重新排版,就有个闪动的过程。产品表示无法接受这种情况,并且在自己业务中我们其实是事先知道这个图片的宽高的。所以在本业务中完全可以预设这个图片的宽高,避免这种不断重新排版的情况。因为现有的库不能满足我们的需求,所以干脆自己动手实现这个瀑布流

原理

在移动端的大多数情况下,图片都是等宽不等高。如下图,我们在第一排排下了多个等宽不登高的元素

新元素需要放置第一行元素最短的一个后面

如果还需要在添加元素,需要计算出所有列中最短的列,然后把元素放到最短的列后面

效果大概就是如下,现在来具体分析一下其中的步骤

首先我们需要明确所有的内容其实通过脱离文档流,用定位把元素定位在各自的位置上,所以先调整元素的样式

<div className="container">
    {
        list.map(item => (<img src={item.src} className="item" />))
    }
</div>
<style>
.container {

    position: relative;
}
.item {
    position: absolute
}
</style>
复制代码

排列第一行

现在我们假设一共需要排列column列,然后每列之间的间隔是gap宽度,每一列的宽度为columnWidth。需要渲染的列表是list,每个元素都需要设置他的left值,和top值。因为第一行没有前置元素,所以这里单独处理一下。

const items = docuquery.querySelectorAll('item');
items.forEach((item, index) => {
    if (index < column) {
        item.style.top = 0;
        item.style.left = (columnWidth + gap) * index
    }
})
复制代码

从上面可以看到元素的left的值就是每列的宽度加上边距,在乘上当前是属于第几列。

获取所有列的最小高度

在对第一行进行排版以后,开始最二行进行排版,新加入的元素需要在第一行最短的元素的后面,所以我们需要计算所有列的高度进行保存,然后得出高度最小列的长度和最小列所在的索引,所以在第一行元素的排版代码中,我们对每列的高度进行存储

const items = docuquery.querySelectorAll('item');
items.forEach((item, index) => {
    if (index < column) {
        item.style.top = 0;
        item.style.left = (columnWidth + gap) * index
        arr.push(item.style.top + top)
    }
})
复制代码

例如上图,元素6的top值就是元素3的高度加上元素3的高度。他的left值就是最小列元素的offsetLeft。代码表示如下:

   let minHeight = arr[0];
   let index = 0;
   for (let j = 0; j < arr.length; j++) {
      if (minHeight > arr[j]) {
        minHeight = arr[j];
        index = j;
      }
  }
复制代码

排列剩余元素

我们假设第一行第一个元素是最小的,遍历高度的存储数组,找到高度最小的那个,并且找到他的索引列值,然后开始设置下一个元素的位置

items[i].style.top = `${arr[index] + gap}px`;
items[i].style.left = `${items[index].offsetLeft}px`;
复制代码

设置完新元素的位置后,我们需要更新最小列的高度信息,具体如下:

 arr[index] = arr[index] + items[i].offsetHeight + gap;
复制代码

当所以结果都计算完以后,我们需要更新父元素的高度。因为在文档开头,我们使用了绝对定位,让元素脱离了文档流

 const container = document.querySelector('.container')
 container.style.height = `${max(arr)}px`;
复制代码

以上就是瀑布流的实现,也是网上大部分教程的内容

重排列

但是在实际项目中,我们还需要考虑这个图片的加载过程,因为不定高的图片没有加载完成的时候是拿不到他的高度的。所以我们还需要监听这个图片的加载过程,每次当图片加载完成以后我们都需要对其进行重新排版,所以需要在上述代码中加入:

const images = container.querySelectorAll('img');
      images.forEach(image => {
        image.onload = () => {
          refresh();
        };
      });
复制代码

获取到容器中会影响布局的图片,然后获取他们,监听他们的onload事件,当某个图片加载完成以后,需要重新排列。

此外考虑到DOM结构的动态修改,所以借助MutationObserver实现对DOM结构变化的监听

const observe = new MutationObserver(() => {
      refresh(options);
    });
复制代码

使用flex实现瀑布流

除了使用固定定位外,我们也可以使用flex来实现瀑布流。例如在前面例子中,一共有column列,列与列之间的宽度为gap。

原理

在flex中,我们可以在容器下面放置column个盒子,然后真正需要排列的元素,就放到每个盒子内部。

image.png 下面是DOM的结构

<div className="contianer">
    <div className="flex1">
        {
            list1.map((item) => <img src={item.src}>)
        }
    </div>
    <div classsName="flex2">
         {list2.map((item) => <img src={item.src}>)}
    </div>
    <div classsName="flex3">
         { list3.map((item) => <img src={item.src}>)}
    </div>
</div>
<style>
    .containter {
        display: flex;
    }
</style>
复制代码

在容器下面我们维护了column个列表,在子盒子下面渲染对应的列表,例如第一个盒子就渲染第一个列表,以此类推。大多数市面上的文章就介绍到这里就戛然而止,但是事实上我们需要对提供的列表进行重新组装,保证排列的先后顺序

首先还是单独对第一行进行处理,首先生成column长度的数组去存储每列需要渲染的元素

const arr = Arry.from({length: column}).fill([])
list.forEach((item, index) => {
    if (index < column) {
        arr[0].push(item)
    }
})
复制代码

然后在添加第二行的时候我们仍然需要去获取当前高度最短的列,然后把元素加入对应的列数中。至于最小高度计算和最小高度的列的计算方式和上面绝对定位的方式类似,这里就不一一讲解了

相比较于绝对定位,在最后我们不需要设置容器的高度,因为容器的高度能够被容器内部的元素撑起来的。

总结

对比两种实现方式,核心的内容都是一样的,需要明白如何去计算最小的高度,新加入的元素如何加入到已经的排列结果中的。如果图片的宽高是未知的,还需要去监听图片的宽高进行重新排版。

文章分类
前端
文章标签