万能瀑布流

更新日期: 2019-12-10阅读: 2.3k标签: 瀑布流

常见的瀑布流实现大部分只适用于子块尺寸固定或内部有图片异步加载的情况。

而对于子块有图片这种可能引起尺寸变化的情况,通常的做法是写死图片高度,或检测内部的 img 元素从而在 onload 事件中进行重排。

由于我们业务中尺寸变化情况更为复杂,如子块本身异步初始化、内部数据异步获取,且这种尺寸变化时机不可确定,为满足这种需求所以调研完成了一个通用万能的瀑布流实现。

以下代码部分以 vue.js 为例,思路和机制是通用的。


基础瀑布流

先不考虑子块尺寸变化的因素,完成基础的瀑布流布局功能。

基本属性

瀑布流布局的配置有三个,列数 columnCount,块水平间距 gutterWidth、块垂直间距 gutterHeight。

当然也可是使用列宽代替列数,但通常情况下,这样就要求使用方进行列宽计算,有更高的使用成本
props: {
  columnCount: Number,
  gutterWidth: Number,
  gutterHeight: Number,
}

基本结构

对于类列表的结构,在组件开发中通常由两种形式:

  1. 组件内循环 slot
  2. 拆分为容器组件和子块组件,组件间做关联逻辑

组件内循环 slot 的方式如下:

// Waterfall.vue
<template>
  <div>
    <slot v-for="data in list" v-bind="data">
  </div>
</template>
// 使用方--父级组件
<waterfall :list="list">
  <template v-slot="data">
    <ecology-card :ecology-info="data" />
  </template>
</waterfall>

其实现思路是,使用者将列表数据传入组件,组件内部循环出对应个数的 slot,并将每一项数据传入 slot,使用方根据传回的数据进行自定义渲染。

这种方式使用起来比较违反视觉直觉,在使用者角度,不能直接的感受到循环结构,但开发角度,逻辑更封闭,实现复杂逻辑更为简便。

由于瀑布流组件只提供布局功能,应提供更直观的视觉感受,同时在我们的业务需求中,子块部分不尽相同,需要更灵活的自定义子块内容的方式。

所以采取第二种实现方式,拆分设计为 Waterfall.vue 瀑布流容器和 WaterfallItem.vue 瀑布流子块两个组件。

// 使用方
<waterfall>
  <waterfall-item>
    <a-widget /> // 业务组件
  </waterfall-item>
  <waterfall-item>
    <b-image /> // 业务组件
  </waterfall-item>
</waterfall>
// Waterfall.vue
<script>
  render (h) {
    return h('div', this.$slots.default)
  }
<script>

<style>
.waterfall {
  position: relative;
  width: 100%;
  min-height: 100%;
  overflow-x: hidden;
}
</style>

Waterfall.vue 组件只需要与父组件同宽高,并且将插入内部的元素原样渲染。

增删

为了保证在新增或删除子块时使重新布局的成本最小化,我选择由 WaterfallItem.vue 告知 Waterfall.vue 自己的新增和移除。

// Waterfall.vue
data () {
  return {
    children: []
  }
},
methods: {
  add (child) {
    const index = this.$children.indexOf(child)
    this.children[index] = child
    this.resize(index, true)
  },
  delete (child) {
    const index = this.$children.indexOf(child)
    this.children[index].splice(index, 1)
    this.resize(index, false)
  }
}
// WaterfallItem.vue
created () {
  this.$parent.add(this)
},
destoryed () {
  this.$parent.delete(this)
}

那么下面就要开始进行布局逻辑方法的编写。

瀑布流布局受两个因素影响,每个子块的宽和高,我们需要在适当的时候重新获取这两个维度的数据,其中块宽即列宽。

布局要素:列宽

列宽受两个因素的影响,容器宽度和期望的列数,那么列宽明显就是一个计算属性,而容器宽度需要在初始化和窗口变化时重新获取。

