首页 前端知识 让你吃透瀑布流布局

让你吃透瀑布流布局

2024-08-21 10:08:18 前端知识 前端哥 412 695 我要收藏

前端开发中,由于内容的缺失或者大小的不定,经常会碰到参差不齐的块块。那么如何优雅得展示这些内容,美化我们的产品呢?

通常来说,将不规整的内容规整化是最直观的解决方案。例如,对宽高比不同的图片放入到统一尺寸的容器中,如下述例图:
在这里插入图片描述
But,过于规整的限制反而约束了图片自身该有的魅力(许多图片都发生了形变)。同时,当图片的数量达到一定的量级,很容易给用户带来审美疲劳。

定义
接下来给大家介绍一下瀑布流布局,广泛应用于淘x、x东、小红x等各大主流移动端应用中。所谓瀑布流布局,便是指表现为参差不齐的多栏布局,即页面上的内容(如图片、文章摘要等)按照一定的规则排列,但每列的高度不固定。随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。————文心一言
直观来看,瀑布流布局的表现如下图

在这里插入图片描述

实现方式

1、纯CSS
使用column-count可以设定数据划分的列数,并通过column-gap设定列间距。该方式实现起来较为简洁,但在这种实现方式下,数据会按照左上、左中、左下、中上、中中、中下、右上、右中、右下的原则排列(以三列为例)。因此当有新数据增加时,总是会出现在右下的位置,同时也会导致旧数据的位置发生变化。(不太推荐)

<script setup>
// 图片列表路径
const imgList = [
  '/src/assets/jpgs/01.webp'... // 其余数据省略
]
</script>
<template>
  <div class="waterfall-width-column">
    <div class="image-box" v-for="img in imgList" :key="img">
      <img :src="img" :alt="img" style="margin-top: 12px" />
    </div>
  </div>
</template>

<style lang="scss" scoped>
.waterfall-width-column {
  column-count: 3;
  column-gap: 10px;
  .image-box {
    img {
      display: block;
      width: 100%;
    }
  }
}
</style>

2、Flex布局
首先,介绍该实现方式的思路。

  1. 根据设备分辨率大小,设定合适的列数以及列宽(可以添加resize事件进行更新);
  2. 为每个列设定高度记录器,记录现存的每列高度,同时为每列预留好要使用的数据源;
  3. 当需要插入新元素时,首先将使用该元素的宽高比按照列宽进行换算,计算出实际要展示的高度;
  4. 找出当前高度记录器中记录的高度最低的列索引,将该元素插入到该列数据源中,并更新其高度计数器;
    在这里插入图片描述
<script lang="ts" setup>
import { onMounted, Ref, ref } from 'vue'
// 可配置参数
const props = defineProps({
  marginRow: {
    type: Number,
    default: 16,
  },
  marginColum: {
    type: Number,
    default: 12,
  },
  minWidth: {
    type: Number,
    default: 200,
  },
  maxWidth: {
    type: Number,
    default: 300,
  },
})

// 图片路径,通常以接口形式返回
const imgList = [
  '/src/assets/jpgs/01.webp',
  '/src/assets/jpgs/02.webp'... // 省略其他数据
]
// 图片宽高比,通常以接口形式返回
const widthAndHeight = [
  [4, 3],
  [102, 125]... // 省略其他数据
]
// 图片列表容器
const imgsContainer = ref()
// 图片自适应宽度
const imgWidth = ref(200)
// 列表列数
const imgColumn = ref(0)
// 每列图片的数组
const imgSplitList: Ref<any[]> = ref([])
// 高度纪录器
let heightRecord: any[] = []
const loaded = ref(false)
// 把鸡蛋放到篮子里
const loadImg = () => {
  // 获取容器宽度
  const containerWidth = imgsContainer.value.offsetWidth - 20
  if (containerWidth <= props.minWidth * 2 + props.marginRow) {
    // 容器宽度处于瀑布布局的下限
    imgWidth.value = props.minWidth
    imgColumn.value = 2
  } else {
    // 寻找合适的列数
    let columNum = 1
    let width: number
    do {
      columNum++
      width = Math.floor(
        (containerWidth - (columNum - 1) * props.marginColum) / columNum
      )
    } while (width >= props.maxWidth)
    imgWidth.value = width
    imgColumn.value = columNum
  }

  imgSplitList.value = []
  for (let index = 0; index < imgColumn.value; index++) {
    imgSplitList.value.push([])
  }
  // 初始化高度记录器
  heightRecord = new Array(imgColumn.value).fill(0)
  // 循环装载图片
  imgList.forEach((_, index) => getOffest(index))
  loaded.value = true
}
const getOffest = (index: number) => {
  // 遍历目前纵向偏移最少的值 所在的索引
  let yIndex = 0,
    yOffest = heightRecord[0]
  for (let i = 0; i < heightRecord.length; i++) {
    if (heightRecord[i] < yOffest) {
      yOffest = heightRecord[i]
      yIndex = i
    }
  }
  // 更新该列的纵向偏移 在原基础上添加图片展示的高度以及下外边距
  heightRecord[yIndex] = parseInt(
    yOffest + getImgHeight(index) + props.marginRow
  )
  imgSplitList.value[yIndex].push(imgList[index])
}
// 根据图片自身的宽高比,转化为实际应该展示的宽度
const getImgHeight = (index: number) => {
  const [width, height] = widthAndHeight[index]
  return (height * imgWidth.value) / width
}
onMounted(() => {
  loadImg()
})
window.addEventListener('resize', loadImg)
</script>

