前言

今天来介绍一下关于图片瀑布流布局的实现,关于瀑布流,相信大家都很熟悉了:
在这里插入图片描述
如上图所示,就是瀑布流的布局了。
本文开发环境:windows10 + vue3 + vite + tailwindcss

实现思路

看过了效果,我们来聊聊如何实现。
首先,我们要确认一个排序的原则,瀑布流的第一行肯定是顺序排列的,之后从第二行开始,最先插入图片的是目前高度最小的列,再依次排序下去
在这里插入图片描述

确认了这一点后,接下来就是创建一个瀑布流组件waterfall.vue,确认应该设置的参数:

1. 接收的props

  • data
    首先肯定是图片列表,其中保存图片地址
  • nodeKey
    唯一key,用于循环创建图片时指定key
  • columns
    希望展示的图片列数
  • picturePreLoad
    图片是否预加载,当你的数据中不包括图片高度时,应该设置该项为true,这样可以等待图片加载后再渲染布局
  • rowSpacing
    行间距
  • columnSpacing
    列间距
const props = defineProps({
  data: {
    type: Array,
    required: true
  },
  // 唯一标识key
  nodeKey: {
    type: String
  },
  columns: {
    type: Number,
    default: 3
  },
  picturePreLoad: {
    type: Boolean,
    default: true
  },
  rowSpacing: {
    type: Number,
    default: 20
  },
  colSpacing: {
    type: Number,
    default: 20
  }
})

2. 组件样式

我这边是基于vue3+tailwindcss实现的,话说tailwindcss真的好用,不过即使不用这个问题也不大,因为样式很简单,我们希望图片是横向排列的,而且根据瀑布流的排列逻辑,应该很容易想到相对定位+绝对定位的方式。

<template>
  <div class="relative m-auto bg-white" ref="containerRef" :style="{ height: containerHeight + 'px' }">
    <template v-if="columnWidth && data.length > 0">
      <div v-for="(item, index) in data" :key="nodeKey ? item[nodeKey] : index"
        class=" m-waterfall-item absolute bg-white" :style="{
          width: columnWidth + 'px',
          left: item._style?.left + 'px',
          top: item._style?.top + 'px'
        }">
        <slot :item="item" :width="columnWidth"></slot>
      </div>
    </template>
    <template v-else>加载中</template>
  </div>
</template>

3. 设置基本变量

从上面的视图代码中我们用到了几个变量

1、containerHeight:外部容器的高度;
2、columnWidth:每列的宽度;
3、item._style?.left:每个item与左边的距离;
3、item._style?.top:每个item与顶部的距离。

那么在代码中这样体现

// 总高度
const containerHeight = ref(0)
// 每一列的宽度
const columnWidth = ref(0)
// 容器实例
const containerRef = ref(null)
// 容器总宽度,设置它是为了计算每列的宽度
const containerWidth = ref(0)
// 容器与左边的距离,后续计算定位时使用
const containerLeft = ref(0)

4. 计算每列的宽度

根据上述的数据来计算容器总宽度,进而计算出每列的宽度

// 计算容器宽度以及左边距
const calcContainerWidth = () => {
  const { paddingLeft, paddingRight } = getComputedStyle(containerRef.value)
  containerLeft.value = parseFloat(paddingLeft)
  containerWidth.value = containerRef.value.offsetWidth - parseFloat(paddingLeft) - parseFloat(paddingRight)
}
// 计算列宽
const calcColumnWidth = () => {
  // 先计算容器宽度
  calcContainerWidth()
  // 每列宽度 = (容器宽度 - 所有列边距) / 4
  columnWidth.value = (containerWidth.value - props.colSpacing * (props.columns - 1)) / props.columns
}

onMounted(() => {
  // 计算列宽
  useColumnWidth()
  console.log(columnWidth.value)
})

5. 设置每列的高度

我们还需要记录每列的高度,最后容器的高度就是这些列中最大的那个高度

// 每列的高度,对象key:列数,value:高度
const columnHeightObj = ref({})
// 初始化每列高度
const initColumnHeightObj = () => {
  for (let i = 0; i < props.columns; i++) {
    columnHeightObj.value[i] = 0
  }
  // console.log('columnHeightObj.value', columnHeightObj.value);
}