// Waterfall.vue
data () {
  return {
    // ...
    containerWidth: 0
  }
},
computed: {
  colWidth () {
    return (this.containerWidth - this.gutterWidth * (cols -1))/this.cols
  }
},
methods: {
  //...
  getContainerWidth () {
    this.containerWidth = this.$el.clientWidth
  }
},
mounted () {
  this.getContainerWidth()
  window.addEventListener('resize', this.getContainerWidth)
},
destory () {
  window.removeEventListener('resize', this.getContainerWidth)
}

也不要忘记在组件销毁时移除监听。

布局要素:块高

子块高的获取时机有两个:获取新增的块的高度和列宽变化时重新获取所有。

data () {
  return {
    //...
    childrenHeights: []
  }
},
resize (index, update) {
  this.$nextTick(() => {
    if (!update) {
      this.childrenHeights.splice(index, 1)
    } else {
      const childrenHeights = this.childrenHeights.slice(0, index)
      for (let i = index; i < this.children.length; i++) {
        childrenHeights.push(this.$children[i].$el.getBoundingClientRect().height)
      }
      this.childrenHeights = childrenHeights
    }
  })
},
watch: {
  colWidth () {
    this.resize(0, true)
  }
}
  • 在删除块时只需要删除对应块 dom 的尺寸,不需要更新其他块的高度。
  • 新增块或列宽变化时,子块 DOM 未必实际渲染完成,所以需要添加 $nextTick 等待 DOM 的实际渲染,从而可以获得尺寸。
  • 列宽变化时,重新获取所有块的高度。

布局计算

布局思路如下:

  1. 记录每列的高度,取最短的列放入下一个块,并更新此列高度。
  2. 如果最短的列高度为 0,那么取块最少的列为目标列,因为可能块高为 0,块垂直间距为 0,导致一直向第一列添加块。
  3. 在此过程中根据列数和列宽获取每个块的布局位置。
// Waterfall.vue
computed: {
  //...
  layouts () {
    const colHeights = new Array(this.columnCount).fill(0)
    const colItemCounts = new Array(this.columnCount).fill(0)
    const positions = []
    this.childrenHeights.forEach(height => {
      let col, left, top
      const minHeightCol = colHeights.indexOf(min(colHeights))
      const minCountCol = colItemCounts.indexOf(min(colItemCounts))
      if (colHeights[minHeightCol] === 0) {
        col = minCountCol
        top = 0
      } else {
        col = minHeightCol
        top = colHeights[col] + this.gutterHeight
      }
      colHeights[col] = top + height
      colItemCounts[col] += 1
      left = (this.colWidth + this.gutterWidth) * col
      positions.push({ left, top })
    })
    const totalHeight = max(colHeights)
    return {
      positions,
      totalHeight
    }
  },
  positions () {
    return this.layouts.positions || []
  },
  totalHeight () {
    return this.layouts.totalHeight || 0
  }
}

同时需要注意的一点是,在整个布局的高度发生改变的时候,可能会伴随着滚动条的出现和消失,这会引起布局区域宽度变化,所以需要对 totalHeight 增加监听。

watch: {
  totalHeight () {
    this.$nextTick(() => {
      this.getContainerWidth()
    })
  }
}

当 totalHeight 发生变化时,重新获取容器宽度,这也是为什么 getContainerWidth 方法中使用 clientWidth 值的原因,因为 clientWidth 不包含滚动条的宽度。

同时在 totalHeight 发生改变后要使用 $nextTick 后获取宽度,因为 totalHeight 是我们的计算值,此刻,布局数据变化引发的视图渲染还未发生,在 $nextTick 回调等待视图渲染更新完成,再获取 clientWidth。

同时我们也不需要关注 totalHeight(newValue, oldValue) 中 newValue 和 oldValue 是否相等,来而避免后续计算,因为若相等是不会触发 totalHeight 的 watch 行为的。

同理,也不需要判断 totalHeight 变化前后 clientWidth 是否一致来决定是否要对 containerWidth 重新赋值,从而避免引发后续的列宽、布局计算,因为 Vue.js 内都做了优化,只需重新获取并赋值,避免无用的“优化”代码。

排列

计算完成的位置和列宽需要应用到 WaterfallItem.vue 上