<template>
  <div
    ref="imgsContainer"
    style="
      display: flex;
      flex-direction: row;
      overflow-y: auto;
      padding-right: 7px;
    "
  >
    <template v-if="loaded">
      <div v-for="column in imgColumn" :key="column" style="flex: 1">
        <img
          v-for="item in imgSplitList[column - 1]"
          :key="item"
          :src="item"
          :style="{ width: imgWidth + 'px' }"
        />
      </div>
    </template>
  </div>
</template>

<style lang="scss" scoped></style>

3、绝对定位方式
绝对定位的实现逻辑与Flex布局的实现方式类似,均要保留高度记录器。所不同的是,插入元素时,需要额外手动计算元素的横向偏移量和纵向偏移量。在该方式下,我们可以将所有元素的绝对定位的top值和left值均设置为0,转而使用css3的transform的translate方法实现,这种方案可以借助GPU加速进行优化。
在这里插入图片描述

<script lang="ts" setup>
import { onMounted, ref } from 'vue'
// 可配置参数
const props = defineProps({
  marginRow: {
    type: Number,
    default: 16,
  },
  marginColum: {
    type: Number,
    default: 12,
  },
  minWidth: {
    type: Number,
    default: 200,
  },
  maxWidth: {
    type: Number,
    default: 300,
  },
})

// 图片路径
const imgList = [
  '/src/assets/jpgs/01.webp',
  '/src/assets/jpgs/02.webp',
]
// 图片宽高比
const widthAndHeight = [
  [4, 3],
  [102, 125],
]
// 图片列表容器
const imgsContainer = ref()
// 图片自适应宽度
const imgWidth = ref(200)
// 列表列数
const imgColumn = ref(0)
// 高度纪录器
let heightRecord: any[] = []
const loaded = ref(false)
// 把鸡蛋放到篮子里
const loadImg = () => {
  // 获取容器宽度
  const containerWidth = imgsContainer.value.offsetWidth - 20
  if (containerWidth <= props.minWidth * 2 + props.marginRow) {
    // 容器宽度处于瀑布布局的下限
    imgWidth.value = props.minWidth
  } else {
    // 寻找合适的列数
    let columNum = 1
    let width: number
    do {
      columNum++
      width = Math.floor(
        (containerWidth - (columNum - 1) * props.marginColum) / columNum
      )
    } while (width >= props.maxWidth)
    imgWidth.value = width
    imgColumn.value = columNum
    // 初始化高度记录器
    heightRecord = new Array(columNum).fill(0)
    loaded.value = true
  }
}
const getOffest = (index: number) => {
  // 遍历目前纵向偏移最少的值 所在的索引
  let yIndex = 0,
    yOffest = heightRecord[0]
  for (let i = 0; i < heightRecord.length; i++) {
    if (heightRecord[i] < yOffest) {
      yOffest = heightRecord[i]
      yIndex = i
    }
  }
  // 求取横向偏移
  let xOffest = yIndex * (imgWidth.value + props.marginColum)
  // 更新该列的纵向偏移 在原基础上添加图片展示的高度以及下外边距
  heightRecord[yIndex] = parseInt(
    yOffest + getImgHeight(index) + props.marginRow
  )
  return { transform: `translate(${xOffest}px, ${yOffest}px)` }
}
// 根据图片自身的宽高比,转化为实际应该展示的宽度
const getImgHeight = (index: number) => {
  const [width, height] = widthAndHeight[index]
  return (height * imgWidth.value) / width
}
onMounted(() => {
  loadImg()
})
window.addEventListener('resize', loadImg)
</script>

<template>
  <div
    ref="imgsContainer"
    style="position: relative; overflow-y: auto; padding-right: 7px"
  >
    <template v-if="loaded">
      <template v-for="(item, index) in imgList" :key="item">
        <img
          :src="item"
          :style="{
            position: 'absolute',
            left: 0,
            top: 0,
            ...getOffest(index),
            width: `${imgWidth}px`,
            'object-fit': 'contain',
          }"
        />
      </template>
    </template>
  </div>
</template>

<style lang="scss" scoped></style>

4、Flex与绝对定位实现方式的差异
虽然前述介绍了Flex的实现原理与绝对定位实现方式很相似,但两者仍存在着一个本质的不同:Flex方式是先设定好每个元素所在的列和顺序后再展示,**即所有元素计算完成后再渲染。而绝对定位方式是在渲染时再计算该元素应该放置的位置。**可以对比两者代码中高度计数器更新时机,便可以分析得出该结论。

5、最终效果
在这里插入图片描述

这里是原作者哦

转载请注明出处或者链接地址:https://www.qianduange.cn//article/16306.html
标签
评论
发布的文章

在线OJ项目

2024-08-27 21:08:43

JSON2YOLO 项目教程

2024-08-27 21:08:42

Redis 实现哨兵模式

2024-08-27 21:08:22

大家推荐的文章
会员中心 联系我 留言建议 回顶部
复制成功!