萬能瀑布流

常見的瀑布流實現大部分只適用於子塊尺寸固定或內部有圖片異步加載的狀況。html

而對於子塊有圖片這種可能引發尺寸變化的狀況,一般的作法是寫死圖片高度,或檢測內部的 img 元素從而在 onload 事件中進行重排。vue

因爲咱們業務中尺寸變化狀況更爲複雜,如子塊自己異步初始化、內部數據異步獲取,且這種尺寸變化時機不可肯定,爲知足這種需求因此調研完成了一個通用萬能的瀑布流實現。git

如下代碼部分以 Vue.js 爲例,思路和機制是通用的。github

基礎瀑布流

先不考慮子塊尺寸變化的因素,完成基礎的瀑布流佈局功能。瀏覽器

基本屬性

瀑布流佈局的配置有三個,列數 columnCount,塊水平間距 gutterWidth、塊垂直間距 gutterHeight異步

固然也但是使用列寬代替列數,但一般狀況下,這樣就要求使用方進行列寬計算,有更高的使用成本
props: {
  columnCount: Number,
  gutterWidth: Number,
  gutterHeight: Number,
}

基本結構

對於類列表的結構,在組件開發中一般由兩種形式:佈局

  1. 組件內循環 slot
  2. 拆分爲容器組件和子塊組件,組件間作關聯邏輯

組件內循環 slot 的方式以下:字體

// Waterfall.vue
<template>
  <div>
    <slot v-for="data in list" v-bind="data">
  </div>
</template>
// 使用方--父級組件
<waterfall :list="list">
  <template v-slot="data">
    <ecology-card :ecology-info="data" />
  </template>
</waterfall>

其實現思路是,使用者將列表數據傳入組件,組件內部循環出對應個數的 slot,並將每一項數據傳入 slot,使用方根據傳回的數據進行自定義渲染。優化

這種方式使用起來比較違反視覺直覺,在使用者角度,不能直接的感覺到循環結構,但開發角度,邏輯更封閉,實現複雜邏輯更爲簡便。this

因爲瀑布流組件只提供佈局功能,應提供更直觀的視覺感覺,同時在咱們的業務需求中,子塊部分不盡相同,須要更靈活的自定義子塊內容的方式。

因此採起第二種實現方式,拆分設計爲 Waterfall.vue 瀑布流容器和 WaterfallItem.vue 瀑布流子塊兩個組件。

// 使用方
<waterfall>
  <waterfall-item>
    <a-widget /> // 業務組件
  </waterfall-item>
  <waterfall-item>
    <b-image /> // 業務組件
  </waterfall-item>
</waterfall>
// Waterfall.vue
<script>
  render (h) {
    return h('div', this.$slots.default)
  }
<script>

<style>
.waterfall {
  position: relative;
  width: 100%;
  min-height: 100%;
  overflow-x: hidden;
}
</style>

Waterfall.vue 組件只須要與父組件同寬高,而且將插入內部的元素原樣渲染。

增刪

爲了保證在新增或刪除子塊時使從新佈局的成本最小化,我選擇由 WaterfallItem.vue 告知 Waterfall.vue 本身的新增和移除。

// Waterfall.vue
data () {
  return {
    children: []
  }
},
methods: {
  add (child) {
    const index = this.$children.indexOf(child)
    this.children[index] = child
    this.resize(index, true)
  },
  delete (child) {
    const index = this.$children.indexOf(child)
    this.children[index].splice(index, 1)
    this.resize(index, false)
  }
}
// WaterfallItem.vue
created () {
  this.$parent.add(this)
},
destoryed () {
  this.$parent.delete(this)
}

那麼下面就要開始進行佈局邏輯方法的編寫。

瀑布流佈局受兩個因素影響,每一個子塊的寬和高,咱們須要在適當的時候從新獲取這兩個維度的數據,其中塊寬即列寬。

佈局要素:列寬

列寬受兩個因素的影響,容器寬度和指望的列數,那麼列寬明顯就是一個計算屬性,而容器寬度須要在初始化和窗口變化時從新獲取。

// Waterfall.vue
data () {
  return {
    // ...
    containerWidth: 0
  }
},
computed: {
  colWidth () {
    return (this.containerWidth - this.gutterWidth * (cols -1))/this.cols
  }
},
methods: {
  //...
  getContainerWidth () {
    this.containerWidth = this.$el.clientWidth
  }
},
mounted () {
  this.getContainerWidth()
  window.addEventListener('resize', this.getContainerWidth)
},
destory () {
  window.removeEventListener('resize', this.getContainerWidth)
}

也不要忘記在組件銷燬時移除監聽。

佈局要素:塊高

子塊高的獲取時機有兩個:獲取新增的塊的高度和列寬變化時從新獲取全部。

