Vue封装一个瀑布流图片容器组件

发布时间 2023-08-05 14:57:44作者: JYeontu

说在前面

?最近在捣鼓自己的个人博客网站,有一个模块需要用到瀑布流图片?展示,于是我就将其封装成了一个组件,以后可以导入就能使用,具体效果如下?:

1649245798(1).png

什么是瀑布流

瀑布流,又称瀑布流式布局。是比较流行的一种网站页面布局,视觉表现为参差不齐的多栏布局,随着页面滚动条向下滚动,这种布局还会不断加载数据块并附加至当前尾部。最早采用此布局的网站是Pinterest,逐渐在国内流行开来。国内大多数清新站基本为这类风格。

实现思路

实现瀑布流布局主要有两种思路,一种是固定图片宽度和容器宽度,动态计算展示图片的列数;另一种则是固定图片展示的列数,通过容器宽度动态计算图片的宽度,接下来让我们一起来看看这两种不同的实现方式吧。

一、固定图片宽度

1649246513(1).png
假设我们现在有这么一个容器和一些图片,应该怎么使用瀑布流布局将图片在容器中展示呢?

1、计算容器可以展示图片列数

设图片的宽度为imgWidth,容器宽度为contentWidth,那么容器可以展示图片的列数就应该为:
parseInt(contentWidth / imgWidth)或者使用Math.floor来计算也可以,就是最大列数应该是容器宽度除于图片宽度向下取整的值,如下图:

1649246825(1).png
剩下的区域不足以放下多一张图片,所以应该向下取整。

2、使用图片填充每一列

知道列数了之后那么我们就可以开始使用图片来对每一列进行填充了,如下图,容器可以划分成这么5列。

1649247109(1).png
接下来用下面图片填充进容器中
1649247273(1).png

Excalidraw 2022_4_6 20_15_26 00_00_00-00_00_30.gif
如上图动画演示一般,最后的结果如下图(数字代表图片顺序):

1649247541(1).png
看到这里是不是马上就明白了,每次图片插入的时候总是会选择当前高度最低的列进行插入如第6张应该插入到第二列的第二张下面,那么用代码要怎么实现呢?

3、代码实现

html
<template>
    <div id="img-content" class="img-content">
        <img
            class="img"
            :style="'width:' + imgWidth + 'px;'"
            v-for="(item, index) in imgList"
            :key="'img' + index"
            :src="item"
        />
    </div>
</template>
CSS
<style lang="scss" scoped>
.img-content {
    height: 30vh;
    width: 320px;
    position: relative;
    .img {
        position: absolute;
        vertical-align: top;
        margin: 3px;
    }
}
</style>
JavaScript
参数配置

我们需要接收参数有:

  • (1)imgList:展示的图片列表;
  • (2)imgWidth:图片的大小;
  • (3)imgMargin:图片的间距。
props: {
    imgList: {
        type: Array,
        default: () => []
    },
    imgWidth: {
        type: Number,
        default: 100
    },
    imgMargin: {
        type: Number,
        default: 3
    }
},
图片定位计算
  • (1)heightArr:高度数组
    我们可以定义一个heightArr变量来保存当前每一列的使用高度。
  • (2)minIndex:当前高度最低的列下标
    插入时需要找到当前高度最低的列,将图片插入该列。
  • (3)赋予图片top和left
// 高度数组最小的高度
const minHeight = Math.min(...heightArr);
// 高度数组最小的高度的索引
const minIndex = heightArr.indexOf(minHeight);
item.style.top = minHeight + "px";
item.style.left = minIndex * imgWidth + "px";
  • (4)完整代码
