欢迎来到 jackNEss'窝窝
I like simple mind

瀑布流组件 f2e_waterfall

2012年08月05日

最近公司频道页改版,变成了各种瀑布流,其中搞笑频道的改版由我操刀,心中窃喜….终于有机会接触瀑布流了,哈哈,经过3天外加一个通宵的努力,组件终于诞生了。

瀑布流须知:

Pinterest创造的瀑布流样式,有几个预设前提:

  1. 图片极重要,文字不重要;
  2. 用户浏览无明确目的,对复杂的索引无依赖性;
  3. 图片整体美观度较高。

因此恰恰适合Pinterest这样的“好图挖掘与收藏网站”。最近几个月国内跟风瀑布流,也太盲目了些。至于我接手的这次改版..嗯,如果那些图能经过编辑修饰一下,估计勉强还适合用瀑布流的…

瀑布流功能点分析

当然在我们制作控件之前,我们必须搞清楚我们的控件需要实现什么?

假设我们称瀑布流内的各个子模块为格子。

交互方面

  • 瀑布流格子的插入方式为插入到瀑布流各列中高度最小的那一列的里面。
  • 瀑布流的排列方式可以是从左到右排列or从右到左排列。
  • 瀑布流中除了一般格子之外,还有可能存在特殊格子。
  • 滚动到瀑布流 底部/瀑布流各列中最小高度的那个的底部+-N px 的时候自动加载。
  • 无限加载/加载到一定次数停止。
  • 如果格子高度发生改变,那么格子之下的格子位置必须顺延跟着发生改变。
  • 浏览器宽度发生改变,如果这个瀑布流是全屏布满的排列方式时,在这个宽度中可容纳的格子个数发生改变时,格子重排。
  • 浏览器宽度发生改变时,如果这个瀑布流是全屏布满的排列方式时,这些格子整体保持居中。

瀑布流实现方式

站在前人的肩膀上,我们可以看得更远。基于这个原则,在我开工之前,也查了下网上瀑布流的实现方法,大概分为2种:

  1. 通过绝对定位瀑布流内模块实现
  2. N列布局方法

由于个人比较喜欢类似 花瓣网 的那种 在chrome下能做到 模块由四周飘到目标位置上的效果,为了实现这种效果,我选择使用绝对定位来进行瀑布流处理。(我擦,咋我写这文章的时候那效果木有鸟?!我记得明明有的..花瓣网)

瀑布流的实现——绝对定位实现原理

选择用这种方式实现,我们必须有一颗处变不惊的心,与心思细密的思维,因为这种方式实现的计算量之大,大得在你敲完整整一套计算模块高度,位置的公式方法按F5之后,出错了你还得排查半天才找到到底是哪里算错了。

在制作过程中我们遇到的功能点

以下是我在制作的时候需要停顿思考的几个功能点问题

  • 如何给格子排列?
  • 当浏览器宽度发生改变的时候,如何让格子整体保持居中?
  • 如何使用JSONP读取数据?
  • 如何更好地将读取过来的数据拼装到我们的格子里面?
  • 由于我们加载的图片必须完成加载才能获得该图片的宽高,那如何检测图片是否加载完成?
  • 由于这个是滚动瀑布流内各列中的最大高度or最小高度的时候,触发再加载事件,我们通常会用onscroll来检测是否滚动到触发再加载事件的高度以上,但是就会出现一种情况:图片加载完,滚动条高度也到达了可触发再加载事件的高度了,但是必须再滚一下才能触发。如何防止这种情况?
  • 由于瀑布流触发加载方式的特殊性,如何避免瞬间触发多次加载的情况?
  • 格子在加载完成并排列到正确位置上之后,如果由于他本身触发的某些事件而使格子高度发生改变时,应该如何进行格子重排?
  • 加载格子的过度效果如何做得自然一些?

下面我就一一解释一下我是如何解决这些问题的

功能点实现

当浏览器宽度发生改变的时候,如何让格子整体保持居中?

理论上的实现是当改变的时候,全部格子的left值都重新计算一次,但是我觉得这样的运算量太大了,所以我用在格子外层套多一个div,用来定位格子整体的left值,免去了庞大的计算量:

<div class="outset" style="left:213px">
  <div class="cell">格子</div>
  <div class="cell">格子</div>
  <div class="cell">格子</div>
  <div class="cell">格子</div>
</div>>

如何给格子排列?

在考虑如何实现这个功能之前,我们必须要知道的是,瀑布流格子的插入方式为插入到瀑布流各列中高度最小的那一列的里面。

我在这里的做法是,新建一个用于储存格子位置信息的二维数组,里面储存的正好是我们瀑布流的每一列格子的index信息。

在这里,我会使用一个数组,用来储存每一列的高度。

假设现在有 5 列的格子,那么我们新增格子需要插入第几列是这样的算法:

//js document
//假设我们每行的列高如下
var columnHeights = [0,12,50,148,62,254];
var minHeightColumn = 0;
var minHeight = columnHeights[minHeightColumn];

for(var i = 0, len = columnHeights.length; i < len; i++){
  var fh = columnHeights[i];
  minHeight > fh? (
    minHeight = fh,
	minHeightColumn = i
  ):"";
}

这样获得的 minHeightColumn 就是我们新增格子要插入的列数了。

假设我们格子的插入顺序是这样:

那么我新建的二维数组就是以下这样的结构:

//js document
var sitemap = [];
sitemap[0] = [1,10,14];
sitemap[1] = [2,7,12];
sitemap[2] = [3,6,11];
sitemap[3] = [4,8,13];
sitemap[4] = [5,9,15];

这样做的好处其实是在我们计算得出我们新增的格子是要插入在那一列中的时候,可以很快地就能算出新增格子的top、left值。

假如我们新增格子插入的是第一列,那么新增格子的top、left值为:

//js document
//假设这个就是我们需要新增的格子
var newCell;
//假设这个是我们格子之间的间隔值
var columnGap = 10; 
//假设这个是我们规定每个格子的width值
var columnWidth = 320;
//假设这个是我们需要插入的列数
var col = 0;
var nowColumnSitemap = sitemap[col];
//瀑布流内的所有格子
var cells = wfArea.children;
var nowColumnLastCell = (nowColumnSitemap.length > 0? cells[nowColumnSitemap[nowColumnSitemap.length - 1]]:null);

newCell.style.top = (nowColumnLastCell? parseInt(nowColumnLastCell.style.top) + nowColumnLastCell.offsetHeight) + columnGap + "px";
newCell.style.left = col * ( columnWidth + columnGap ) + "px";

//别忘了给新增格子所在列的高度增加
minHeights[col] += columnGap + newCell.offsetHeight;

如何使用JSONP读取数据?

原理我在这里就不再多做介绍了,具体可参考我的这篇文章 JSONP实现跨域数据传输

要给JSONP设置一个加载时限,防止一些网速慢的用户一旦加载不到数据,我们的网站也得提供些后备方案来处理。

当我们请求的JSONP报错或者请求失败时,在浏览我们该页面的用户是完全不知道情况的,因为页面呈现的还是 loading 正在加载的样式,所以我们必须为用户制作一个计时器,当这个操作超时的时候,执行后备方案。

//js document;

var jsonKey;
//这个是一般情况下执行的正常方案
var successCallback;
//这个是超时之后启用的后备方案
var errorCallback;
//假设我们超时设置为8秒
var timeout = 8000;

jsonKey = setTimeout(function(){
  successCallback = null;
  errorCallback();
},timeout);
successCallback = function(){
  clearTimeout(jsonKey);
  //..执行正常流程的函数
}

如何更好地将读取过来的数据拼装到我们的格子里面?

由于这种东西的结构其实是存在经常改动和新增动态元素的可能,所以我决定使用前端模板来实现。

在这里我使用{%key%}这样的形式为我们动态的部分,其他为静态。
其中这个key值为对应json数据中的值,例如 {%data%} 相当于json数据 jsonData 中的:
jsonData.data;

var jsonData = {
	"data":521,
	"name":"jackNEss"
}

假如我的前端模板是这样:

var boxModule=[
	'<div class="wf_box">',
		'<div class="wf_imgbox">',
			'<a href="#" title="{%title%}"><img src="http://www.jackness.org/wp-content/themes/JStyle/images/default/blank.png" _src="{%imageHref%}" alt="{%title%}"/></a>',
		'</div>',
		'<h3 class="wf_tl"><a href="#">{%title%}</a></h3>',
		'<p class="wf_details">{%details%}</p>',
	'</div>'
].join("");

json数据如此:

jsonData = {
	"title":"格子模板标题",
	"imageHref":"01.jpg",
	"details":"我是格子模板详情"
}

那么我们解析用函数就是:

//前端模块生成
function moduleRebuild(moduleStr,jsonData){
	var result = moduleStr.replace(otherAttr.reg,function(){
		var fword = arguments[0],
			attr = fword.substring(2,fword.length -2);
		return jsonData[attr]||"";
	});
	return result;
};

由于我们加载的图片必须完成加载才能获得该图片的宽高,那如何检测图片是否加载完成?

在这里我们会碰到的情况是:1图片完全没加载过;2图片已经在浏览器缓存里面,已经加载完成的。

鉴于这2种情形,下面是我整合的图片加载检测控件:

/*
	 * img loader
	 * exp: jns.imgAJAXLoader(elm,"xxx.jpg",function(){alert("img loaded."),function(){alert("img load error")}})
	 * date: 2012-7-29
	 * ver 3.1
	 */
	jns.imgAJAXLoader = function(target,src,callback,onerror,timeout){
		if(!target ||target.tagName !="IMG"||typeof src !="string"){return;}

		var dc = document,
			onloadEvent = function(){},
			onErrorEvent = function(){},
			timeoutKey,
			itimeout = 8000;

		typeof callback == "function"? onloadEvent = callback:"";
		typeof onerror == "function"? onErrorEvent = onerror:"";
		typeof timeout == "number"? itimeout = timeout:"";
		
		/*--
		if(target.src == src && target.complete){
			return;
		};
		--*/

		var felm = dc.createElement("img");
		felm.style.cssText = [
			";position:absolute",
			";left:0",
			";top:-9999px",
			";visibility:hidden",
			";display:block",
			";zoom:1",
			";margin:0",
			";padding:0",
			";width:100%",
			";height:auto",
			";float:left;",
			";border:0"
		].join("");
		felm.src = src;
		dc.body.appendChild(felm);
		if(felm.complete){
			completeHandle.call(felm);
			return;
		};

		timeoutKey = setTimeout(function(){
			felm.onreadystatechange = null;
			felm.onload = null;
			onErrorEvent.call(target);
			completeHandle.call(felm);
		},itimeout);

		if(felm.readyState){
			
			felm.onreadystatechange = function(){
				if( this.readyState == "complete" ){
					clearTimeout(timeoutKey);
					this.onreadystatechange = null;
					completeHandle.call(this);
				}
			}
			if(this.readyState == "complete"){
				clearTimeout(timeoutKey);
				completeHandle.call(this);
				this.onreadystatechange = null;
			}

		}
		else{
			felm.onload = function(){
				clearTimeout(timeoutKey);
				completeHandle.call(this);
			}
		};		

		function completeHandle(){
			this.style.width = "auto"; //这个修复用于 ie6 加载图片完成后 图片默认大小有时候会是 28x30...蛋疼吧
			this.style.height = "auto";
			target.src = src;
			var iSrc = src;
			var iWidth = this.offsetWidth;
			var iHeight = this.offsetHeight;
			if(this){
				dc.body.removeChild(this);
			}
			onloadEvent.apply(target,[iSrc,iWidth,iHeight]);
		};

	};
处理图片加载失败的情况

和JSONP的超时处理一样,由于这个图片加载超时涉及到我们模块大小的正常显示,所以当他超时的时候,我们必须要给这个图片设定一个固定的高度,使我们该模块能正常地显示在页面上。

由于这个是滚动瀑布流内各列中的最大高度or最小高度的时候,触发再加载事件,我们通常会用onscroll来检测是否滚动到触发再加载事件的高度以上,但是就会出现一种情况:图片加载完,滚动条高度也到达了可触发再加载事件的高度了,但是必须再滚一下才能触发。如何防止这种情况?

这个就是在新增格子添加成功之后,检测是否滚动条已经到达了可执行再加载的位置,如果是,执行再加载事件。

由于瀑布流触发加载方式的特殊性,如何避免瞬间触发多次加载的情况?

在这里,我使用一个小小的 setTimeout 来把再加载事件延迟来避免这种情况发生。

//js document
function slideLoad(){
  clearTimeout(arguments.callee.timeoutKey);
  timeoutKey = setTimeout(function(){
    //加载事件
  },200);
}

格子在加载完成并排列到正确位置上之后,如果由于他本身触发的某些事件而使格子高度发生改变时,应该如何进行格子重排?

首先我们要知道这个格子改变之后,其实影响的是和他同一列安插在他后面的格子,所以我们重新定位的就应该是这些格子。然后就是计算了。

这个是我做的组件的demo http://www.jackness.org/lab/2012/waterfall/demo.html

版本更新 v3

在应用在项目的时候,SEO这边反馈说这个瀑布流不利于搜索蜘蛛的抓取,所以在这个版本添加了个功能,就是如果瀑布流内部原先有模块的话,自动将里面的模块当作是瀑布流里面的格子进行排列。另外一点就是如果瀑布流中的格子里面的图片如果给他设置了一个高度的话,即 <img height=”500″ /> 的话,瀑布流将不等图片完全加载出来就优先显示在页面上。

JS地址:http://www.jackness.org/lab/2012/waterfall/js/f2e_waterfall_v.3.js

demo地址:http://www.jackness.org/lab/2012/waterfall/demo_v.3.html

分类javascript
标签,
阅读 914
  • 评论加载中...

标签云

分类目录

最新留言

  • 评论加载中...

与我联系

如有疑问or建议可通过以下方式跟我取得联系.

Q Q:373435871
Email:jackness1208@gmail.com
© Copyright 2011 - 2014 jackNEss.org All Rights Reserved 粤ICP备14065612号
首页 | 关于我 | 网站地图 | RSS