图片横向瀑布流

目前到了一家新的公司做前端开发,遇到了一个需求,这个需求是做如下的一个瀑布流,瀑布流的样式类似于百度图片的瀑布流:
公司产品瀑布流截图
这个是目前已经完成的瀑布流,现在将瀑布流的核心代码贴出,本人的能力有限,代码写得比较丑陋,请见谅。

算法

我们先去百度图片查看一下,发现,其中的每一行的图片图片的高度相同,但是每行与每行的图片高度有些微的差别:
在这里插入图片描述
在这里插入图片描述
这说明,百度的算法是,一行定高,而不同行之间不定高,于此同时,查看每一行的数量,可以发现:
在这里插入图片描述
在这里插入图片描述
数量从5-8个不等,那么算法很容易可以推理出来:
在这里插入图片描述
举个栗子,如果是三个话:
在这里插入图片描述
移动一下:
在这里插入图片描述
所以,首先获取一行的图片的宽高比,利用屏幕(父容器)的宽度,除以一定数量的图片的宽高比值,并将得到的高度与限定的高度进行对比,如果高于限定的高度,则增加一行的图片数量,如果小于限定的图片高度,则减少一行图片的数量,直到在一行之内。
但是,会出现如下的特殊情况:

  • 如果图片数量不够了,则将该行的高度限定在最大的图片高度;
  • 如果一行的数量不够,高度太高,但是加入下一张图片高度却小于限定的最低高度;或者,直到加到限定的一行的数量都不足以将高度正好放在限定的高度之中;那么,则规定一个阀值,这个阀值是指你可以容忍的图片拉伸或者缩短图片高度的量。

所以,这个算法的关键就在你需要限定一个高度的范围,以及限定一行图片的数量。如果可能的话,尽可能让后端直接返回图片的高度比,这样,能够有效减少因为网络或者图片数量过多,造成的图片展示的性能问题。

一下是算法的核心代码:

// 定义部分常量
const tooHigh = 'tooHigh'; // 高度高于规定的最大高度
const tooLow = 'tooLow'; // 高度低于规定的最小高度
const forward = 'forward'; // 向前加一
const stretching = 'stretching'; // 图片是否拉伸
// const defaultMargin = 2; // 默认的图片之间的间距

// 定义一个深拷贝函数  接收目标target参数
function deepClone(target) {
    // 定义一个变量
    let result;
    // 如果当前需要深拷贝的是一个对象的话
    if (typeof target === 'object') {
        // 如果是一个数组的话
        if (Array.isArray(target)) {
            result = []; // 将result赋值为一个数组,并且执行遍历
            for (let i in target) {
                // 递归克隆数组中的每一项
                result.push(deepClone(target[i]))
            }
            // 判断如果当前的值是null的话;直接赋值为null
        } else if (target === null) {
            result = null;
            // 判断如果当前的值是一个RegExp对象的话,直接赋值    
        } else if (target.constructor === RegExp) {
            result = target;
        } else {
            // 否则是普通对象,直接for in循环,递归赋值对象的所有值
            result = {};
            for (let i in target) {
                result[i] = deepClone(target[i]);
            }
        }
        // 如果不是对象的话,就是基本数据类型,那么直接赋值
    } else {
        result = target;
    }
    // 返回最终结果
    return result;
}

/**
 * 主体函数,用来计算并且返回高度值
 * @param {Arary} list 传来的含有图片宽高的数组 
 * @param {Number} parentContentWidth 父组件的宽度
 * @param {Number} minHeight 手动设置的最大高度
 * @param {Number} maxHeight 手动设置的最小高度
 * @param {Number} maxRowImageCount 一行最大图片数量
 * @param {Number} margin 内部元素之间的间隔大小
 */
