Waterfall最常见的实现只适用于子块大小固定或者内部有图片异步加载的情况。对于子块有图片可能引起尺寸变化的情况,通常的做法是硬编码图片的高度,或者在onload事件中检测内部img元素重新排列。由于我们业务中的大小变化比较复杂,比如子块本身的异步初始化和内部数据的异步获取,而这种大小变化的时机是不确定的,为了满足这种需求,我们完成了一个通用的瀑布实现。下面代码部分以Vue.js为例,思路和机制具有普适性。基本瀑布流不考虑子块大小变化的因素,完成基本瀑布流布局功能。基础属性瀑布流布局一共有三种配置,列数columnCount,块的水平间距gutterWidth,块的垂直间距gutterHeight。当然,也可以使用列宽代替列号,但通常这需要用户自己计算列宽,使用成本较高props:{columnCount:Number,gutterWidth:Number,gutterHeight:Number,}基本结构为for类列表的结构在组件开发中通常有两种形式:组件中的循环槽拆分为容器组件和子块组件,组件之间的关联逻辑为如下://Waterfall.vue
//用户——父组件实现思路是用户将列表数据传入组件,组件循环出对应编号的slot,将每一项数据传入slot,用户根据返回的da进行自定义渲染塔。这种方法使用起来有悖常理。从用户的角度,无法直接感受到循环结构,但从开发的角度来看,逻辑更加封闭,更容易实现复杂的逻辑。由于瀑布组件只提供了布局功能,应该能提供更直观的视觉体验。同时,在我们的业务需求中,分块是不一样的,我们需要更灵活的方式来自定义分块的内容。因此采用第二种实现方式,拆分设计,分为Waterfall.vue瀑布容器和WaterfallItem.vue瀑布子块两个组件。//用户//业务组件//业务组件/waterfall>//Waterfall.vueWaterfall.vue组件只需要和父组件一样宽高,里面插入的元素会原样渲染。增删改查为了尽量减少增删子块时重新布局的代价,我选择让WaterfallItem.vue通知WaterfallItem.vue自己的增删改。//Waterfall.vuedata(){return{children:[]}},methods:{add(child){constindex=this.$children.indexOf(child)this.children[index]=childthis.resize(index,true)},delete(child){constindex=this.$children.indexOf(child)this.children[index].splice(index,1)this.resize(index,false)}}//WaterfallItem.vuecreated(){this.$parent.add(this)},destoryed(){this.$parent.delete(this)}那么接下来就是开始写布局逻辑方法了。瀑布流布局受两个因素的影响,每个子块的宽度和高度,我们需要在适当的时候重新获取这两个维度的数据,其中块宽就是列宽。布局元素:列宽列宽受两个因素影响,容器宽度和期望的列数,那么列宽显然是一个计算属性,在初始化和窗口变化时需要重新获取容器宽度。//Waterfall.vuedata(){return{//...containerWidth:0}},computed:{colWidth(){return(this.containerWidth-this.gutterWidth*(cols-1))/this.cols}},方法:{//...getContainerWidth(){this.containerWidth=this.$el.clientWidth}},mounted(){this.getContainerWidth()window.addEventListener('resize',this.getContainerWidth)},destory(){window.removeEventListener('resize',this.getContainerWidth)}也不要忘记在组件被销毁时移除监听器。布局元素:blockheight获取子block高度的时机有两种:获取新添加的block的高度和当列宽变化时重新获取all。data(){return{//...childrenHeights:[]}},resize(index,update){this.$nextTick(()=>{if(!update){this.childrenHeights.splice(index,1)}else{constchildrenHeights=this.childrenHeights.slice(0,index)for(leti=index;i{letcol,left,topconstminHeightCol=colHeights.indexOf(min(colHeights))constminCountCol=colItemCounts.indexOf(min(colItemCounts))if(colHeights[minHeightCol]===0){col=minCountColtop=0}else{col=minHeightColtop=colHeights[col]+this.gutterHeight}colHeights[col]=top+heightcolItemCounts[col]+=1left=(this.colWidth+this.gutterWidth)*colpositions.push({left,top})})consttotalHeight=max(colHeights)返回{positions,totalHeight}},positions(){returnthis.layouts.positions||[]},totalHeight(){返回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.避免无用的“优化”代码。计算出的排列位置和列宽需要应用到WaterfallItem.vue
这样,基本的瀑布流逻辑就结束了。使用现代浏览器单击此处以定期预览高度随机的WaterfallItems并将其插入到Waterfall中。完成初始渲染时限制子块高度固定的瀑布流后,如何做一个子块大小变化时可以感知并重新布局的瀑布流?UniversalWaterfallHowPerceivesSizeChanges根据这篇文章,滚动事件可以用来检测元素的大小变化。简而言之:以scrollTop为例,在滚动方向为右下的前提下,scrollTop已经滚动到最大值,当内容(子元素)的高度固定且大于容器时,容器高度变大,已经滚动到底部,容器只能从上边界向上扩展,上边界到内容区域上边界的距离变小,scrollTop变小触发滚动.当容器高度变小时,容器底边向上收缩,容器上边界到内容区域上边界的距离不变,scrollTop不变,不触发滚动。当内容为容器大小的200%时,当容器高度变大时,内容区域以200%同步变化,有足够的空间让容器向下扩展,所以下边界向下扩展,上边界边界不移动,内容区域上边界到上边界的距离不一样。改变,scrollTop保持不变。当容器高度变小时,内容区域的下边界收缩是容器的两倍,容器下边界收缩的空间不足,导致上边界相对于内容区域上移,scrollTop变小触发滚动。所以我们可以使用:内容区域的大小是固定的并且远大于容器的大小,增加检测容器的大小。内容区域尺寸为容器尺寸的200%,检测容器尺寸缩小。修改那么WaterfallItem.vue需要调整如下