實(shí)現(xiàn)瀑布流布局,就這幾行代碼?
瀑布流布局是一種比較流行的頁(yè)面布局方式,表現(xiàn)為參差不齊的多欄卡片。跟網(wǎng)格布局相比,顯得更靈動(dòng),更具藝術(shù)氣息。
瀑布流布局
實(shí)現(xiàn)瀑布流布局的方式有多種,比如multi-column布局,grid布局,flex 布局等。但是這些實(shí)現(xiàn)方式都有各自的局限性,代碼也略復(fù)雜。
其實(shí),有個(gè)最原始、最簡(jiǎn)單,也是兼容性最好的實(shí)現(xiàn)方式,那就是使用絕對(duì)定位。瀑布流布局的元素是一些等寬不等高的卡片,只要根據(jù)元素的實(shí)際寬高計(jì)算出自己的坐標(biāo)位置就行了。
要計(jì)算坐標(biāo)自然要用到 JavaScript,這就不是純 CSS 方案,對(duì)某些前端極客來(lái)講顯得不那么純粹。不過(guò)只要理清思路了,也用不了幾行代碼。本文就給出最近實(shí)現(xiàn)的一個(gè)版本。
- // 計(jì)算每個(gè)卡片的坐標(biāo)
 - export function calcPositions({ columns = 2, gap = 7, elements }) {
 - if (!elements || !elements.length) {
 - return [];
 - }
 - const y = []; //上一行卡片的底部縱坐標(biāo)數(shù)組,用于找到新卡片填充位置
 - const positions = []; // 每個(gè)卡片的坐標(biāo)數(shù)組
 - elements.forEach((item, index) => {
 - if (y.length < columns) { // 還未填滿(mǎn)一行
 - y.push(item.offsetHeight);
 - positions.push({
 - left: (index % columns) * (item.offsetWidth + gap),
 - top: 0
 - });
 - } else {
 - const min = Math.min(...y); // 最小縱坐標(biāo)
 - const idx = y.indexOf(min); // 縱坐標(biāo)最小的卡片索引
 - y.splice(idx, 1, min + gap + item.offsetHeight); // 替換成新卡片的縱坐標(biāo)
 - positions.push({
 - left: idx * (item.offsetWidth + gap),
 - top: min + gap
 - });
 - }
 - });
 - // 由于采用絕對(duì)定位,容器是無(wú)法自動(dòng)撐開(kāi)的。因此需要計(jì)算實(shí)際高度,即最后一個(gè)卡片的top加上自身高度
 - return { positions, containerHeight: positions[positions.length - 1].top + elements[elements.length - 1].offsetHeight };
 - }
 
上面這段代碼的作用就是計(jì)算每個(gè)卡片的left、top,以及容器的總高度。關(guān)鍵位置都有注釋?zhuān)瑧?yīng)該不難理解。
有了這幾行核心代碼,要想封裝成瀑布流組件就很容易了。以 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>
 
不過(guò)這樣其實(shí)還會(huì)有點(diǎn)問(wèn)題,就是doLayout的執(zhí)行時(shí)機(jī)。因?yàn)樵摲桨富诮^對(duì)定位,需要元素在渲染完成后才能獲取到實(shí)際寬高。如果卡片內(nèi)有延遲加載的圖片或者其他動(dòng)態(tài)內(nèi)容,高度會(huì)發(fā)生變化。這種情況下就需要在DOM更新后主動(dòng)調(diào)用一次doLayout重新計(jì)算布局。
如果大家有更好的實(shí)現(xiàn)方案,歡迎交流!
代碼倉(cāng)庫(kù):https://github.com/kaysonli/masonry-lite
npm 包:masonry-lite
如果覺(jué)得對(duì)你有幫助,幫忙點(diǎn)個(gè)不要錢(qián)的star。
本文轉(zhuǎn)載自微信公眾號(hào)「1024譯站」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系1024譯站公眾號(hào)。

















 
 
 









 
 
 
 