6. 获取瀑布流各个item的高度

从上一步我们可以获取到每列的宽度,那么接下来就是获取瀑布流各个item的高度了,在这一块又需要分为俩种情况:

6.1 图片预加载

当picturePreLoad传入true时,需要等待图片预加载完成,我们再进行排列,那么,首先就得实现图片的预加载,这一块详见代码吧,主要就是利用Promise.all来实现的:

// 获取所有图片的高度
let itemHeights = []
/**
 * 当需要等待图片加载时
 */
const waitImgComplete = () => {
  // 每次重新计算item高度时,先清空之前的数据,以免影响
  itemHeights = []
  // 首先获取全部item的节点
  const itemElments = [...document.getElementsByClassName('m-waterfall-item')]
  // 获取所有图片的dom信息
  const imgElements = getImgElements(itemElments)
  // 获取图片列表
  const imgList = getAllImgs(imgElements)
  // console.log(imgList);
  // 等待图片加载
  onImgAllComplete(imgList).then(() => {
    imgElements.forEach((item) => {
      itemHeights.push(item.offsetHeight)
    })
    // 渲染item位置
    useItemLocation()
  })
}

// 关于处理图片预加载的代码可以单独放在utils.js文件中
// utils.js
/**
 * 获取所有包含img的节点
 */
export function getImgElements(itemElements) {
  const imgElements = []
  itemElements.forEach((el) => {
    imgElements.push(...el.getElementsByTagName('img'))
  })
  return imgElements
}

/**
 * 从img节点中获取所有img
 */
export function getAllImgs(imgElements) {
  const imgs = []
  imgElements.forEach(el => {
    imgs.push(el.src)
  })
  return imgs
}

/**
 * 等待所有图片加载完成
 */
export function onImgAllComplete(allImgs) {
  const promises = []
  allImgs.forEach((img, index) => {
    promises[index] = new Promise(resolve => {
      const imgObj = new Image()
      imgObj.src = img
      imgObj.onload = () => {
        resolve({
          img,
          index
        })
      }
    })
  })
  return Promise.all(promises)
}

如此,我们就可以得到一个装有瀑布流各个元素的高度的数组了。

6.2 图片不预加载

这种就是不等待图片加载,直接开始渲染瀑布流,很显然,当第一次加载图片时,图片尚未加载完毕就开始渲染瀑布流,肯定会导致样式变形,当然,这种情况其实也很好解决,让后端传图片数据时带上图片高度就是了,但是这个问题先不进行处理,这篇文章主要还是针对第一种方式。


/**
 * 图片不预加载,会有样式问题
 */
const useItemHeight = () => {
  // 首先清空旧数据
  itemHeights = []
  // 首先获取全部item的节点
  const itemElments = [...document.getElementsByClassName('m-waterfall-item')]
  itemElments.forEach(el => {
    itemHeights.push(el.offsetHeight)
  })
  // 渲染item位置
  useItemLocation()
}

7. 计算各个item定位

7.1 首先定义一个渲染的方法

/**
 * 渲染item的位置
 * 1、根据瀑布流的排列,我们知道,优先将图片插入到高度最小的列,所以_style的left值就是先找到高度最小的列,然后获取其列数
 * 2、根据列数及列间距计算出与左边的距离,而与顶部的距离其实就是当前列的最小高度;
 * 3、设置完left值和top值后,要对当前最小的高度进行更新,就是加上刚刚插入的图片的高度,再进行接下来的对比
 */
const useItemLocation = () => {
  props.data.forEach((item, index) => {
    if (item._style) return
    item._style = {}
    // 计算与左边的距离
    item._style.left = getLeft()
    // 计算与顶部的巨鹿
    item._style.top = getTop()
    // 对当前最小高度的列进行高度的更新
    increaseMinColumnHeight(index)
  })
  // 设置容器总高度
  containerHeight.value = getMaxHeight(columnHeightObj.value)
}

7.2 计算各个item与容器左边的距离

