瀑布布局的JavaScript实现方式

瀑布式布局是一种多列等宽不等高的一种页面展示方式,用于图片来源比较复杂,图片尺寸比较复杂时可以使用的一种展示方式,这种展示方式比较优美,让人有种错落有致的感觉.这种展示方式在淘宝的我要买,新浪微博的广场以及蘑菇街等等网站都有应用.这里是我刚刚做的一个小站91美图

实现布局有三个思路:

    o

  1. 最传统的思路,多弄几个容器,分几列,然后往每个列里面插入元素.其实用table分几列实现更加方便:P;
  2. o

  3. 使用html5中css3的多列布局来实现.参见w3c标准中的css3多列布局模块;
  4. o

  5. 使用绝对布局,通过javascript生成元素的布局位置.

前两种方法在网上都有比较详细的介绍,我这里就不再多说了,这里主要说一下我做的第三种实现的优缺点以及我的实现思路.

第三种方案是所有的要布局的元素都是绝对定位的,然后通过javascript来判断每个元素位置,动态设置位置实现布局.

缺点

需要使用javascript来遍历元素,然后要根据前一个元素来判断后一个元素的位置,这样可能对一些老版本的浏览器造成负担,特别是IE6这种老古董,而且在javascript失效的时候,整个页面的布局都会乱掉.另外如果整个页面宽度是变化的,则可能每次窗口尺寸改变时都要重新计算所有元素的位置,在页面中元素较多的时候可能会有闪烁的现象.另外如果页面中出现图片,则需要实现定义好图片的尺寸,否则会出现无法正确计算元素位置的情况.

优点

布局更加灵活,元素绝对定位,可以使用javascript灵活操作.页面宽度改变时可以重新布局整个页面.可以使页面的中的元素真正流动起来,让新添加的元素插入到高度最低的列,使页面的低端更加整齐,对插入的元素高地要求较低.可以较为方便的实现延迟加载.

具体实现

最开始我实现的时候是通过使用javascript生成虚拟的列,根据元素的顺序为每个元素分配一个列和行,然后计算每个元素的位置,举个例子,现在假设有四个列:

使其在页面中布局.事实上这个解决办法跟第一种和第二种是一个道理的.最后页面每列的高度差别可能会很大.

//getElements()方法用于获取页面中的元素
var items = getElements();
var columnCount = 4;
var columnWidth = 230;
var padding = 8;
//遍历所有元素
for(var i = 0, len = items.length; i < len; i++){
o//获取当前的元素
ovar currentItem = items[i];
o//获取当前对象的列
ovar currentColumn = (i + 1) % 4;
o//获取当前对象的行
ovar currentLevel = parseInt(i / 4);

o//有了当前的行跟列可以根据上一层的对象计算出当前对象的高度
ovar left = currentColumn * columnWidth;
ovar top = items[i - 4] ? 0 : items[i - 4].style.top + items[i - 4].clientHeight + padding;
o//设置当前的位置
ocurrentItem.style.top = top + 'px';
ocurrentItem.style.left = left + 'px';
}

代码和逻辑都比较简单,根据当前的行跟列计算出位置就行了.但是这个逻辑还是会出现元素高地差距过大的情况.看一下新浪weibo的广场图片效果:

新浪weibo广超图片列表效果图

新浪weibo广超图片列表效果图

可以看到越到最后可能列高度之间的差距会越大.这不是我们想要实现的效果.

所以我这里换了一个思路,虚拟的列还是要有的,但是行的概念我们抛弃掉,我采用的是一个类似流动的概念,新插入的元素是根据每个列的高度,那个高度最低就流向哪个列,最后确保每个列的高度都趋近一致,实现我们想要的效果.当然我们可以采取获取所有元素的高度,然后统一计算一下,获取一个最佳的排列方法,但是这会给浏览器带来较大的计算量,而且如果不断的加载更多的图片我们会得不偿失,所以我们采用的是一个流动的模型,只让当前对象寻找最低的高度然后插入.

这里我实现了一个Column对象,一个ImgItem对象.Column对象用于维护每一列的信息,包括列的最到高度列宽度等列信息.ImgItem对象保存了一个html节点对象,还有一些设置元素位置,获取当前元素位置的方法.

下面是Column对象的代码:

var Column = function(order){
othis.order = order;
othis.maxHeight = 0;
othis.columnWidth = 230;
othis.left = this.columnWidth * order;
othis.lastItem = null;
othis.positioned = false;
othis.setReferItem = function(item){
othis.lastItem = item;
o}

othis.getHeight = function(){
oif(this.lastItem){
othis.maxHeight = this.lastItem.getBottom();
o}
oreturn this.maxHeight;
o}

othis.getLeft = function(){
oreturn this.left;
o}
};