const computedImages = (list, parentContentWidth, minHeight, maxHeight, maxRowImageCount, margin) => {
    // 获取传来的数据组的长度
    const length = list.length;
    // 规定的一行最大的图片数量
    const halfCount = Math.ceil(maxRowImageCount / 2);
    // 这里是结果
    let result = [];
    // 如果长度为0,并且一半的长度未0,不进行原酸
    if (length === 0 || halfCount === 0) return [];
    // 如果一行最多图片数量的一般大于数组的长度
    if (halfCount >= length) {
        // 用来指向计算部分数组的开始的指针
        let recordFlag = 0;
        // 用来指向计算部分数组的结尾的指针
        let i = 0;
        // 利用循环来代替递归,防止栈溢出
        while (true) {
            // 从数组本地中分离出一行需要的数组
            const interceptedArray = deepClone(list.slice(recordFlag, length - i));
            const nextOne = [];
            if (!(i >= length - 1)) {
                nextOne.push(deepClone(list[i]));
            }
            // 计算高度
            const finalHeight = computedRowCounts(interceptedArray, parentContentWidth, minHeight, maxHeight, margin, nextOne) || [];
            if (finalHeight.judge && finalHeight.judge === forward) {
                recordFlag = i;
                i = i + 1;
            }
            if (finalHeight.judge && finalHeight.judge === stretching) {
                if (i + halfCount > length) {
                    recordFlag = i;
                    i = length;
                } else {
                    recordFlag = i;
                    i += halfCount;
                }
            }
            // 如果高度过高,说明一行过少
            if (finalHeight && finalHeight.judge && finalHeight.judge === tooHigh) {
                // 则将图片高度定在设置的最大高度
                const array = maxHeightOrMinHeightComputed(interceptedArray, maxHeight, margin);
                result = deepClone([...result, ...array]);
                // 返回数组,循环停止
                return result;
            }
            // 如果太小,说明一行的数量太多
            if (finalHeight && finalHeight.judge && finalHeight.judge === tooLow) {
                // 说明已经高度已经不能再小了
                if (i <= recordFlag + 1) {
                    const array = maxHeightOrMinHeightComputed(interceptedArray, minHeight, margin);
                    result = deepClone([...result, ...array]);
                    // 继续计算下一个
                    recordFlag = i;
                    i += 1;
                } else {
                    // 减少1继续
                    i -= 1;
                }
            } else { // 正好落在最大和最小之间
                // result = [...result, ...finalHeight];
                if (finalHeight.judge && finalHeight.judge === stretching) {
                    result = deepClone([...result, ...finalHeight.result]);
                    // recordFlag = i;
                } else if (finalHeight.judge && finalHeight.judge === forward) {
                    result = deepClone([...result, ...finalHeight.result]);
                    recordFlag = i;
                } else {
                    result = deepClone([...result, ...finalHeight]);
                    recordFlag = i;
                    if (i + halfCount >= length) {
                        i = length;
                    } else {
                        i += halfCount;
                    }
                }
            }
            if (result.length >= length) {
                return result;
            }
        }
    } else {
        let recordFlag = 0;
        let i = halfCount;
        while (true) {
            const interceptedArray = deepClone(list.slice(recordFlag, i));
            const nextOne = [];
            if (!(i > length - 1)) {
                nextOne.push(list[i]);
            }
            const finalHeight = computedRowCounts(interceptedArray, parentContentWidth, minHeight, maxHeight, margin, nextOne) || [];
            if (finalHeight.judge && finalHeight.judge === forward) {
                recordFlag = i;
                i = i + 1;
            }
            if (finalHeight.judge && finalHeight.judge === stretching) {
                if (i + halfCount >= length) {
                    recordFlag = i;
                    i = length;
                } else {
                    recordFlag = i;
                    i += halfCount;
                }
            }
            // 如果计算一行太高,则意味着一行数量太少
            if (finalHeight.judge && finalHeight.judge === tooHigh) {
                // 如果 i 比 数组长度还大,数组到头,则将该行设置为最大高度
                if (i >= length) {
                    const array = maxHeightOrMinHeightComputed(interceptedArray, maxHeight);
                    result = deepClone([...result, ...array]);
                } else if (interceptedArray.length >= maxRowImageCount) {
                    const array = maxHeightOrMinHeightComputed(interceptedArray, maxHeight);
                    result = deepClone([...result, ...array]);
                    recordFlag = i;
                    if (i + halfCount >= length) {
                        i = halfCount;
                    } else {
                        i += halfCount;
                    }
                } else { // 否则,则加一继续计算
                    i += 1;
                }
            } else if (finalHeight.judge && finalHeight.judge === tooLow) { // 如果太小,则意味着一行过多
                if (i >= length) {// 如果 i 比数组长度还多或者相等
                    const array = maxHeightOrMinHeightComputed(interceptedArray, minHeight);
                    result = deepClone([...result, ...array]);
                }
                // 如果一行已经只有一个图片了
                if (i <= recordFlag + 1) {
                    const array = maxHeightOrMinHeightComputed(interceptedArray, maxHeight);
                    result = deepClone([...result, ...array]);
                    // 继续下一个
                    recordFlag = i;
                    i += 1;
                } else { // 否则,一行数量减1,继续下一个
                    i -= 1;
                }
            } else { // 正好落在区间范围,则整合进结果数组
                if (finalHeight.judge && finalHeight.judge === stretching) {
                    result = deepClone([...result, ...finalHeight.result]);
                    // recordFlag = i;
                } else if (finalHeight.judge && finalHeight.judge === forward) {
                    result = deepClone([...result, ...finalHeight.result]);
                    recordFlag = i;
                }
                else {
                    result = deepClone([...result, ...finalHeight]);
                    recordFlag = i;
                    if (i + halfCount >= length) {
                        i = length;
                    } else {
                        i += halfCount;
                    }
                }
            }
            if (result.length >= length) {
                return result;
            }
        }
    }

}

