动态排列重组元素「瀑布流」丝滑实现

314 阅读1分钟

动态排列重组瀑布流

点击查看瀑布流demo

瀑布流.gif

思路

核心:利用定位来进行瀑布流排布

<!-- 项目已提前引入了 tailwindcss -->
<div class="container flex mx-auto">
  <div class="relative w-full water-fall-wrapper" ref="waterFallWrapper"></div>
</div>

随机生成 num 个瀑布流元素

const createManyItem = (waterFallWrapperElement: HTMLDivElement, num: number) => {
    for (let i = 0; i < num; i++) {
      const divElem = document.createElement('div')
      divElem.className = 'absolute'
      // 随机高度
      divElem.style.height = `${Math.floor(300 * Math.random())}px`
      // 随机背景色
      divElem.style.backgroundColor = `rgb(${Math.floor(256 * Math.random())}, ${Math.floor(256 * Math.random())}, ${Math.floor(256 * Math.random())})`
      // 过渡动画
      divElem.style.transition = 'all 1s'
      waterFallWrapperElement.appendChild(divElem)
    }
  }

生成数组来记录瀑布流每列的高度

// 长度为 列数 的数组
let fallLineHeightList: number[] = new Array(colNum).fill(0)

关键步骤,排布瀑布流元素逻辑

// 排列瀑布流元素
const arrangeWaterFallElem = (waterFallWrapperElement: HTMLDivElement) => {
  waterFallWrapperElement.childNodes.forEach((itemElement) => {
    // 每个 water-fall 元素
    const divItemElem = itemElement as HTMLDivElement
    // 找到瀑布流最矮的那一列,也就是我们要插入的那一列
    // lowestInfo.height 最矮那列的高度
    // lowestInfo.index 最矮那列的索引
    const lowestInfo = findLowestFallLine()
    const currentElemHeight = divItemElem.offsetHeight
    const currentElemWidth = waterFallWrapperElement.clientWidth / colNum
    // 更新元素的宽度和定位
    divItemElem.style.width = `${currentElemWidth}px`
    divItemElem.style.top = `${lowestInfo.height}px`
    divItemElem.style.left = `${currentElemWidth * (lowestInfo.index % waterFallAttr.colNum)}px`
    // 更新瀑布流每列高度
    fallLineHeightList[lowestInfo.index] += currentElemHeight
  })
}

其中 找到瀑布流最矮的那一列 的函数实现是

// 找到最低的瀑布流
const findLowestFallLine = (): iLowestInfo => {
  const lowestInfo: iLowestInfo = { index: 0, height: Infinity }
  fallLineHeightList.forEach((fallLineHeight, index) => {
    if (fallLineHeight < lowestInfo.height) {
      lowestInfo.index = index
      lowestInfo.height = fallLineHeight
    }
  })
  return lowestInfo
}

interface iLowestInfo {
  index: number
  height: number
}

至此,一个简易的瀑布流主要逻辑就完成了!

优化:

加上监听页面宽度变化,重新计算列数,元素位置,并提供了元素之间的gap数值

文末贴上vue3的代码实现:

<script setup lang="ts">
  import { debounce } from '@/utils/utils'
  import { onMounted, ref } from 'vue'
  interface iLowestInfo {
    index: number
    height: number
  }
  const waterFallWrapper = ref(null)
  // 窗口宽度对应瀑布流列数
  const screenWidthMap = new Map([
    [[0, 640], 2],
    [[640, 768], 3],
    [[768, 1024], 4],
    [[1024, 1280], 6],
    [[1280, 1536], 8],
    [[1536, Infinity], 8]
  ])
  // 瀑布流属性
  const waterFallAttr = {
    containerWidth: 0, // 容器宽度
    colNum: 0, // 列数
    colGap: 10, // 列gap
    rowGap: 10 // 行gap
  }
  // 瀑布流高度 list
  let fallLineHeightList: number[] = new Array(waterFallAttr.colNum).fill(0)
  // 找到最低的瀑布流
  const findLowestFallLine = (): iLowestInfo => {
    const lowestInfo: iLowestInfo = { index: 0, height: Infinity }
    fallLineHeightList.forEach((fallLineHeight, index) => {
      if (fallLineHeight < lowestInfo.height) {
        lowestInfo.index = index
        lowestInfo.height = fallLineHeight
      }
    })
    return lowestInfo
  }
  // 生成 num 个瀑布流元素
  const createManyItem = (waterFallWrapperElement: HTMLDivElement, num: number) => {
    for (let i = 0; i < num; i++) {
      const divElem = document.createElement('div')
      divElem.className = 'absolute'
      divElem.style.height = `${Math.floor(300 * Math.random())}px`
      divElem.style.backgroundColor = `rgb(${Math.floor(256 * Math.random())}, ${Math.floor(
        256 * Math.random()
      )}, ${Math.floor(256 * Math.random())})`
      divElem.style.transition = 'all 1s'
      waterFallWrapperElement.appendChild(divElem)
    }
  }
  // 监听 容器宽度 的变化
  const viewObserver = (waterFallWrapperElement: HTMLDivElement) => {
    window.addEventListener(
      'resize',
      // 防抖一下,节省性能
      debounce(() => {
        if (waterFallWrapperElement.clientWidth !== waterFallAttr.containerWidth) {
          waterFallAttr.containerWidth = waterFallWrapperElement.clientWidth
          calcColNum(waterFallWrapperElement)
        }
      }, 100)
    )
  }
  // 计算合适的排布 并 排列瀑布流元素
  const calcColNum = (waterFallWrapperElement: HTMLDivElement) => {
    for (let v of screenWidthMap.entries()) {
      let beforeColNum = waterFallAttr.colNum
      if (
        waterFallAttr.containerWidth > v[0][0] &&
        waterFallAttr.containerWidth <= v[0][1] &&
        beforeColNum !== v[1]
      ) {
        waterFallAttr.colNum = v[1]
        arrangeWaterFallElem(waterFallWrapperElement)
        break
      }
    }
  }
  // 排列瀑布流元素
  const arrangeWaterFallElem = (waterFallWrapperElement: HTMLDivElement) => {
    fallLineHeightList = new Array(waterFallAttr.colNum).fill(0)
    waterFallWrapperElement.childNodes.forEach((itemElement) => {
      // 每个 water-fall 元素
      const divItemElem = itemElement as HTMLDivElement
      const lowestInfo = findLowestFallLine()
      const currentElemHeight = divItemElem.offsetHeight
      const currentElemWidth =
        waterFallWrapperElement.clientWidth / waterFallAttr.colNum -
        ((waterFallAttr.colNum - 1) / waterFallAttr.colNum) * waterFallAttr.colGap
      divItemElem.style.width = `${currentElemWidth}px`
      divItemElem.style.top = `${lowestInfo.height}px`
      divItemElem.style.left = `${
        (currentElemWidth + waterFallAttr.colGap) *
        (lowestInfo.index % waterFallAttr.colNum)
      }px`
      fallLineHeightList[lowestInfo.index] += currentElemHeight + waterFallAttr.rowGap
    })
  }
​
  onMounted(() => {
    const waterFallWrapperElement = waterFallWrapper.value as unknown as HTMLDivElement
    waterFallAttr.containerWidth = waterFallWrapperElement.clientWidth
    createManyItem(waterFallWrapperElement, 200)
    viewObserver(waterFallWrapperElement)
    calcColNum(waterFallWrapperElement)
  })
</script><template>
  <div class="container flex mx-auto">
    <div class="relative w-full water-fall-wrapper" ref="waterFallWrapper"></div>
  </div>
</template>