瀑布流布局的實現步步升級(原生JS)

吳青強 8年前發布 | 42K 次閱讀 JavaScript開發 JavaScript

瀑布流實現其實已經不是什么新鮮的玩意了,國內外多個展示性網站如花瓣網百度圖片等 都早已采用了瀑布流的頁面布局方式。瀑布流布局巧妙地重排元素并填補了容器的所有空間,適合小數據塊,每個數據塊內容相近且沒有側重。通常,隨著頁面滾動 條向下滾動,這種布局還會不斷加載數據塊并附加至當前尾部。所以,我們給這樣的布局起了一個形象的名字 — 瀑布流布局。

今天心血來潮,決定自己開發一個瀑布流布局,希望能兼容IE6+,而且能實現響應式布局,便在紙上構思其實現邏輯和思路。折騰了一個下午,于是,便有了下文。

HTML和CSS布局

一看到這種不規則的布局,第一時間蹦入腦中的就是父容器相對定位和子元素絕對定位,通過動態定義left和top的值來實現排版。

以下是我的HTML和CSS代碼():

/* 這里用了通配選擇器一鍵清除默認樣式,大家不要學我,一般不建議這樣使用,建議使用Normalize.css完成樣式初始化 */
* {
    margin: 0;
    padding: 0;
    border: 0;
}
.waterfall {
    width: 960px;
    margin: 10px auto;
    position: relative;
}
.waterfall:after, .waterfall:before {
    content: " ";
    display: table;
}
.flow {
    width: 310px;
    background: #333;
    position: absolute;
    border: 1px solid #ccc;
    box-shadow: #cccccc 2px 3px 3px;
    transition: left .5s linear;
    -webkit-transition: left .5s linear;
    -moz-transition: left .5s linear;
    -o-transition: left .5s linear;
}
.flow .flowItem {
    width: 100%;
    font-size: 42pt;
    color: #fff;
    text-align: center;
}
<div class="waterfall" id="waterfall">
    <div class="flow"><div class="flowItem" style="height: 100px;">1</div></div>
    <div class="flow"><div class="flowItem" style="height: 200px;">2</div></div>
    <div class="flow"><div class="flowItem" style="height: 150px;">3</div></div>
    <div class="flow"><div class="flowItem" style="height: 400px;">4</div></div>
    <div class="flow"><div class="flowItem" style="height: 180px;">5</div></div>
    <div class="flow"><div class="flowItem" style="height: 120px;">6</div></div>
    <div class="flow"><div class="flowItem" style="height: 300px;>7</div></div>
    <div class="flow"><div class="flowItem" style="height: 100px;">8</div></div>
    <div class="flow"><div class="flowItem" style="height: 120px;">9</div></div>
    <div class="flow"><div class="flowItem" style="height: 105px;">10</div></div>
    <div class="flow"><div class="flowItem" style="height: 180px;11</div></div>
    <div class="flow"><div class="flowItem" style="height: 120px;">12</div></div>
    <div class="flow"><div class="flowItem" style="height: 300px;">13</div></div>
    <div class="flow"><div class="flowItem" style="height: 100px;">14</div></div>
    <div class="flow"><div class="flowItem" style="height: 120px;">15</div></div>
    <div class="flow"><div class="flowItem" style="height: 105px;">16</div></div>
</div>

快掩住我的眼,亮瞎了,什么鬼東西哇,丑死咧!!!

莫急,下面我們好好來分析一下用JS動態分配定位元素的left和top值,重新排版一下。

JS實現一:測試版,僅為了實現而實現

為了測試,我先擬定了3列布局,每一個瀑布流元素塊的寬度為310px,元素塊之間的垂直和水平間距均為15px,經過我的人工計算,包裹容器的寬度剛好960px。

首先,我要思考的是,要如何實現定位呢?一開始的時候,我想著,要不比較當前的塊元素的前三個元素的offsetTop和offsetHeight得到最矮的一列,然后根據這一列定位當前元素。很快,這個想法實現起來漏洞百出,而且不合邏輯,deny掉啦。

這種局部的過程是什么?就是當前元素排布在最矮的列上啊。如果我們能動態獲取當前最矮的列高以及第幾列,不就能準確定位當前元素了嗎?那么問題來 了,怎么獲取當前最矮的列高和列序號呢?把每一列的高度存在一個數組里面,每次定位完當前元素,都實時修改當前定位的列高,那樣每一次比較都能獲得準確的 最矮列了。原理就這么簡單。

那么如何快速獲取一個數組中最小的值呢?最簡單的方法之直接調用Math.min()方法Math.min.apply(null, myArray)。當然也可以用循環遞歸來實現判斷,但是使用原生的Math方法性能會更高一些。

