前端之瀑布流布局(多種實(shí)現(xiàn)方案)

瀑布流布局,一般指根據(jù)內(nèi)容高度自適應(yīng)填充到某一列以使整體頁面和諧,常見的有圖片網(wǎng)站,比如每一行每一列的內(nèi)容是錯(cuò)開的。

瀑布流布局一般是下面這個(gè)樣子

PC:
PC端

移動(dòng)端:
image.png

Tips:本文字?jǐn)?shù)稍長,思路清晰,耐心讀下來絕對讓你掌握瀑布流布局!
下面介紹三種實(shí)現(xiàn)方案,推薦大家仔細(xì)看看第一種

方案一: js實(shí)現(xiàn),css略微輔助

在線演示: https://cdpn.io/lilylaw/fullpage/mdJpdLp

目標(biāo)要求:
  1. 實(shí)現(xiàn)瀑布流
  2. PC和移動(dòng)端都要適配
  3. 圖片不可變形,寬度固定
  4. 瀑布流的列數(shù)不固定,隨屏幕寬度的變化而變化
  5. 若屏幕寬度有富余,則圖片瀑布流區(qū)域居中展示。
思路分析
  1. 實(shí)現(xiàn)瀑布流思路如下:
    (1)根據(jù)圖片寬度,計(jì)算當(dāng)前屏幕一共有幾列。
    舉例:當(dāng)前屏幕寬度是820px,每一個(gè)包裹著圖片的盒子寬度是200px,所以一共可以排820/200 = 4 列(向下取整)
    (2)先按順序排第一列,記錄下每一列當(dāng)前占據(jù)的高度值。
    舉例:假設(shè)當(dāng)前屏幕寬度為820px,每一個(gè)包裹著圖片的盒子寬度是200px,可以排4列,現(xiàn)在有10個(gè)圖片高度分別是[120,55,800,380,77,20,130,600,453,780] (單位px),先排前4個(gè)圖片,排完之后這4列高度依次是[120,55,800,380],把這4列當(dāng)前高度值存起來。
    (3)第一列排完之后開始排后面的元素,要找出所有列數(shù)中高度最小的那一個(gè),排在它的下面,高度值累加。以此類推。
    舉例:接(2)中的例子,現(xiàn)在這4列高度依次是[120,55,800,380],可以看到第2列高度最小,所以第5個(gè)圖片排在第2列,緊挨著第2列的第1個(gè)圖片下。然后第2列的高度變成了 55+77 = 132 ,當(dāng)前這4列的高度變成了[120,132,800,380]。剩下的圖片也按照這個(gè)邏輯去排就ok了。
  2. PC和移動(dòng)端適配在html中配置如下:
  <meta name='viewport' content='width=device-width,initial-scale=1.0,user-scalable=no' />
  1. 圖片不變形,寬度固定
    html中的 img不是塊級元素,它屬于行內(nèi)替換元素,雖然width、height等屬性仍然有效,但非塊狀元素在用css或js修飾時(shí)會(huì)遇到很多不可預(yù)測的問題。基于此,我習(xí)慣給每一個(gè)img用一個(gè)div包裹住,以后直接操作外層div,如下
<div style="width:200px;height:auto">
  <img src="xxxxx" alt='xxxxx' style="width:100%;height:auto"/>
</div>
  1. 第4個(gè)要求說白了是要監(jiān)聽瀏覽器窗口寬度變化,然后重新布局,所以給窗口對象添加一個(gè)resize 監(jiān)聽事件就ok了
  window.addEventListener('resize',()=>{
    // 重新布局
  })
  1. 圖片區(qū)域居中顯示:如果針對單個(gè)圖片去計(jì)算位置以使整個(gè)區(qū)域居中顯示,這種方法計(jì)算量大,得不償失。所以將所有圖片包在一個(gè)div里,使最外層div居中就ok了。
代碼
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>瀑布流布局</title>
    <meta name='viewport' content='width=device-width,initial-scale=1.0,user-scalable=no' />
    <style>
        html{
            font-size: 12px;
        }
        *{
            margin: 0;
            padding: 0;
        }
    </style>
