图片加载完执行函数
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值
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) => (
));
}
return null;
}
大概逻辑没有问题,
目前存在的问题:
1.如果原尺寸图片比较小,给定的宽度较大,图片会模糊,(下回分解)
2.无法适应移动端尺寸,图片显示有问题(然后去网上搜的解决方案,如下加css)
img{
width: auto;
max-width: 100%;
}
3.此案例没考虑图片之间的空隙,大家可以参考这个大佬的,自己试试
有空隙的瀑布流布局