直接上代碼(因為用到querySelectorAll()和indexOf(),所以不能兼容IE6~8):

 var waterfall = document.getElementById("waterfall");
 var flowItems = waterfall.querySelectorAll(".flow");

 // 簡單版(只兼容至IE9,寬度、列數固定)
 // 共3列,每一列的寬度固定為310px,元素塊之間的水平和垂直間距均為15px;瀑布流包含塊的寬度為960px;

 // 聲明瀑布流中每一列高度的數組pin[]
 var pin = [];
 pin[0] = flowItems[0].offsetTop + flowItems[0].offsetHeight;
 pin[1] = flowItems[1].offsetTop + flowItems[1].offsetHeight;
 pin[2] = flowItems[2].offsetTop + flowItems[2].offsetHeight;
 // 循環瀑布流元素的高度
 for(var i = 0, len = flowItems.length; i < len; i++) {
     if(i >= 3) {
         // 獲取三個數中的最小值
         var minH = Math.min.apply(null, pin);
         // 獲取高度數組中最小高度的索引
         var minHItem = pin.indexOf(minH);
         // 把當前元素在視覺上置于最小高度的一列
         flowItems[i].style.left = minHItem * (310 + 15) + "px";
         flowItems[i].style.top = minH + 15 + "px";
         // 重置列的高度
         pin[minHItem] += flowItems[i].offsetHeight + 15;
         }else if(i < 3){
         flowItems[i].style.top = 0;
         flowItems[i].style.left = (i % 3) * (310 + 15) + "px";
     }
 }

效果圖如下:
瀑布流布局的實現步步升級(原生JS)

在線DEMO請戳這里~

無疑,如你所見,耦合度高得無法直視,因為一些參數都是寫死的,復用性較差。我們可以把一些參數抽離出來,把函數封裝成獨立的模塊。見下面升級版的方法。

升級版:參數抽離和解決低版本IE的兼容性

重新對瀑布流定位函數進行了封裝。并且把一些可自定義的參數抽離出來,由于參數數量較多,所以使用對象來存儲數據。這樣,我們就能自定義修改參數獲得多種布局方式啦。

由于上面的實現方法中因為使用了querySelectorAll()和indexOf()函數而導致不能兼容IE6~8。在這里,我重寫了這兩個函數實現,讓低版本瀏覽器也能愉快地打開網站。

JS代碼見下:

 var waterfallParent = document.getElementById("waterfall");
 var flowItems = getClassName(waterfallParent, "flow");
 // 定義瀑布流布局參數,如下:
 // parent:瀑布流包裹容器,類型為DOM對象;floowItems:瀑布流布局子元素組,類型為DOM對象數組;pin:列數,類型為int;
 // width:每個瀑布流布局元素的寬度,類型為int;horizontalMargin:元素塊之間的水平間距,類型為int;
 // verticalMargin:元素塊之間的垂直間距,類型為int;
 var currentFlow = {
 parent: waterfallParent,
 flowItems: flowItems,
 pin: 4,
 width: 310,
 horizontalMargin: 15,
 verticalMargin: 15
 };

 waterfall(currentFlow);

 // 其中flow是一個對象,分別包含如下鍵值:
 // parent:瀑布流包裹容器,類型為DOM對象;floowItems:瀑布流布局子元素組,類型為DOM對象數組;pin:列數,類型為int;
 // width:每個瀑布流布局元素的寬度,類型為int;horizontalMargin:元素塊之間的水平間距,類型為int;
 // verticalMargin:元素塊之間的垂直間距,類型為int;
 function waterfall(flow) {
     // 聲明瀑布流中每一列高度的數組pin[]
     var pin = new Array(flow.pin);
     // 瀑布流框塊數組
     var flowItems = flow.flowItems;
     // 聲明每一列高度的初始值
     for(var i = 0, pinLen = pin.length; i < pinLen; i++) {
         pin[i] = flowItems[i].offsetTop + flowItems[i].offsetHeight;
     }
     // 循環瀑布流元素的高度
     for(var i = 0, len = flowItems.length; i < len; i++) {
         if(flow.width) {
             flowItems[i].style.width = flow.width + "px";
         }

         if(i >= flow.pin) {
             // 獲取pin數組中的最小值
             var minH = Math.min.apply(null, pin);
             // 獲取高度數組中最小高度的索引
             var minHItem = pin.indexOf(minH);
             // 把當前元素在視覺上置于最小高度的一列
             flowItems[i].style.left = minHItem * (flow.width + flow.horizontalMargin) + "px";
             flowItems[i].style.top = minH + flow.verticalMargin + "px";
             // 重置列的高度
             pin[minHItem] += flowItems[i].offsetHeight + flow.verticalMargin;
         }else if(i < flow.pin){
             flowItems[i].style.top = 0;
             flowItems[i].style.left = (i % flow.pin) * (flow.width + flow.horizontalMargin) + "px";
         }
     }
     // 計算瀑布流容器的寬度
     flow.parent.style.width = flow.pin * flow.width + (flow.pin - 1) * flow.horizontalMargin + "px";

 }

 // 獲取className的元素集合
 // 參數:obj指父元素;oClassName為元素的class屬性值
 function getClassName(obj, oClassName) {
     // IE9+及標準瀏覽器可以直接使用getElementsByClassName()獲取className元素集合
     if(document.getElementsByClassName) {
        return obj.getElementsByClassName(oClassName);
     }else {
         // classNameArr用來裝載class屬性值為oClassName的元素;
         var classNameArr = [];
         // 獲取obj的直接子元素
         var objChild = obj.children || obj.childNodes;
         // 遍歷obj元素,獲取class屬性值為oClassName的元素列表
         for(var i = 0; i < objChild.length; i++) {
         // 判斷obj子元素的class屬性值中是否含有oClassName
         if( hasClassName(objChild[i], oClassName) ) {
         classNameArr.push(objChild[i]);
         }
     }
     return classNameArr;
     }
 }

 // Array.indexOf()函數的兼容性重寫
 if (!Array.prototype.indexOf) {
     Array.prototype.indexOf = function(ele) {
         // 獲取數組長度
         var len = this.length;
         // 檢查值為數字的第二個參數是否存在,默認值為0
         var fromIndex = Number(arguments[1]) || 0;
         // 當第二個參數小于0時,為倒序查找,相當于查找索引值為該索引加上數組長度后的值
         if(fromIndex < 0) {
            fromIndex += len;
         }
        // 從fromIndex起循環數組
         while(fromIndex < len) {
             // 檢查fromIndex是否存在且對應的數組元素是否等于ele
             if(fromIndex in this && this[fromIndex] === ele) {
                 return fromIndex;
             }
             fromIndex++;
         }
         // 當數組長度為0時返回不存在的信號:-1
         if (len === 0) {
            return -1;
         }
     }
 }