methods: {
    waterfallHandler() {
        const imgWidth = this.imgWidth + this.imgMargin * 2;
        const contentW = document.getElementById("img-content").offsetWidth;
        // 获取图片的列数
        const column = parseInt(contentW / imgWidth);
        // 高度数组
        const heightArr = new Array(column).fill(0);
        const imgList = document.getElementsByClassName("img");
        for (let i = 0; i < imgList.length; i++) {
            const item = imgList[i];
            // 当前元素的高度
            const itemHeight = item.offsetHeight;
            // 高度数组最小的高度
            const minHeight = Math.min(...heightArr);
            // 高度数组最小的高度的索引
            const minIndex = heightArr.indexOf(minHeight);
            item.style.top = minHeight + "px";
            item.style.left = minIndex * imgWidth + "px";
            heightArr[minIndex] += itemHeight;
        }
    }
}
  • (5)监听页面尺寸变化
    页面尺寸发生变化时重新计算。
window.onresize = () => {
    return (() => {
        this.waterfallHandler();
    })();
};

4、完整代码

<template>
    <div id="img-content" class="img-content">
        <img
            class="img"
            :style="'width:' + imgWidth + 'px;'"
            v-for="(item, index) in imgList"
            :key="'img' + index"
            :src="item"
        />
    </div>
</template>

<script>
export default {
    name: "JWaterfall",
    props: {
        imgList: {
            type: Array,
            default: () => []
        },
        imgWidth: {
            type: Number,
            default: 100
        },
        imgMargin: {
            type: Number,
            default: 3
        }
    },
    mounted() {
        window.onresize = () => {
            return (() => {
                this.waterfallHandler();
            })();
        };
        this.waterfallHandler();
    },
    methods: {
        waterfallHandler() {
            const imgWidth = this.imgWidth + this.imgMargin * 2;
            const contentW = document.getElementById("img-content").offsetWidth;
            // 获取图片的列数
            const column = parseInt(contentW / imgWidth);
            // 高度数组
            const heightArr = new Array(column).fill(0);
            const imgList = document.getElementsByClassName("img");
            for (let i = 0; i < imgList.length; i++) {
                const item = imgList[i];
                // 当前元素的高度
                const itemHeight = item.offsetHeight;
                // 高度数组最小的高度
                const minHeight = Math.min(...heightArr);
                // 高度数组最小的高度的索引
                const minIndex = heightArr.indexOf(minHeight);
                item.style.top = minHeight + "px";
                item.style.left = minIndex * imgWidth + "px";
                heightArr[minIndex] += itemHeight;
            }
        }
    }
};
</script>

<style lang="scss" scoped>
.img-content {
    height: 30vh;
    width: 320px;
    position: relative;
    .img {
        position: absolute;
        vertical-align: top;
        margin: 3px;
    }
}
</style>

二、固定图片列数

1、计算每一列宽度

设展示的列数为column,容器宽度为contentWidth,那么容器每一列的宽度就应该为:
parseInt(contentWidth / column)或者使用Math.floor来计算也可以,也即每一张图片的宽度应该为parseInt(contentWidth / column),如下图:
image.png

2、使用图片填充每一列

1649252106(1).png
如上图,每一个li代表一列,只要对其设置浮动float:left;即可,所以这种方法不需要计算图片的具体坐标值,只需要将图片插入到高度最低的列中,具体实现步骤与上一种方法是差不多的,也是需要不断从高度最低的列进行插入,就不再赘述一遍了。

3、代码实现

html
<template>
    <div id="j-water-fall-content"></div>
</template>
CSS
<style lang="scss" scoped>
#j-water-fall-content {
    margin: 0;
    padding: 0;
}
</style>
JavaScript
参数配置

我们需要接收参数有:

  • (1)imgList:展示的图片列表;
  • (2)column:展示的列数;
  • (3)imgMargin:图片的间距。