</head>
<body>
    <div class="wrap">
        <div id='forcenter'></div>
    </div>
    <script src="./waterfall.js"></script>
    <script>
        let mwf = new WaterFall({
            id: 'forcenter',
            imgUrls: [
                'https://icweiliimg6.pstatp.com/weili/l/195564498297815041.webp',
                'https://weiliicimg9.pstatp.com/weili/l/461972764095807490.webp',
                'https://icweiliimg9.pstatp.com/weili/l/447251256884854785.webp',
                'https://icweiliimg6.pstatp.com/weili/l/165755629970653210.webp',
                'https://weiliicimg6.pstatp.com/weili/l/218593064967209016.webp',
                'https://icweiliimg6.pstatp.com/weili/l/320841925450530819.webp',
                'https://icweiliimg1.pstatp.com/weili/l/165755655736786957.webp',
                'https://icweiliimg1.pstatp.com/weili/l/452540045247774720.webp',
                'https://weiliicimg9.pstatp.com/weili/l/223888416044482620.webp',
                'https://icweiliimg9.pstatp.com/weili/l/292051943627161624.webp',
                'https://icweiliimg1.pstatp.com/weili/l/235158650747486283.webp',
                'https://weiliicimg1.pstatp.com/weili/l/165755681505542155.webp',
                'https://icweiliimg1.pstatp.com/weili/l/202821919289376781.webp',
                'https://weiliicimg9.pstatp.com/weili/l/611415730252415145.webp'
            ]
        });

        window.onload = ()=>{
            mwf.WFRender();
        }

        window.onresize = ()=>{
            mwf.resizeWF();
        }
    </script>
</body>
</html>
// waterfall.js

function WaterFall(obj){
    this.id = obj.id;
    this.imgUrls = obj.imgUrls;
    this.container = document.getElementById(this.id);
    this.container.className = 'forcenter';
}

WaterFall.prototype = {
    WFRender: function(){
        let flag = 0;   // 標(biāo)志位,用于檢測每一張圖片都加載完畢

    this.imgUrls.map((item,i)=>{
      let div = document.createElement('div');
      div.className = 'item';

      let img = new Image();
      img.src = item;
      img.onload = () => {
        flag++;
        div.appendChild(img);
        this.container.appendChild(div);

                // 全部圖片加載完畢后再開始布局,否則獲取不到元素的最終高度。
        if(flag===this.imgUrls.length){
          this.resizeWF();
        }
      }
        });

    let styleE = document.createElement('style');
    styleE.innerHTML = `div.item{
        position: absolute;
        transition: all .5s;  /* 使過渡平滑*/
        width : 200px;
        height: auto;
        padding:5px;
        box-sizing: border-box;  /* 非常重要,可將不必要的計(jì)算略去 */
      }
      div.item img{
        width: 100%;
        height: auto;
      }
      .forcenter{
        position: relative;
        margin: auto;
      }`;
     this.container.appendChild(styleE);
    },
    resizeWF: function(){
        let winW = window.innerWidth;
    let itemNum = Math.floor(winW/200); // 當(dāng)每一個(gè)項(xiàng)的寬度都是固定的時(shí)候,需要計(jì)算出瀏覽器一行可以排列幾個(gè)。
    this.container.style.width = (itemNum*200)+'px'; // 用于居中的包裹盒子的寬度

    let saveColumnHeight = [];  // 定義一個(gè)數(shù)組,用于存儲(chǔ) 每一列所有元素的高度 之和
    let items = document.querySelectorAll('.item');

    for(let i=0; i< items.length; i++){
        if(saveColumnHeight.length<itemNum){
            saveColumnHeight[i] = items[i].offsetHeight;// 當(dāng)布局的元素還沒占滿一行時(shí),繼續(xù)向數(shù)組中添加第一行第i列的高度
            setDiv(items[i],200*i,0);    // 放置div
        }else{  // 當(dāng)已經(jīng)占滿一行時(shí),就找出每一列的最小高度,然后當(dāng)前的這個(gè)div放在高度最小的那一列
            let pos = getMinH(saveColumnHeight);    //去找高度最小的那一列
            saveColumnHeight[pos.column] += items[i].offsetHeight;
            setDiv(items[i],pos.left,pos.top);    // 放置div
        }
    }
    }
}