在線DEMO請戳這里~

這種實現似乎很完美了,至少現在的我覺得還算OK。但是由于我們現在做的網站大多都是響應式布局的,而以上的JS實現都是固定瀑布流容器寬度和列數的,顯然并不能滿足需求。

響應式版

有了上面封裝好的可自定義列數的瀑布流布局函數,下面的實現就輕松多啦。我們可以檢測當前設備的寬度,并根據探測的設備寬度決定當前排布的列數以及瀑布流包裹容器的寬度。

在這里,我聲明的響應斷點是1200px, 960px, 767px 和320px。

具體代碼見下:

// 超升級版(列數和每一列的寬度、元素塊之間的邊距為不定值,兼容IE6~8,實現響應式布局)
var waterfallParent = document.getElementById("waterfall");
var flowItems = getClassName(waterfallParent, "flow");
// 聲明瀑布流浮動參數
// parent:瀑布流包裹容器,類型為DOM對象;floowItems:瀑布流布局子元素組,類型為DOM對象數組;pin:列數,類型為int;
// width:每個瀑布流布局元素的寬度,類型為int;horizontalMargin:元素塊之間的水平間距,類型為int;
// verticalMargin:元素塊之間的垂直間距,類型為int;
var currentFlow = {
    parent: waterfallParent,
    flowItems: flowItems,
    pin: 4,
    width: 310,
    horizontalMargin: 15,
    verticalMargin: 15
};

// 聲明響應式的響應斷點
var deviceWidth = {
    D: 1200,
    C: 960,
    B: 767,
    A: 320
};

// 響應式瀑布流布局繪制
window.onresize = responseFlow;
responseFlow();
function responseFlow() {
    var deviceW;
    // 判斷當前的設備屏幕寬度
    function checkDeviceW() {
        var screenW = document.documentElement.offsetWidth || document.body.offsetWidth;
        if(screenW >= deviceWidth.A && screenW < deviceWidth.B) {
            deviceW = "A";
        }else if(screenW >= deviceWidth.B && screenW < deviceWidth.C) {
            deviceW = "B";
        }else if(screenW >= deviceWidth.C && screenW < deviceWidth.D) {
            deviceW = "C";
        }else if(screenW >= deviceWidth.D) {
            deviceW = "D";
        }
    }
    checkDeviceW();

    // 修改不同響應下瀑布流布局的列數
    switch(deviceW) {
        case "A":
            currentFlow.pin = 1;
            break;
        case "B":
            currentFlow.pin = 2;
            break;
        case "C":
            currentFlow.pin = 3;
            break;
        case "D":
            currentFlow.pin = Math.floor(currentFlow.parent.offsetWidth / currentFlow.width);
            break;
    }
    // 瀑布流重繪
    waterfall(currentFlow);
}

