常见的瀑布流实现大部分只适用于子块尺寸固定或内部有图片异步加载的情况。
而对于子块有图片这种可能引起尺寸变化的情况,通常的做法是写死图片高度,或检测内部的 img 元素从而在 onload 事件中进行重排。
由于我们业务中尺寸变化情况更为复杂,如子块本身异步初始化、内部数据异步获取,且这种尺寸变化时机不可确定,为满足这种需求所以调研完成了一个通用万能的瀑布流实现。
先不考虑子块尺寸变化的因素,完成基础的瀑布流布局功能。
瀑布流布局的配置有三个,列数 columnCount,块水平间距 gutterWidth、块垂直间距 gutterHeight。
当然也可是使用列宽代替列数,但通常情况下,这样就要求使用方进行列宽计算,有更高的使用成本
props: {
columnCount: Number,
gutterWidth: Number,
gutterHeight: Number,
}
对于类列表的结构,在组件开发中通常由两种形式:
组件内循环 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)
}
}
布局思路如下:
// 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 最大值前提下
当内容(子元素)高度固定且大于容器时
当内容为 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>
// Waterfall.vue
methods: {
// ...
update (child) {
const index = this.$children.indexOf(child)
this.childrenHeights.splice(index, 1, this.$children[index].$el.getBoundingClientRect().height)
}
}
在父组件中只需要更新此元素的高度即可,自会触发后续布局计算。
至此,可动态感知尺寸变化的万能瀑布流也就完成了,使用现代浏览器点此预览
预览中定时修改部分 WaterfallItem 的字体大小,从而触发子块尺寸变化,触发重新布局。
在以上实现之外还可以做一些其他优化,如:
按需监听尺寸变化,对 WaterfallItem 组件添加新的 props,如:
原文:https://segmentfault.com/a/1190000021254906
瀑布流作为当前比较流行的一种网页布局方式,在视觉上呈现出参差不齐、琳琅满目、唯美的视觉效果,该布局随着页面滚动,数据不断加载并附加至当前页面的尾部。这篇文章主要介绍关于vue框架中常使用的瀑布流组件
瀑布流布局是种常见的布局方式,常用于图片相关的样式展示,通过CSS3的多列(Multi-column)属性,可以简单的实现类似效果。设置外部容器多列列数(column-count)和列间距(column-gap)
是一种常见的网页布局方式,在许多网站中我们都能看到“瀑布流”的效果,其特征是有网页视窗有多个高度不同宽度相同的“块”组成。因其样式酷似飞流直下的瀑布,故将这种布局方式称为瀑布流。
瀑布流 又称瀑布流式布局,是比较流行的一种网站页面布局方式。即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次按照规则放入指定位置。
瀑布流布局中的图片有一个核心特点 —— 等宽不定等高,瀑布流布局在国内网网站都有一定规模的使用,比如pinterest、花瓣网等等。那么接下来就基于这个特点开始瀑布流探索之旅。
又称瀑布流式布局,是比较流行的一种网站页面布局方式。即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高,根据图片原比例缩放直至宽度达到我们的要求,依次按照规则放入指定位置。
用vue来实现一个瀑布流效果,加载网络图片,同时有下拉刷新和上拉加载更多功能效果。然后针对这几个效果的实现,捋下思路:根据加载数据的顺序,依次追加标签展示效果;
瀑布流又称瀑布流式布局,是比较流行的一种网站页面布局方式。视觉表现为参差不齐的多栏布局,即多行等宽元素排列,后面的元素依次添加到其后,等宽不等高。基本思路就是利用wx:if和数组的下标对2取余来判断是排在左列还是排在右列
JavaScript实现简单的图片瀑布流插件,功能:1).可以自动根据浏览器视口宽度,改变图片瀑布流的宽度2).添加了函数防抖功能
依赖 column 便可实现最简单实用的瀑布流布局,我这里前端框架用的是 Vue, 用其他的也一样,column-count: 3; 内容均分三份
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!