data () {
  return {
    //...
    childrenHeights: []
  }
},
resize (index, update) {
  this.$nextTick(() => {
    if (!update) {
      this.childrenHeights.splice(index, 1)
    } else {
      const childrenHeights = this.childrenHeights.slice(0, index)
      for (let i = index; i < this.children.length; i++) {
        childrenHeights.push(this.$children[i].$el.getBoundingClientRect().height)
      }
      this.childrenHeights = childrenHeights
    }
  })
},
watch: {
  colWidth () {
    this.resize(0, true)
  }
}
  • 在刪除塊時只須要刪除對應塊 DOM 的尺寸,不須要更新其餘塊的高度。
  • 新增塊或列寬變化時,子塊 DOM 未必實際渲染完成,因此須要添加 $nextTick 等待 DOM 的實際渲染,從而能夠得到尺寸。
  • 列寬變化時,從新獲取全部塊的高度。

佈局計算

佈局思路以下:

  1. 記錄每列的高度,取最短的列放入下一個塊,並更新此列高度。
  2. 若是最短的列高度爲 0,那麼取塊最少的列爲目標列,由於可能塊高爲 0,塊垂直間距爲 0,致使一直向第一列添加塊。
  3. 在此過程當中根據列數和列寬獲取每一個塊的佈局位置。
// Waterfall.vue
computed: {
  //...
  layouts () {
    const colHeights = new Array(this.columnCount).fill(0)
    const colItemCounts = new Array(this.columnCount).fill(0)
    const positions = []
    this.childrenHeights.forEach(height => {
      let col, left, top
      const minHeightCol = colHeights.indexOf(min(colHeights))
      const minCountCol = colItemCounts.indexOf(min(colItemCounts))
      if (colHeights[minHeightCol] === 0) {
        col = minCountCol
        top = 0
      } else {
        col = minHeightCol
        top = colHeights[col] + this.gutterHeight
      }
      colHeights[col] = top + height
      colItemCounts[col] += 1
      left = (this.colWidth + this.gutterWidth) * col
      positions.push({ left, top })
    })
    const totalHeight = max(colHeights)
    return {
      positions,
      totalHeight
    }
  },
  positions () {
    return this.layouts.positions || []
  },
  totalHeight () {
    return this.layouts.totalHeight || 0
  }
}

同時須要注意的一點是,在整個佈局的高度發生改變的時候,可能會伴隨着滾動條的出現和消失,這會引發佈局區域寬度變化,因此須要對 totalHeight 增長監聽。

watch: {
  totalHeight () {
    this.$nextTick(() => {
      this.getContainerWidth()
    })
  }
}

totalHeight 發生變化時,從新獲取容器寬度,這也是爲何 getContainerWidth 方法中使用 clientWidth 值的緣由,由於 clientWidth 不包含滾動條的寬度。

同時在 totalHeight 發生改變後要使用 $nextTick 後獲取寬度,由於 totalHeight 是咱們的計算值,此刻,佈局數據變化引起的視圖渲染還未發生,在 $nextTick 回調等待視圖渲染更新完成,再獲取 clientWidth

同時咱們也不須要關注 totalHeight(newValue, oldValue)newValueoldValue 是否相等,來而避免後續計算,由於若相等是不會觸發 totalHeightwatch 行爲的。

同理,也不須要判斷 totalHeight 變化先後 clientWidth 是否一致來決定是否要對 containerWidth 從新賦值,從而避免引起後續的列寬、佈局計算,由於 Vue.js 內都作了優化,只需從新獲取並賦值,避免無用的「優化」代碼。

排列

計算完成的位置和列寬須要應用到 WaterfallItem.vue

<template>
  <div class="waterfall-item" :style="itemStyle">
    <slot />
  </div>
</template>

<script>
export default {
  created () {
    this.$parent.add(this)
  },
  computed: {
    itemStyle () {
      const index = this.$parent.$children.indexOf(this)
      const { left, top } = this.$parent.positions[index] || {}
      const width = this.$parent.colWidth
      return {
        transform: `translate3d(${left}px,${top}px,0)`,
        width: `${width}px`
      }
    }
  },
  destoryed () {
    this.$parent.delete(this)
  }
}
</script>

<style>
.waterfall-item {
  box-sizing: border-box;
  border: 1px solid black;
  position: absolute;
  left: 0;
  right: 0;
}
</style>

結果

至此,基礎瀑布流邏輯也就結束了,使用現代瀏覽器點此預覽

預覽中定時向 Waterfall 中插入高度隨機的 WaterfallItem。

完成限定子塊高度在初始渲染時就固定的瀑布流後,怎麼能作一個不管何時子塊尺寸變化,都能進行感知並從新佈局的瀑布流呢?

