让你吃透瀑布流布局

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

通常来说,将不规整的内容规整化是最直观的解决方案。例如,对宽高比不同的图片放入到统一尺寸的容器中,如下述例图:
在这里插入图片描述
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、最终效果
在这里插入图片描述

这里是原作者哦

操作系统是计算机系统中的核心组成部分,负责管理和协调计算机硬件和软件资源,提供程序运行环境。在CSDN上有很多关于操作系统的专题文章,以下将从操作系统的基本概念、功能和常见类型等方面简要介绍一下。 首先是操作系统的基本概念。操作系统是一种系统软件,它是计算机硬件和应用软件之间的桥梁,提供给应用程序一系列的服务和资源,同时负责调度和管理系统资源。它为用户屏蔽了底层的硬件差异,提供了一个统一的、易于使用的界面。 操作系统主要有四个基本功能。首先是处理器管理,负责将处理器分配给系统中的各个进程,并进行进程切换,实现多道程序并发执行。其次是内存管理,管理计算机的内存资源,包括分配、回收和保护等操作。再次是文件管理,负责管理文件的存储、命名和保护等操作,提供了文件操作的接口。最后是设备管理,负责管理计算机的各种设备,包括输入输出设备和存储设备等。 常见的操作系统有多种类型。最主流的是Windows、Linux和Mac OS等桌面操作系统。此外还有服务器操作系统,如Windows Server和Linux等,用于管理和部署服务器。还有嵌入式操作系统,如Android和iOS等,用于移动设备和物联网设备。操作系统也有实时操作系统,用于需要实时控制和响应的系统,如工控系统和航空航天系统等。 总之,操作系统是计算机系统中不可或缺的重要组成部分,通过CSDN上的相关文章,我们可以更深入了解操作系统的基本概念、功能和不同类型。这些知识对于理解计算机系统的工作原理和提升编程能力都有着重要意义。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值