/**
 * 用来计算一行高度
 * @param {Array} list 需要计算的数组
 * @param {Number} parentContentWidth 父组件的宽度
 * @param {Number} minHeight 最小高度
 * @param {Number} maxHeight 最大高度
 * @param {Number} margin 是否有 margin
 * @param {Object} nextOne 下一个图片的属性,用来对图片进行与判断,减少循环计算的次数
 * @returns 如果高度过高,或者过低,返回一个对象,对象中包含过高(tooHigh)或者过低(tooLow)以及计算出来的高度,如果落在最高和最低的范围中,则返回插入高度和宽度的数组
 */
const computedRowCounts = (list = [], parentContentWidth, minHeight, maxHeight, margin, nextOne) => {
    const result = deepClone(list);
    const plusOne = deepClone([...list, ...nextOne]);
    const length = list.length;
    // 比值之和
    let ratioSum = null;

    let numberPlusOneRatioSum = null;
    for (const item of result) {
        ratioSum += (item.width) / (item.height);
    }

    for (const item of plusOne) {
        numberPlusOneRatioSum += (item.width) / (item.height);
    }
    // 算出高度,并进行对比
    const computedHeight = parentContentWidth / ratioSum;
    const plusOneHeight = parentContentWidth / numberPlusOneRatioSum;

    // 返回包含过高对象或者过低对象或者落在范围中的数组
    if (computedHeight >= minHeight && computedHeight <= maxHeight) {
        if (margin > 0) {
            const array = maxHeightOrMinHeightComputed(result, computedHeight, margin);
            return array;
        }
        const array = maxHeightOrMinHeightComputed(result, computedHeight);
        return array;
    }
    if (computedHeight < minHeight) {
        if ((minHeight - plusOneHeight) / (length + 1) <= 10) {
            return {
                result: maxHeightOrMinHeightComputed(plusOne, plusOneHeight, margin),
                judge: forward,
            }
        } else {
            return {
                judge: tooLow,
                computedHeight: computedHeight,
            };
        }
    }
    if (computedHeight > maxHeight) {
        if ((computedHeight - maxHeight) / length <= 10) {
            return {
                result: maxHeightOrMinHeightComputed(result, computedHeight, margin),
                judge: stretching,
            }
        } else {
            return {
                judge: tooHigh,
                computedHeight: computedHeight,
            };
        }
    }
}

/**
 * 用于计算每一行的高度
 * @param {Array} 传来的图片属性数组
 * @param {Number} 计算的高度
 * @param {Number} 外边距
 */
const maxHeightOrMinHeightComputed = (list, height, margin) => {
    const array = deepClone(list);
    if (margin > 0) {
        for (const item of array) {
            const width = (height / item.height * item.width) - (margin * 2);
            item.width = width;
            item.height = height;
        }
        return array;
    }
    for (const item of array) {
        const width = (height / item.height) * item.width;
        item.height = height;
        item.width = width;
    }
    return array;
}

export {
    computedImages,
}; 

顺便贴一个在后端没有传递图片宽高比值时,使用的方法:


/**
 * 
 * @param {Array} list 
 * @param {Function} 自定义的属性回调函数 
 */
const getImageWidthHeightFromRemote = (list, callback) => {
    const imageList = [];
    let executorFunction = (item, resolve) => {
        let image = new Image();
        image.src = item.src;
        let defaultGetProperty = (image, item) => {
            return {
                src: image.src,
                width: image.width,
                height: image.height,
            }
        }
        if (callback && callback instanceof Function) {
            defaultGetProperty = callback;
        }
        image.onload = () => {
            resolve(defaultGetProperty(image, item));
        }
    }
    if (list instanceof Array && list.length > 0) {
        for (const item of list) {
            const promise = new Promise((resolve, reject) => { executorFunction(item, resolve, reject) });
            imageList.push(promise);
        }
    }
    return imageList;
}

具体的react组件,已经开源在我的gitee上面,不过目前 readme 文档没有做好,算法也没有优化,暂时不给地址了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值