图片瀑布流布局-等宽不等高

参考链接

图片加载完执行函数

react屏幕监听尺寸变化

原生js实现瀑布流

几种实现瀑布流的方式

我的需求

1、从后台获取图片,然后给定图片宽度,计算图片的高度,等宽不等高布局

2、当页面尺寸变化时,图片列数发生改变,适应页面的尺寸

实现

分析:

有两种方法:第一种简单的,用css  column-count属性,  给定图片宽度,然后用 监听事件监听resize, 改变column-count即可

window.addEventListener("resize", this.onWindowSizeChanged);

https://www.runoob.com/cssref/css3-pr-column-count.html

第二种复杂的,用js

下面全部内容都是介绍复杂的方法:

项目使用的是react+typescript,我是利用绝对定位来做的,计算出每张图片的left, top,给图片加样式,页面尺寸变化的时候,改变图片的left和top值

  • API

export interface IImageInfoProvider {
    //filter表示筛选条件,请根据你自己的项目自行定义IImageFilter, 例如:  { id:string }
    //IImage表示图片的类型,自行定义, 例如:{id:string;  date:Date; }...
  fetch(filter?: IImageFilter): Promise;
}
// props
export interface IProps {
  imageProvider: IImageInfoProvider;
}
//state
interface IState {
  /** fetch获取到的图片信息 */
  images: IImage[];
  /** 页面宽度 */
  imageContainerWidth: number;
  // 每个图片的位置(left、top)
  arr: [];
}

 

  • 获取数据函数
  private async fetchData() {
    //拿到数据
    const images = await this.props.imageProvider.fetch({id:"xxx"});
    //计算图片的位置
    const image = await this.addLocationInImage(images);
    this.setState({ images, arr: image })
  }
  • 监听页面尺寸变化的函数
 private async getImageContainerWidth() {
    //这里是获取浏览器宽度,Math.max那部分是从网上找的,能解决浏览器兼容性
    const imageContainerWidth =
      window.innerWidth ||
        Math.max(document.documentElement.clientWidth || document.body.clientWidth;
    this.setState({imageContainerWidth},async () => {
        //当页面尺寸变化的时候,获取图片位置
        const image = await this.addLocationInImage(this.state.images);
        //下面这部分代码,通过原生js修改style
        const imageArr = document.getElementsByClassName(
          "imageContainer",
        );
        for (let i = 0; i < imageArr.length; i++) {
          imageArr[i].setAttribute(
            "style",
            `position:absolute;left:${image[i].left}px;top:${image[i].top}px`,
          );
        }
   //注意了,修改style只是其中一种方法
       //也可以替换成 this.setState({arr:image}); arr这个数组就是来放图片位置的
      },)
}

  /** 监听函数*/
  private listenScreenChange() {
    window.addEventListener("resize", this.getImageContainerWidth);
  }
  /** 移除监听 */
  private removeListenScreenChange() {
    window.removeEventListener("resize", this.getImageContainerWidth);
  }
  • 计算图片位置的函数

建议先看下这个重要原理

  // 重要原理:
  // 举个例子,比方说列数为3
        
      // 第一行图片高度(图1,图2,图3)
      // 高度分别是      20,10,30,
      // 下标分别是      0 , 1, 2,
      // 第一行的 top都为0, left就是(下标*图片给定宽度)
      // 例如: 图3, top:0 , left :2*450,

      // 第一行最小高度是10, 下标是1
      // 那么图4呢 (假设图4高度为40)   图4挨着下标为1 ,高度为10的排列,
      // 所以图4  left为(1 * 图片给定宽度),top 为 10

      // 图5呢?(假设图5高度为50)  此时可以把上面4个图片看成新的一行
      // 新一行图片高度(图1,图2+图4,图3)
      // 高度分别是      20, 10+40, 30   //第2列高度为(图2高度 + 图4高度)
      // 下标分别是      0 ,   1,   2    //第2列下标为(新的1)
      // 新一行的最小高度为20,下标为0,
      // 图5紧挨着下标为0,高度为20的排列
      // 所以图5  left为(0 * 图片给定宽度),top 为 20

      // 图6呢?(假设图6高度为60)  可以把上面5个图片看成新的一行
      // 新一行图片高度(图1+图5,图2+图4,图3)
      // 高度分别是      20+50, 10+40, 30  //第1列高度为(图1高度 + 图5高度)
      // 下标分别是      0 ,   1,   2     //下标不变
      // 新一行的最小高度为30,下标为2,
      // 图6紧挨着下标为2,高度为30的排列
      // 所以图6  left为(2 * 图片给定宽度),top 为 30

      // 往后以此类推
      // 重点算出,新一列的数据, 新的每一行的数据

代码

/** 返回正整数 */
  private positiveInteger(num: number) {
    if (/(^[1-9]\d*$)/.test(num.toString())) {
      return num;
    }
    return Math.ceil(num);
  }
/** 计算每张图片的高, 因为给定了宽度,所以我是按照图片原比例来计算的 */
  private async getEachImageHeight(imageSrc: string, eachImageWidth: number) {
    const img = new Image();
    img.src = imageSrc;
    //图片成功加载后返回高度
    return new Promise(resolve => {
      img.onload = () => {
        const newImageHeight = this.positiveInteger(
          (eachImageWidth * img.height) / img.width,
        );
        resolve(newImageHeight);
      };
    });
  }
/** 在数组中添加高度属性,值为计算出的每张图片的高度  */
  private async addHeightInImageArr(imageArr: any, eachImageWidth: number) {
    for (let i = 0; i < imageArr.length; i++) {
      const imgHeight = await this.getEachImageHeight(
        imageArr[i].src,
        eachImageWidth,
      );
      imageArr[i] = { ...imageArr[i], height: imgHeight };
    }
    return Promise.resolve(imageArr);
  }
/** 计算left和top */
private async addLocationInImage(images: IImage[]) {
    //图片给定宽度
    const eachImageWidth = 450;
    const imageArr = JSON.parse(JSON.stringify(images));
    /** 列数 = 页面宽 / 图片给定宽度 */
    const columnValue = Math.floor(
      this.state.imageContainerWidth / eachImageWidth,
    );
    // 有高度属性的数组
    const locationOfEachImage = await this.addHeightInImageArr(
      imageArr,
      eachImageWidth,
    );
    //这个数组用来存放每行的图片信息
    const imageHeightArr: any = [];
    /** 找出每行最小的高对应的下标 */
    function getIndex(arr: any, minHeight: number): number | undefined {
      for (let i = 0; i < arr.length; i++) {
        if (arr[i] === minHeight) {
          return Number(i);
        }
      }
    }
      // 循环有高度属性的数组
  for (let i = 0; i < locationOfEachImage.length; i++) {
    // 第一列, 找出第一行最小的高度以及对应的下标,所以第一行各图片的绝对定位的  
    //top都是0,left就是(当前下标*图片给定的宽度)
    if (i < columnValue) {
      imageHeightArr.push(locationOfEachImage[i].height);
      locationOfEachImage[i] = {
        // 重构当前项
        // 增加id属性
        id: locationOfEachImage[i].id,
        height: locationOfEachImage[i].height,
        // 增加left属性
        left: this.positiveInteger((eachImageWidth) * i),
        // 增加top属性
        top: 0,
      };
    } else {
      // 然后下一行的第一张图, 位置紧挨着上一行最小高度下标对应的图片,那么你需要计算出
      // (上一行最小高度 ,对应下标),然后循环到最后一张图
      // 找出最小高度
      const minHeight = Math.min.apply(null, imageHeightArr);
      /** 找到下标 */
      let minHeightIndex = 0;
      const indexValue = getIndex(imageHeightArr, minHeight);
      if (indexValue) {
        minHeightIndex = indexValue;
      }
      // 增加位置属性
      locationOfEachImage[i] = {
        id: locationOfEachImage[i].id,
        height: locationOfEachImage[i].height,
        left: this.positiveInteger(
          (eachImageWidth) * minHeightIndex,
        ),

        top: this.positiveInteger(minHeight),
      };
      /** 新一列的高度 */
      const newMinHeight =
        minHeight + locationOfEachImage[i].height;
      // 新一行
      for (let k = 0; k < imageHeightArr.length; k++) {
        if (k === minHeightIndex) {
          imageHeightArr[k] = newMinHeight;
        }
      }
    }
  }
    //返回带id,height,left,top属性的数组,该数组长度和图片数组长度一致
  return locationOfEachImage;
  }

  • 这些函数放到组件里头
  public async componentDidMount() {
    //获取图片数据,计算图片的位置
    this.fetchData();
    //监听页面尺寸变化
    this.listenScreenChange();
  }

  public componentWillUnmount() {
    //移除页面尺寸的监听
    this.removeListenScreenChange();
  }


  public render() {
    return (<>{this.imagesRender()})
  }

  //图片渲染
  private imagesRender() {
    const imageArr = this.state.images;
    if (
      Object.prototype.toString.call(imageArr) === "[object Array]" &&
      imageArr.length !== 0
    ) {
      const arr = this.state.arr;
      return imageArr.map((e, i) => (
        
image
)); } return null; }

大概逻辑没有问题,

目前存在的问题:

1.如果原尺寸图片比较小,给定的宽度较大,图片会模糊,(下回分解)

2.无法适应移动端尺寸,图片显示有问题(然后去网上搜的解决方案,如下加css)

img{
    width: auto;
    max-width: 100%;
}

3.此案例没考虑图片之间的空隙,大家可以参考这个大佬的,自己试试

有空隙的瀑布流布局

你可能感兴趣的:(react,typescript,瀑布流,监听页面变化,等宽不等高)