// 工具性函數(shù),不必放入原型鏈。
function getMinH(arr){
  let flag = {
      left:0,
      top:0,
      column:0
  }

  arr.map((item,i)=>{
      if(flag.top===0){
          flag.top = item;
      }else{
          if(flag.top>item){  // 找出高度最小的那一列
              flag.top = item;
              flag.left = 200*i;
              flag.column = i;
          }
      }
  });

  return flag;
}

function setDiv(item,left,top){
  item.style.left = left+'px';
  item.style.top = top+'px';
}
Tips
  1. 在布局之前,確保所有圖片已加載完畢,否則無法獲取最終高度導(dǎo)致布局出錯(cuò)。
  2. css中有一個(gè)屬性box-sizing,它的作用是將元素的borderpadding都計(jì)算在元素尺寸之內(nèi)(詳情請自己查閱),這么設(shè)置有一個(gè)好處:平常我們布局為了避免圖片緊挨著不好看經(jīng)常加一些paddingmargin,因此計(jì)算過程就復(fù)雜了。box-sizing屬性可以將padding包含進(jìn)圖片尺寸,通過padding使圖片避免緊挨著,在計(jì)算上只考慮最外層尺寸就ok了,省了很多事。

方案二 純css

目標(biāo)要求
  1. 實(shí)現(xiàn)瀑布流樣式
  2. PC和移動(dòng)端都要適配
  3. 圖片不變形
  4. 瀑布流列數(shù)自適應(yīng)
思路分析

css中有這么兩個(gè)屬性:

  • column-count 將元素劃分的列數(shù),若為auto,則自適應(yīng)
  • column-gap 元素之間的間隙
  • column-width 每一列的寬度
    使用這三個(gè)屬性基本可以實(shí)現(xiàn)啦!
代碼
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>瀑布流布局</title>
    <meta name='viewport' content='width=device-width,initial-scale=1.0,user-scalable=no' />
    <style>
        html{
            font-size: 12px;
        }
        *{
            margin: 0;
            padding: 0;
        }
        div.item{
            transition: all .5s;
            /*width : 200px;*/
            height: auto;
            padding:5px;
            box-sizing: border-box;
        }
        .wrap{
            column-width: 200px;
            column-count: auto;
            column-gap: 1rem;
        }
    </style>
</head>
<body>
<div class="wrap" id='wrap'>

</div>
<script>
    var imgUrls = [
        'https://icweiliimg1.pstatp.com/weili/l/452540045247774720.webp',
        'https://weiliicimg9.pstatp.com/weili/l/223888416044482620.webp',
        'https://icweiliimg6.pstatp.com/weili/l/320841925450530819.webp',
        'https://icweiliimg6.pstatp.com/weili/l/195564498297815041.webp',
        'https://weiliicimg9.pstatp.com/weili/l/461972764095807490.webp',
        'https://icweiliimg1.pstatp.com/weili/l/165755655736786957.webp',
        'https://icweiliimg9.pstatp.com/weili/l/447251256884854785.webp',
        'https://icweiliimg6.pstatp.com/weili/l/165755629970653210.webp',
        'https://weiliicimg6.pstatp.com/weili/l/218593064967209016.webp',
        'https://icweiliimg9.pstatp.com/weili/l/292051943627161624.webp',
        'https://icweiliimg1.pstatp.com/weili/l/235158650747486283.webp',
        'https://weiliicimg1.pstatp.com/weili/l/165755681505542155.webp',
        'https://icweiliimg1.pstatp.com/weili/l/202821919289376781.webp',
        'https://weiliicimg9.pstatp.com/weili/l/611415730252415145.webp'
    ];

    window.onload = function(){
        let container = document.getElementById('wrap');

        imgUrls.map((item,i)=>{
            let div = document.createElement('div');
            div.className = 'item';

            let img = document.createElement('img');
            img.src = item;
            img.style.width = '100%';
            img.style.height = 'auto';

            div.appendChild(img);
            container.appendChild(div);
        });
    }