// 其中flow是一個對象,分別包含如下鍵值:
// pin:列數,類型為int;
function waterfall(flow) {
    // 聲明瀑布流中每一列高度的數組pin[]
    var pin = new Array(flow.pin);
    // 瀑布流框塊數組
    var flowItems = flow.flowItems;
    // 聲明每一列高度的初始值
    for(var i = 0, pinLen = pin.length; i < pinLen; i++) {
        pin[i] = flowItems[i].offsetTop + flowItems[i].offsetHeight;
    }
    // 循環瀑布流元素的高度
    for(var i = 0, len = flowItems.length; i < len; i++) {
        if(flow.width) {
            flowItems[i].style.width = flow.width + "px";
        }

        if(i >= flow.pin) {
            // 獲取pin數組中的最小值
            var minH = Math.min.apply(null, pin);
            // 獲取高度數組中最小高度的索引
            var minHItem = pin.indexOf(minH);
            // 把當前元素在視覺上置于最小高度的一列
            flowItems[i].style.left = minHItem * (flow.width + flow.horizontalMargin) + "px";
            flowItems[i].style.top = minH + flow.verticalMargin + "px";
            // 重置列的高度
            pin[minHItem] += flowItems[i].offsetHeight + flow.verticalMargin;
        }else if(i < flow.pin){
            flowItems[i].style.top = 0;
            flowItems[i].style.left = (i % flow.pin) * (flow.width + flow.horizontalMargin) + "px";
        }
    }
    // 計算瀑布流容器的寬度
    flow.parent.style.width = flow.pin * flow.width + (flow.pin - 1) * flow.horizontalMargin + "px";
}

// 獲取className的元素集合
// 參數:obj指父元素;oClassName為元素的class屬性值
function getClassName(obj, oClassName) {
    // IE9+及標準瀏覽器可以直接使用getElementsByClassName()獲取className元素集合
    if(document.getElementsByClassName) {
        return obj.getElementsByClassName(oClassName);
    }else {
        // classNameArr用來裝載class屬性值為oClassName的元素;
        var classNameArr = [];
        // 獲取obj的直接子元素
        var objChild = obj.children || obj.childNodes;
        // 遍歷obj元素,獲取class屬性值為oClassName的元素列表
        for(var i = 0; i < objChild.length; i++) {
            // 判斷obj子元素的class屬性值中是否含有oClassName
            if( hasClassName(objChild[i], oClassName) ) {
                classNameArr.push(objChild[i]);
            }
        }
        return classNameArr;
    }
}

// Array.indexOf()函數的兼容性重寫
if (!Array.prototype.indexOf) {
    Array.prototype.indexOf = function(ele) {
        // 獲取數組長度
        var len = this.length;
        // 檢查值為數字的第二個參數是否存在,默認值為0
        var fromIndex = Number(arguments[1]) || 0;
        // 當第二個參數小于0時,為倒序查找,相當于查找索引值為該索引加上數組長度后的值
        if(fromIndex < 0) {
            fromIndex += len;
        }
        // 從fromIndex起循環數組
        while(fromIndex < len) {
            // 檢查fromIndex是否存在且對應的數組元素是否等于ele
            if(fromIndex in this && this[fromIndex] === ele) {
                return fromIndex;
            }
            fromIndex++;
        }
        // 當數組長度為0時返回不存在的信號:-1
        if (len === 0) {
            return -1;
        }
    }
}

效果如下:
瀑布流布局的實現步步升級(原生JS)

在線DEMO請戳這里~

總結

還可以用Ajax動態加載數據,以實現數據源源不斷加載的效果,篇幅太長,分下一篇文章寫,就醬紙。

這種實現方法有一個十分不好的地方就是:一旦用戶禁止了JavaScript或者JavaScript未加載完成,就不能正常顯示頁面內容。另外, 由于布局采用父容器相對定位和子元素絕對定位,而絕對定位會使元素脫離文檔流,從而導致父容器高度塌陷。所以,一般常見的處理辦法是將瀑布流布局置于頁面 的尾部,或者動態獲取父容器的高度。

總的來說,雖然網上有很多瀑布流布局的插件和實現方法,而且實現原理也比較簡單。但是親自實踐就會發現一些技巧和難點譬如如何獲取數組中的最小值、動態判斷屏幕寬度等都是很值得思考和優化的。

喜歡就拿去用吧。源碼在此~

來自:http://www.dengzhr.com/js/405

Save

 

 本文由用戶 吳青強 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!