瀑布流佈局是一種比較流行的頁面佈局方式,表現爲參差不齊的多欄卡片。跟網格佈局相比,顯得更靈動,更具藝術氣息。
實現瀑布流佈局的方式有多種,比如multi-column
佈局,grid
佈局,flex
佈局等。但是這些實現方式都有各自的侷限性,代碼也略複雜。
其實,有個最原始、最簡單,也是兼容性最好的實現方式,那就是使用絕對定位
。瀑布流佈局的元素是一些等寬不等高的卡片,只要根據元素的實際寬高計算出自己的座標位置就行了。
要計算座標自然要用到 JavaScript,這就不是純 CSS 方案,對某些前端極客來講顯得不那麼純粹。不過只要理清思路了,也用不了幾行代碼。本文就給出最近實現的一個版本。
// 計算每個卡片的座標
export function calcPositions({ columns = 2, gap = 7, elements }) {
if (!elements || !elements.length) {
return [];
}
const y = []; //上一行卡片的底部縱座標數組,用於找到新卡片填充位置
const positions = []; // 每個卡片的座標數組
elements.forEach((item, index) => {
if (y.length < columns) { // 還未填滿一行
y.push(item.offsetHeight);
positions.push({
left: (index % columns) * (item.offsetWidth + gap),
top: 0
});
} else {
const min = Math.min(...y); // 最小縱座標
const idx = y.indexOf(min); // 縱座標最小的卡片索引
y.splice(idx, 1, min + gap + item.offsetHeight); // 替換成新卡片的縱座標
positions.push({
left: idx * (item.offsetWidth + gap),
top: min + gap
});
}
});
// 由於採用絕對定位,容器是無法自動撐開的。因此需要計算實際高度,即最後一個卡片的top加上自身高度
return { positions, containerHeight: positions[positions.length - 1].top + elements[elements.length - 1].offsetHeight };
}
上面這段代碼的作用就是計算每個卡片的left
、top
,以及容器的總高度。關鍵位置都有註釋,應該不難理解。
有了這幾行核心代碼,要想封裝成瀑布流組件就很容易了。以 Vue 爲例,可以這樣封裝:MasonryLite.vue
<template>
<div class="masonry-lite">
<slot></slot>
</div>
</template>
<script>
import { calcPositions } from './index.js';
export default {
name: 'MasonryLite',
props: {
gap: {
type: Number,
default: 12,
},
columns: {
type: Number,
default: 2,
},
},
data() {
return {};
},
mounted() {
this.doLayout();
},
methods: {
doLayout() {
const children = [...this.$el.querySelectorAll('.masonry-item')];
if (children.length === 0) {
return;
}
const { positions, containerHeight } = calcPositions({
elements: children,
columns: this.columns,
gap: this.gap,
});
children.forEach((item, index) => {
item.style.cssText = `left:${positions[index].left}px;top:${positions[index].top}px;`;
});
this.$el.style.height = `${containerHeight}px`;
},
},
};
</script>
<style lang="scss" scoped>
.masonry-lite{
position: relative;
}
.masonry-item {
position: absolute;
}
</style>
使用組件:
<MasonryLite>
<div class="product-card masonry-item" v-v-for="(item, index) in items" :key="index">
<img :src="item.imageUrl" />
<header>{{ item.title }}</header>
</div>
</MasonryLite>
不過這樣其實還會有點問題,就是doLayout
的執行時機。因爲該方案基於絕對定位,需要元素在渲染完成後才能獲取到實際寬高。如果卡片內有延遲加載的圖片或者其他動態內容,高度會發生變化。這種情況下就需要在DOM更新後主動調用一次doLayout
重新計算佈局。
如果大家有更好的實現方案,歡迎交流!
如果覺得對你有幫助,幫忙點個不要錢的star。
本文分享自微信公衆號 - 1024譯站(trans1024)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。