ImgItem对象的代码:

var ImgItem = function(referNode, column){
othis.referNode = referNode;
othis.bottom = -1;
othis.positioned = false;
};

ImgItem.prototype = {
o/*
o *set the refer node's top
o * @param value: Number
o */
osetTop: function(value){
othis.referNode.style.top = value + 'px';
o},
o/*
o *set the refer node's left
o * @param value: Number
o */
osetLeft: function(value){
othis.referNode.style.left = value + 'px';
o},
o/*
o *get the refer node bottom position
o */
ogetBottom: function(){
oif(this.positioned){
oif(this.bottom < 0){
othis.bottom = parseInt(this.referNode.style.top) + this.referNode.clientHeight;
o}
oreturn this.bottom;
o}else{
othrow("current node has not been positioned!");
o}
o},
osetPosition: function(column){
othis.positioned = true;
othis.setLeft(column.getLeft());
othis.setTop(column.getHeight() + 10);
ocolumn.setReferItem(this);
o}
};

基础打好了,下面要做的就是给元素进行布局了:

//首先根据配置信息中的列数初始化列
for(var i = 0, len = this.config.columnCount; i < len; i++){
othis.columns.push(new Column(i));
}
//获取页面上已存在的对象
var liItems = document.getElementById('img_list').getElementsByTagName('li');
//将所有的对象进行布局
for(i = 0, len = liItems.length; i < len; i++){
othis.addNewItem(liItems[i]);
}

好吧这里还用到了一个addNewItem方法:

getMinHeightColumn: function(){
ovar minHeight = -1, tempColumn = null;
o//遍历所有的列,获取当前高度最低的列,并返回
ofor(var i = 0,len = this.columns.length; i < len; i++){ if(minHeight > this.columns[i].getHeight() || minHeight == -1){
ominHeight = this.columns[i].getHeight();
otempColumn = this.columns[i];
o}
o}

oreturn tempColumn;
},

getMaxHeight: function(){
ovar maxHeight = -1;
o//遍历列对象,获取高度最高的列并返回高度
ofor(var i = 0, len = this.columns.length; i < len; i++){
oif(maxHeight < this.columns[i].getHeight()){
omaxHeight = this.columns[i].getHeight();
o}
o}

oreturn maxHeight;
},
addNewItem: function(liItem){
o//设置元素的位置
ovar imgItem = new ImgItem(liItem);
oimgItem.setPosition(this.getMinHeightColumn());
othis.cachedItems.push();
o//设置容器的高度
odocument.getElementById('img_list').style.height = this.getMaxHeight() + 'px';
}

基本的布局逻辑已经都齐了,还有的就是lazyload的一些逻辑了,这些逻辑都比较通用.加载后布局对象的逻辑是相同的.这里就不再赘述了.

点击下载源文件

抛砖引玉,如有谬误还请不吝指教.

转载请注明原文出处《瀑布布局的JavaScript实现方式》 如无特别声明,所有文章均遵守创作共用 署名-非商业-禁止演绎 3.0协议。

JavaScript与设计模式

这篇文章主要是记录自己的一些想法,同时也能够让刚刚接触编程的人对如何构建健壮易用的代码有所了解,而有丰富编程经验的高手能够给予意见和建议.这里打算把这个议题写成一个系列文章,这篇就算做一个引子,废话可能多一点,见谅:P

模式实际上是生活中很常见的概念,它并不神奇.举个例子,我们迈过一个台阶,这里很自然的你就使用了一种模式,要先抬高一条腿,然后将脚落到前面的台阶上,然后再抬高另外一条腿,放到另外一个台阶上,这个过程很自然的完成了.当然你也可以选择两条腿同时蹦上去,或者加上手,爬上去,但这不是首选的方式.我们迈过台阶的时候实际就是一个迈台阶的模式,你不用考虑很久直接就会迈过去,事实上如果你连迈台阶都要考虑很久的话…可能你的日子就没法正常过了.

程序设计中的模式也是对程序设计中可能碰到的问题的解决方法进行抽象和总结得出的一些范式.你可以在解决典型问题的时候使用相关的模式,相当于站在巨人的肩上,只有基础打好了,后面才会有更好的发展.合理的使用设计模式能够使你构建出健壮,易扩展,易维护的程序.