props: {
    imgList: {
        type: Array,
        default: () => []
    },
    column: {
        type: Number,
        default: 4
    },
    imgMargin: {
        type: Number,
        default: 0.5
    }
},
动态插入图片
createImg() {
    const ul = document.getElementById("j-water-fall-content");
    let trueWidth = Math.floor(
        (100 - this.column * this.imgMargin * 2) / this.column
    );
    for (let i = 0; i < this.column; i++) {
        let li = document.createElement("li");
        li.style.listStyle = "none";
        li.style.float = "left";
        li.style.width = `${trueWidth}%`;
        li.style.margin = `0 ${this.imgMargin}%`;
        ul.appendChild(li);
        this.arr.push(li);
        this.minHeight.push(0);
    }
    let img = new Image();
    img.num = 0;
    img.src = this.imgList[img.num];
    img.style.width = "100%";
    // 当图片加载完后
    img.onload = this.loadHandler;
},
loadHandler(that) {
    const img = that.path[0];
    const minHeight = this.minHeight;
    const arr = this.arr;
    // 高度数组的最小值
    const min = Math.min.apply(null, minHeight);
    // 高度数组的最小值索引
    const minIndex = minHeight.indexOf(min);
    // 克隆一份图片
    const im = img.cloneNode(true);
    // 将图片假如对应最小值索引的容器中
    arr[minIndex].appendChild(im);
    // 更新最小值索引的容器的高度
    minHeight[minIndex] += im.height;
    img.num++;
    img.src = this.imgList[img.num];
}

4、完整代码

<template>
    <div id="j-water-fall-content"></div>
</template>

<script>
export default {
    name: "JWaterfall",
    props: {
        imgList: {
            type: Array,
            default: () => []
        },
        column: {
            type: Number,
            default: 4
        },
        imgMargin: {
            type: Number,
            default: 0.5
        }
    },
    data() {
        return {
            minHeight: [],
            arr: []
        };
    },
    mounted() {
        this.createImg();
    },
    methods: {
        createImg() {
            const ul = document.getElementById("j-water-fall-content");
            let trueWidth = Math.floor(
                (100 - this.column * this.imgMargin * 2) / this.column
            );
            for (let i = 0; i < this.column; i++) {
                let li = document.createElement("li");
                li.style.listStyle = "none";
                li.style.float = "left";
                li.style.width = `${trueWidth}%`;
                li.style.margin = `0 ${this.imgMargin}%`;
                ul.appendChild(li);
                this.arr.push(li);
                this.minHeight.push(0);
            }
            let img = new Image();
            img.num = 0;
            img.src = this.imgList[img.num];
            img.style.width = "100%";
            // 当图片加载完后
            img.onload = this.loadHandler;
        },
        loadHandler(that) {
            const img = that.path[0];
            const minHeight = this.minHeight;
            const arr = this.arr;
            // 高度数组的最小值
            const min = Math.min.apply(null, minHeight);
            // 高度数组的最小值索引
            const minIndex = minHeight.indexOf(min);
            // 克隆一份图片
            const im = img.cloneNode(true);
            // 将图片假如对应最小值索引的容器中
            arr[minIndex].appendChild(im);
            // 更新最小值索引的容器的高度
            minHeight[minIndex] += im.height;
            img.num++;
            img.src = this.imgList[img.num];
        }
    }
};
</script>

<style lang="scss" scoped>
#j-water-fall-content {
    margin: 0;
    padding: 0;
}
</style>

三、对比

在我看来方法二的实现要优于方法一,因为方法一需要一个合适的容器宽度和图片宽度,否则会出现一定空间的留白,这样的展示效果并不美观,如下图:

1649253587(1).png
因为容器宽度与图片的宽度取余过大,所以右边会留白,这样给人的视觉效果就不好了,使用方法二来实现就能很好地填充满容器的空间,如下图:

1649253730(1).png

封装成组件

为了便于自己以后复用,所以我将其放进了自己的一个组件库中,用到的时候可以直接引用。

1649255158.png

使用

如下图,在博客中使用
image.png
博客地址:http://jyeontu.xyz/JYeontuBlog/#/home

组件库地址

说在后面

? 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 ? ,平时也喜欢写些东西,既为自己记录 ?,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 ?,写错的地方望指出,定会认真改进 ?,偶尔也会在自己的公众号(前端也能这么有趣)发一些比较有趣的文章,有兴趣的也可以关注下。在此谢谢大家的支持,我们下文再见 ?。

本文由mdnice多平台发布