<template>
  <div class="waterfall-item" :style="itemStyle">
    <slot />
  </div>
</template>

<script>
export default {
  created () {
    this.$parent.add(this)
  },
  computed: {
    itemStyle () {
      const index = this.$parent.$children.indexOf(this)
      const { left, top } = this.$parent.positions[index] || {}
      const width = this.$parent.colWidth
      return {
        transform: `translate3d(${left}px,${top}px,0)`,
        width: `${width}px`
      }
    }
  },
  destoryed () {
    this.$parent.delete(this)
  }
}
</script>

<style>
.waterfall-item {
  box-sizing: border-box;
  border: 1px solid black;
  position: absolute;
  left: 0;
  right: 0;
}
</style>

结果

至此,基础瀑布流逻辑也就结束了,使用现代浏览器点此预览

预览中定时向 Waterfall 中插入高度随机的 WaterfallItem。

完成限定子块高度在初始渲染时就固定的瀑布流后,怎么能做一个无论什么时候子块尺寸变化,都能进行感知并重新布局的瀑布流呢?


万能瀑布流

如何感知尺寸变化

根据这篇文章知,可以利用滚动事件去探知元素的尺寸变化。

简要来说:

以 scrollTop 为例,在滚动方向为向右和向下,已经滚动到 scrollTop 最大值前提下

  • 当内容(子元素)高度固定且大于容器时

    • 容器高度变大时,已滚动到最下方,容器只能上边界向上扩展,上边界到内容区上边界距离变小,scrollTop 变小触发滚动。
    • 容器高度变小时,容器底边向上缩小,容器上边界到内容区上边界距离不变,scrollTop 不变,不触发滚动。
  • 当内容为 200% 的容器尺寸时

    • 容器高度变大时,内容区 200% 同步变化,容器向下扩展空间充足,所以下边界向下扩展,上边界不动,上边界到内容区上边界距离不变,scrollTop 不变。
    • 当容高度变小时,内容区下边界二倍于容器收缩,容器下边界收缩空间不足,导致上边界相对内容区上移,scrollTop 变小触发滚动。

所以我们可以使用:

  • 内容区尺寸固定且远大于容器尺寸,检测容器的尺寸增大。
  • 内容区尺寸为容器尺寸的 200%,检测容器的尺寸减小。

改动

那么 WaterfallItem.vue 需要调整如下

<template>
  <div class="waterfall-item" :style="itemStyle">
    <div class="waterfall-item__shadow" ref="bigger" @scroll="sizeChange">
      <div class="waterfall-item__holder--bigger">
      </div>
    </div>
    <div class="waterfall-item__shadow" ref="smaller" @scroll="sizeChange">
      <div class="waterfall-item__holder--smaller">
      </div>
    </div>
    <slot />
  </div>
</template>

<script>
  mounted () {
    this.$nextTick(() => {
      this.$refs.bigger.scrollTop = '200000'
      this.$refs.smaller.scrollTop = '200000'
    })
  },
  methods: {
    sizeChange () {
      this.$parent.update(this)
    }
  }
</script>

<style>
  .waterfall-item {
    position: absolute;
    left: 0;
    right: 0;
    overflow: hidden;
    box-sizing: border-box;
    border: 1px solid black ;
  }
  .waterfall-item__shadow {
    height: 100%;
    left: 0;
    overflow: auto;
    position: absolute;
    top: 0;
    transform: translateX(200%);
    width: 100%;
  }
  .waterfall-item__holder--bigger {
    height: 200000px;
  }
  .waterfall-item__holder--smaller {
    height: 200%;
  }
</style>
  • slot 为用户的真实 DOM,其撑开 waterfall-item 的高度。
  • 两个分别检测尺寸增加和减小的 waterfall-item__shadow 与 waterfall-item 同高,从而使得用户 DOM 的尺寸变化映射到 waterfall-item__shadow 上。
  • 渲染完成后使 waterfall-item__shadow 滚动到极限位置。
  • 用户 DOM 的尺寸变化触发 waterfall-item__shadow 的 scroll 事件,在事件回调中通知 Waterfall.vue 组件更新对应子块高度。