设计模式方面的资料非常多,但是绝大多数都是针对强类型的语言实现写的,如Java等,而JavaScript则是弱类型的语言,它没有类,接口,抽象类,继承,重载,多态等概念.所以在实现的时候有更多的变通余地,更加灵活,但是随之而来的就是不利于控制.所以编写优秀的JavaScript代码对编程人员的要求反而更高,因为很多东西编译器无法替你检查,你需要自己去控制.

先说类,JavaScript中没有类的概念,但是Function对象可以实现类的大部分功能,它可以作为一个完整的对象来使用,其实我理解的类就是一个有自己属性的,有自己行为的一个抽象,我们没必要拘泥于类的这个概念.这方面的资料很多,这里就不多说了.

再说接口,这个概念JavaScript确实没有,也没类似的代替内容,接口在我理解中其实是签订的一个契约,继承接口的类必须尊重契约做事,至于这个契约是实质的契约还是口头的约定,其形式更大于内容.当然有接口,编译器会在编译的时候发现不遵守契约的坏分子,让其无所遁形,给开发者带来很大的便利.如果所有的类都是君子的话,这个检查就没什么必要了是吧,我相信每个写JavaScript的程序员都是君子:)这里的接口可以是我们的一段注释,我们约定这里必须存在哪些属性,哪些方法,实现的时候就要做到这些,当然我们也可以设计一个额外的功能用于检测目标对象是否真正的实现了相关的方法.

一下是一个简单的实例,我们来定义一个电器类的接口:

code-1:

/*

Interface IElectrialEquipment {

Number Voltage; //额定电压

Number Current; //额定电流

function void TurnOn(); //打开

function void TurnOff(); //关闭

}

*/

我们把这段注释拿来当我们的契约,这里我们不提接口.我们要实现一个实现这个契约的电视机,或者电冰箱.

那我的实现代码应该是这个样子的:

var TV = function(voltage, current){ //Implement IElectrialEquipment
othis.Voltage = voltage;
othis.Current = current;

othis.TurnOn = function(){
o//打开电视机
oconsole.log('电视启动了');
o};
othis.TurnOff = function(){
o//关闭电视机
oconsole.log('电视关闭了');
o};
othis.TurnDown = function(){
o//调低音量
o}
othis.TurnUp = function(){
o//调高音量
o}
}

var Fridge = function(voltage, current){
othis.Voltage = voltage;
othis.Current = current;

othis.TurnOn = function(){
o//打开电冰箱
oconsole.log('冰箱启动了');
o};
othis.TurnOff = function(){
o//关闭电冰箱
oconsole.log('冰箱关闭了');
o};
othis.OpenDoor = function(){
o//打开冰箱门
o};
othis.CloseDoor = function(){
o//关闭冰箱门
o}
}

//现在我们再来做一个万能遥控器
var PowerfulRemoteController = function(IElectrialEquipment){
othis.TurnOn = function(){
oIElectrialEquipment.TurnOn();
o}

othis.TurnOn = function(){
oIElectrialEquipment.TurnOff();
o}

othis.ShowInfo = function(){
oconsole.log('额定电压是: ' + IElectrialEquipment.Voltage + ' 额定电流是: ' + IElectrialEquipment.Current);
o}
}

//现在我要打开冰箱,不然东西全坏了
new PowerfulRemoteController(new Fridge(225, 5)).TurnOn();

//现在我要打开电视看电视,看点新闻联播吧
new PowerfulRemoteController(new TV(225, 2)).TurnOn();

这里如果我们要继续控制我们的微波炉或者电脑,我们只需要按照我们定义好的契约实现相关的对象就可以使用我们的万能遥控器控制了,而且我们的万能遥控器完全不需要修改任何内容,这也就是我们常说的对修改关闭,对扩展开放原则.大家可以在Fx中的命令行里面试一下.但是弊端也有,就是如果某位程序员在实现一个电器的时候没有实现TurnOff或者TurnOn方法,编译器是不会报错的.当然我们的万能遥控器也就失效了.所以这种方法对团队里面的开发人员要求比较高,大家都要是君子,做事情都要按契约来.

事实上我们也可以定义一个检测对象,用检测对象强制检测一下实现的对象中是否含有我们需要的属性或方法,如果没有则报错.

其他的重载多态JavaScript中也没有相应的实现,只能通过一些手段来实现类似的功能,比如重载,JavaScript中后定义的方法会完全覆盖先前定义的方法,这个可以拿来凑合一下当作重载。JavaScript对传入实参的类型和数目没有强制的要求,所以可以通过判断传入的实参的数目和类型来做不同的操作实现类似多态的功能。

转载请注明原文出处《JavaScript与设计模式》 如无特别声明,所有文章均遵守创作共用 署名-非商业-禁止演绎 3.0协议。