最近遇到這麼一個需求,需要在手機上做一個兩列的瀑布流佈局,後來就把這個問題研究了一下,做個記錄。
一般來講,這種佈局可以分為兩種情況:
圖片的數量是一定的,不需要頁面滾動到底部時,再動態載入圖片,只需要將圖片排成若干列
圖片的數量的不定的,頁面觸底時,需要從遠端載入圖片。
前者使用css的方法即可解決,後者則需要js來幫忙。
css解法
一、CSS多列布局
當我們展示的圖片數量一定時,可以優先採用css解法。其中一種方法是藉助css的多欄佈局:
.photos{
column-count: 3;
column-gap: 10px;
}
得到的效果如下:
二、flex佈局
flex佈局同樣可以做到這一點,訣竅在於將flex-direction
設為column
;但是相對於多列布局,需要根據瀑布流的列數,計算一個合適的容器高度,不然可能會導致多出一行。如果你在下面的demo 中,看到了4列,不要懷疑,就是我計算的容器高度不合適導致的。。。
js解法
當圖片需要動態插入時,上面的兩種方法就不合適了,因為他們本質上是將圖片按照縱向進行排列的。圖片動態插入時通常我們希望圖片是按橫向插入到容器中的。這時候就需要js來幫忙了。首先,我們看看瀑布流和揹包問題的關係。
瀑布流的基本思路是將一堆圖片放到若干列中,列與列之間的高度比較均勻,而不會相差太大。假如我們要分成兩列,那麼,問題就變成了,從 n 張圖片中挑出 m 張,使這 m 張圖片的總高度儘量接近 n 張圖片總高度的 1 / 2。於是這就變成了一個揹包問題
01揹包問題
揹包問題是啥這裡不做展開,說白了是將一個複雜的問題分解為幾個簡單的問題,大佬們講的都比我好,網上也有各個語言版本的實現,不太瞭解的同學可以檢視上面的連結。這裡直接放一個函式
function dp(ws, vs, limit) {
let len = ws.length;
let tables = new Array(len).fill().map(x => [])
tables[-1] = new Array(limit + 1).fill(0);
for(let i = 0; i < len; i++) {
for (let w = 0; w <= limit; w++) {
if (ws[i] > w) {
tables[i][w] = tables[i-1][w]
} else {
tables[i][w] = Math.max(tables[i-1][w], tables[i-1][w-ws[i]] + vs[i])
}
}
}
// 回溯得到應該選哪些
let max = limit;
let selected = [];
for (let idx = len - 1; idx >= 0; idx--) {
if (ws[idx] <= max) {
let isSelected = tables[idx-1][max] < tables[idx-1][max-ws[idx]] + vs[idx]
if(isSelected) {
selected.push(idx);
max = max - ws[idx];
}
}
}
return selected;
}
有了這個解法之後,我們也就不難寫出一個瀑布流佈局。具體思路是:假設我們要做一個3列的瀑布流佈局,那麼可以不斷從圖片陣列中選出一組圖片,使圖片的高度接近總高度的1/3,最終得到3組圖片。下面是一個程式碼片段
// colCount 表示要生成幾列
while(colCount--) {
// 獲取被選出的照片索引
let idxs = dp(photoHeights, photoHeights, aver)
// 得到被選出的一組圖片
let photoCol = photos.filter((p,idx) => idxs.includes(idx))
this.cols.push(photoCol)
photoHeights.forEach((v,i) => {
if (idxs.includes(i)) {
photoHeights[i] = null
}
})
}
下面這個demo就是按上面的思路實現的,可以拖動下面的滑塊來改變列數,觀察底部的間隙。在使用揹包演算法解決瀑布流問題時,一個需要我們注意的地方是,要將圖片高度轉化成整數。
參考文章:https://www.cnblogs.com/ainyi/p/8766281.html
https://www.cnblogs.com/AndyJee/p/4543424.html
https://segmentfault.com/a/1190000012829866