从我们一开始得分析可知,当第一行按序排列后,从第二行开始,首先要找到高度最小的列,之后将图片插入该列,那么目的就很明确了,首先获取高度最小的列,然后根据获取到的列数、每列宽度以及列间距计算出与容器左边的距离。

/** 计算与左边的距离 */
const getLeft = () => {
  let left = 0
  // 首先获取高度最小的列的索引
  const minHeightColumn = getMinHeightColumn(columnHeightObj.value)
  // 与左边的距离 = (每列的宽度 + 列边距)* minHeightColumn
  left = (columnWidth.value + props.colSpacing) * minHeightColumn + containerLeft.value
  return left
}

// utils.js
/**
 * 获取列中最小的高度
 * @param {包含列高度的对象} columnHeightObj 
 */
export const getMinHeight = (columnHeightObj) => {
    const heights = Object.values(columnHeightObj)
    return Math.min(...heights)
}
/**
 * 获取当前最小高度的列数
 */
export const getMinHeightColumn = (columnHeightObj) => {
    const minHeight = getMinHeight(columnHeightObj)
    return Object.keys(columnHeightObj).find(key => columnHeightObj[key] === minHeight)
}

7.3 计算item和顶部的距离

当前item和顶部的距离其实就是最小的列的高度,这个应该很好理解,因为我们就是按照当前高度最小的列来排的。

const getTop = () => {
  // 其实高度最小的列的高度
  const minHeight = getMinHeight(columnHeightObj.value)
  return minHeight
}

7.4 高度自增

当设置完当前item的left和top位置后,就需要更新当前列的高度了,其实就是加上item的高度以及行间距嘛

const increaseMinColumnHeight = (index) => {
  // 首先获取高度最小的列的索引
  const minHeightColumn = getMinHeightColumn(columnHeightObj.value)
  columnHeightObj.value[minHeightColumn] += itemHeights[index] + props.rowSpacing
}

7.5 触发渲染

其实做完上面这些步骤,整个瀑布流的功能就基本完毕了,那么剩下的就是什么时候触发渲染方法,很容易想到哈,就是传入的data数据变化时,还要注意根据picturePreLoad的值来判断是否等待图片加载完成:

watch(() => props.data, (newVal) => {
  // 重置数据源
  const resetColumnHeight = newVal.every((item) => !item._style)
  if (resetColumnHeight) {
    // 构建高度记录容器
    initColumnHeightObj()
  }
  nextTick(() => {
    if (props.picturePreLoad) {
      // 预加载
      waitImgComplete()
    } else {
      // 不预加载
      useItemHeight()
    }
  })
}, {
  immediate: true,
  deep: true
})

总结

综上所述,就是一个简单的瀑布流组件的开发思路了,我们主要要抓住几点:

  • 瀑布流的排列规则:瀑布流的第一行肯定是顺序排列的,之后从第二行开始,最先插入图片的是目前高度最小的列,再依次排序下去;
  • 根据你想怎样使用瀑布流组件来觉得传入怎么样的数据,本例中是希望用瀑布流组件包裹内部组件,所以传入的props就有data、nodeKey、colums、picturePreLoad等;
  • 接下来就是确定容器布局以及计算各个item的位置,布局很简单,容器相对定位,内部item绝对定位,而关于计算,就是根据瀑布流的排列规则,优先排列高度最小的列,那么当前item的位置就容易确定了,left值就是高度最小列的列数 * (列宽+列间距)+外部容器的paddiingLeft,item的top值就是当前高度最小列的高度值;
  • 当然,该组件还有待完善,就比如图片不预加载时的处理,同时还可以进行图片懒加载的处理等逻辑,这些以后有时间再写吧。
GitHub 加速计划 / vu / vue
207.55 K
33.66 K
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:2 个月前 )
73486cb5 * chore: fix link broken Signed-off-by: snoppy <michaleli@foxmail.com> * Update packages/template-compiler/README.md [skip ci] --------- Signed-off-by: snoppy <michaleli@foxmail.com> Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 4 个月前
e428d891 Updated Browser Compatibility reference. The previous currently returns HTTP 404. 5 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