// Waterfall.vue
methods: {
  // ...
  update (child) {
    const index = this.$children.indexOf(child)
    this.childrenHeights.splice(index, 1, this.$children[index].$el.getBoundingClientRect().height)
  }
}

在父组件中只需要更新此元素的高度即可,自会触发后续布局计算。

结果

至此,可动态感知尺寸变化的万能瀑布流也就完成了,使用现代浏览器点此预览

预览中定时修改部分 WaterfallItem 的字体大小,从而触发子块尺寸变化,触发重新布局。


优化

在以上实现之外还可以做一些其他优化,如:

  1. 通知 Waterfall.vue 添加的 add 和更新的 update 方法调用有重复(覆盖)触发的情况,可以合并。
  2. 按需监听尺寸变化,对 WaterfallItem 组件添加新的 props,如:

    • 固定大小的就可以不绑定 scroll 监听,且不渲染 waterfall-item__shadow。
    • 只会变化一次的可以对监听使用 once,并在后续更新时不再渲染 waterfall-item__shadow。
  3. 在布局计算完成前对 WaterfallItem 添加不可见 visibility: hidden。
  4. 在元素过多时,使用虚拟渲染的方式,只渲染在视图范围内的 WaterfallItem。
  5. 对应 keep-alive 的路由渲染,在非激活状态是拿不到容器尺寸的,所以需要在 activated 和 deactivated 中进行重新布局的停止和激活,避免错误和不必要的开支。

原文:https://segmentfault.com/a/1190000021254906


链接: https://fly63.com/article/detial/6805

vue实现瀑布流布局的组件/插件总汇

瀑布流作为当前比较流行的一种网页布局方式,在视觉上呈现出参差不齐、琳琅满目、唯美的视觉效果,该布局随着页面滚动,数据不断加载并附加至当前页面的尾部。这篇文章主要介绍关于vue框架中常使用的瀑布流组件

CSS3实现瀑布流布局

瀑布流布局是种常见的布局方式,常用于图片相关的样式展示,通过CSS3的多列(Multi-column)属性,可以简单的实现类似效果。设置外部容器多列列数(column-count)和列间距(column-gap)

js实现无限瀑布流

是一种常见的网页布局方式,在许多网站中我们都能看到“瀑布流”的效果,其特征是有网页视窗有多个高度不同宽度相同的“块”组成。因其样式酷似飞流直下的瀑布,故将这种布局方式称为瀑布流。

瀑布流的布局原理分析(纯CSS瀑布流与JS瀑布流)

瀑布流 又称瀑布流式布局,是比较流行的一种网站页面布局方式。即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次按照规则放入指定位置。

原生JS实现一个瀑布流插件

瀑布流布局中的图片有一个核心特点 —— 等宽不定等高,瀑布流布局在国内网网站都有一定规模的使用,比如pinterest、花瓣网等等。那么接下来就基于这个特点开始瀑布流探索之旅。

纯CSS瀑布流与JS瀑布流

又称瀑布流式布局,是比较流行的一种网站页面布局方式。即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次按照规则放入指定位置。

vue实现网络图片瀑布流 + 下拉刷新 + 上拉加载更多

用vue来实现一个瀑布流效果,加载网络图片,同时有下拉刷新和上拉加载更多功能效果。然后针对这几个效果的实现,捋下思路:根据加载数据的顺序,依次追加标签展示效果;

小程序实现瀑布流和上拉加载

瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。视觉表现为参差不齐的多栏布局,即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高。基本思路就是利用wx:if和数组的下标对2取余来判断是排在左列还是排在右列

JavaScript实现简单的图片瀑布流插件

JavaScript实现简单的图片瀑布流插件,功能:1).可以自动根据浏览器视口宽度,改变图片瀑布流的宽度2).添加了函数防抖功能

css实现瀑布流布局

依赖 column 便可实现最简单实用的瀑布流布局,我这里前端框架用的是 Vue, 用其他的也一样,column-count: 3; 内容均分三份

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!