</script>
</body>
</html>

注意

其實(shí)這樣弄出來的根本不是嚴(yán)格意義上的瀑布流布局,只不過有一個(gè)瀑布流布局的樣子而已,因?yàn)閷Ω叨鹊呐袛嗖蝗鏹s計(jì)算來的嚴(yán)格,所以會(huì)出現(xiàn)一些不容易被注意到的問題(我也是測試了好久才找到),如下圖

image.png

這也是我推薦大家用第一種方法的最主要原因。

方案三 flex布局

這種方案做出來的效果有點(diǎn)奇葩,特殊的應(yīng)用場景可能會(huì)用到,最終效果是橫向瀑布流
flex布局中有一個(gè)屬性flex-flow,指明在哪個(gè)方向上在長度不夠的情況想拆行,詳情大家自己查閱

代碼
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>瀑布流布局</title>
    <meta name='viewport' content='width=device-width,initial-scale=1.0,user-scalable=no' />
    <style>
        html{
            font-size: 12px;
        }
        *{
            margin: 0;
            padding: 0;
        }
        div.item{
            transition: all .5s;
            width : 200px;
            height: auto;
            padding:5px;
            box-sizing: border-box;
        }
        .wrap{
            display: flex;
            flex-flow: column wrap;
            height: 400px;
        }
    </style>
</head>
<body>
<div class="wrap" id='wrap'>

</div>
<script>
    var imgUrls = [
        'https://icweiliimg1.pstatp.com/weili/l/452540045247774720.webp',
        'https://weiliicimg9.pstatp.com/weili/l/223888416044482620.webp',
        'https://icweiliimg6.pstatp.com/weili/l/320841925450530819.webp',
        'https://icweiliimg6.pstatp.com/weili/l/195564498297815041.webp',
        'https://weiliicimg9.pstatp.com/weili/l/461972764095807490.webp',
        'https://icweiliimg1.pstatp.com/weili/l/165755655736786957.webp',
        'https://icweiliimg9.pstatp.com/weili/l/447251256884854785.webp',
        'https://icweiliimg6.pstatp.com/weili/l/165755629970653210.webp',
        'https://weiliicimg6.pstatp.com/weili/l/218593064967209016.webp',
        'https://icweiliimg9.pstatp.com/weili/l/292051943627161624.webp',
        'https://icweiliimg1.pstatp.com/weili/l/235158650747486283.webp',
        'https://weiliicimg1.pstatp.com/weili/l/165755681505542155.webp',
        'https://icweiliimg1.pstatp.com/weili/l/202821919289376781.webp',
        'https://weiliicimg9.pstatp.com/weili/l/611415730252415145.webp'
    ];

    window.onload = function(){
        let container = document.getElementById('wrap');

        imgUrls.map((item,i)=>{
            let div = document.createElement('div');
            div.className = 'item';

            let img = document.createElement('img');
            img.src = item;
            img.style.width = '100%';
            img.style.height = 'auto';

            div.appendChild(img);
            container.appendChild(div);
        });
    }

</script>
</body>
</html>

總結(jié)

第一種方法其實(shí)是最合適的,也是對開發(fā)人員要求最高的,即使有思路與算法在開發(fā)過程中也會(huì)踩很多坑,比如(1)圖片預(yù)加載,(2)querySelector取元素居然獲取不到,(3)使用box-sizing簡化計(jì)算過程降低復(fù)雜性,等等。box-sizing 屬性用好了非常高效,推薦大家去仔細(xì)看看

第二種方法其實(shí)也可行,但是不推薦
第三種方法就有點(diǎn)扯了,估計(jì)不常用到,大家看看就行

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容