写一个vue3来指令实现瀑布流布局

892 阅读2分钟

前言

这主要讨论的是如何使用position的方式实现瀑布流;最后再将其封装到vue3的自定义指令中,做到高可复用

先来上效果图

waterfall.gif

  • 通过效果图,可以比较清晰的看到,这的瀑布流的实现原理是将新增的每个元素添加到高度最小的那一列

实现思路

  • 通过维护一个数组,数组每个元素代表每一列;
    • 比如瀑布流有3列,则数组固定有三个元素
    • 数组中每个元素是一个包含当前列高度的对象
  • 当容器内有新增元素时,将新增元素添加到数组中高度最小的那一列

代码实现

/directive/waterfall.directive.ts

interface ArrColumn {
  height: number; // 列当前高度
  left: number; // 列position-left位置
}

/**
 * 指令参数
 */
interface Params {
  column: number;
  gap: number;
}

let column = 3; // 容器内元素分为多少列,默认3

let gap = 12; // 容器内元素间距,默认12px

let childWidth: number; // 元素宽度 = (容器宽度 - 总间距)/ 列

let arrColumn: ArrColumn[]; // 用来存储每一列的占用高度,每一个元素代表一列;当有新增元素时,将其添加到height最小的那一列

export const waterfall = {
  mounted(el, binding) {
    const options: Params = binding.value;
    if (options?.column) {
      column = options.column;
    }
    if (options?.gap) {
      gap = options.gap;
    }
    initArrColumn(el)
    setChildPosition(el)
  },
  updated(el) {
    setChildPosition(el)
  },
};

/**
 * 初始化arrColumn,通过容器总宽度,间距,列等要素,计算出每一列元素的position-left位置
 * 
 * @param el 容器dom
 */
const initArrColumn = (el) => {
  arrColumn = new Array<ArrColumn>(column).fill(null);
  const { width } = el.getBoundingClientRect();
  childWidth = (width - (column + 1) * gap) / column; // 元素宽度 = (容器宽度 - 总间距)/ 列
  arrColumn = arrColumn.map((_, index) => ({
    height: 0,
    left: childWidth * index + gap * (index + 1)
  }))
};


/**
 * 设置容器中每个子元素的位置
 * class:no-position 代表还没有设置位置的元素,设置后移除
 */
const setChildPosition = (el) => {
    const childDom = el.querySelectorAll('.no-position');
    for(let i = 0; i < childDom.length; i++) {
        const minHeightColumn: ArrColumn = findMinHeightArrColumn()
        childDom[i].style.width = childWidth + 'px'
        childDom[i].style.left = minHeightColumn.left + 'px'
        childDom[i].style.top = minHeightColumn.height + gap + 'px'
        minHeightColumn.height += childDom[i].getBoundingClientRect().height + gap
        childDom[i].classList.remove('no-position')
    }
}

/**
 * 查找最小高度的列,如果最小高度有多列,取最左边的列
 */
const findMinHeightArrColumn = (): ArrColumn => {
    let minHeightColumn: ArrColumn
    arrColumn.forEach(item => {
        if (!minHeightColumn) {
            minHeightColumn = item
            return
        } 
        if (item.height < minHeightColumn.height) {
            minHeightColumn = item
        }
    })
    return minHeightColumn;
}

代码具体实现思路

  • 核心点是arrColumn数组,arrColumn数组中每个元素代表一列

  • 数组的每个元素是个ArrColumn对象,

    • 其中height属性代表容器中已有元素在每一列已占用的高度,当有新增元素的时候,将新增元素添加到arrColumn中height最小的那列
    • left属性代表每一列元素的position-left位置(这是默认每列宽度均分来计算的)
  • mounted的时候:

    • 初始化column和gap,并设置arrColumn的默认值
    • 设置已有子元素的position的top/left位置,每设置一个,同步修改arrColumn中记录的height列高度
  • update的时候:

    • 设置没有给定位置的子元素top/left

实现效果图的全部代码

image.png