萬能瀑布流

如何感知尺寸變化

根據這篇文章知,能夠利用滾動事件去探知元素的尺寸變化。

簡要來講:

scrollTop 爲例,在滾動方向爲向右和向下,已經滾動到 scrollTop 最大值前提下

  • 當內容(子元素)高度固定且大於容器時

    • 容器高度變大時,已滾動到最下方,容器只能上邊界向上擴展,上邊界到內容區上邊界距離變小,scrollTop 變小觸發滾動。
    • 容器高度變小時,容器底邊向上縮小,容器上邊界到內容區上邊界距離不變,scrollTop 不變,不觸發滾動。
  • 當內容爲 200% 的容器尺寸時

    • 容器高度變大時,內容區 200% 同步變化,容器向下擴展空間充足,因此下邊界向下擴展,上邊界不動,上邊界到內容區上邊界距離不變,scrollTop 不變。
    • 當容高度變小時,內容區下邊界二倍於容器收縮,容器下邊界收縮空間不足,致使上邊界相對內容區上移,scrollTop 變小觸發滾動。

因此咱們可使用:

  • 內容區尺寸固定且遠大於容器尺寸,檢測容器的尺寸增大。
  • 內容區尺寸爲容器尺寸的 200%,檢測容器的尺寸減少。

改動

那麼 WaterfallItem.vue 須要調整以下

<template>
  <div class="waterfall-item" :style="itemStyle">
    <div class="waterfall-item__shadow" ref="bigger" @scroll="sizeChange">
      <div class="waterfall-item__holder--bigger">
      </div>
    </div>
    <div class="waterfall-item__shadow" ref="smaller" @scroll="sizeChange">
      <div class="waterfall-item__holder--smaller">
      </div>
    </div>
    <slot />
  </div>
</template>

<script>
  mounted () {
    this.$nextTick(() => {
      this.$refs.bigger.scrollTop = '200000'
      this.$refs.smaller.scrollTop = '200000'
    })
  },
  methods: {
    sizeChange () {
      this.$parent.update(this)
    }
  }
</script>

<style>
  .waterfall-item {
    position: absolute;
    left: 0;
    right: 0;
    overflow: hidden;
    box-sizing: border-box;
    border: 1px solid black ;
  }
  .waterfall-item__shadow {
    height: 100%;
    left: 0;
    overflow: auto;
    position: absolute;
    top: 0;
    transform: translateX(200%);
    width: 100%;
  }
  .waterfall-item__holder--bigger {
    height: 200000px;
  }
  .waterfall-item__holder--smaller {
    height: 200%;
  }
</style>
  • slot 爲用戶的真實 DOM,其撐開 waterfall-item 的高度。
  • 兩個分別檢測尺寸增長和減少的 waterfall-item__shadowwaterfall-item 同高,從而使得用戶 DOM 的尺寸變化映射到 waterfall-item__shadow 上。
  • 渲染完成後使 waterfall-item__shadow 滾動到極限位置。
  • 用戶 DOM 的尺寸變化觸發 waterfall-item__shadowscroll 事件,在事件回調中通知 Waterfall.vue 組件更新對應子塊高度。
// Waterfall.vue
methods: {
  // ...
  update (child) {
    const index = this.$children.indexOf(child)
    this.childrenHeights.splice(index, 1, this.$children[index].$el.getBoundingClientRect().height)
  }
}

在父組件中只須要更新此元素的高度便可,自會觸發後續佈局計算。

結果

至此,可動態感知尺寸變化的萬能瀑布流也就完成了,使用現代瀏覽器點此預覽

預覽中定時修改部分 WaterfallItem 的字體大小,從而觸發子塊尺寸變化,觸發從新佈局。

優化

在以上實現以外還能夠作一些其餘優化,如:

  1. 通知 Waterfall.vue 添加的 add 和更新的 update 方法調用有重複(覆蓋)觸發的狀況,能夠合併。
  2. 按需監聽尺寸變化,對 WaterfallItem 組件添加新的 props,如:

    • 固定大小的就能夠不綁定 scroll 監聽,且不渲染 waterfall-item__shadow
    • 只會變化一次的能夠對監聽使用 once,並在後續更新時再也不渲染 waterfall-item__shadow
  3. 在佈局計算完成前對 WaterfallItem 添加不可見 visibility: hidden
  4. 在元素過多時,使用虛擬渲染的方式,只渲染在視圖範圍內的 WaterfallItem。
  5. 對應 keep-alive 的路由渲染,在非激活狀態是拿不到容器尺寸的,因此須要在 activateddeactivated 中進行從新佈局的中止和激活,避免錯誤和沒必要要的開支。
相關文章
相關標籤/搜索