阅读 882

瀑布流布局

最近在技术群中总能看见小伙伴问你们瀑布流用的是什么插件?想了想好像我也不太懂瀑布流,于是乎就开始了瀑布流学习之路,也就有了下面这篇文章

写的不好请多担待

什么是瀑布流布局?

瀑布流布局如上图所示,由多列组成(至少两列),且每一列中的元素都是固定宽度,高度不固定。在淘宝的物品列表、百度图片等地方都能看见类似案例。

有几种实现方式?

1.通过css实现(不是本次分享重点)

优点:

  • 使用方便,只需要几行css代码即可

缺点:

  • 兼容性不行(css3)

实现方式:

// 在最外层的父容器的css中设置需要展示的列数,及间隔
columns: 3
gap: 10
复制代码
2.通过JavaScript实现

缺点:

  • 需要引入特定的js库(或者写大量的代码)

优点:

  • 具有较好的兼容性

完整版手写JavaScript代码:

;
(function (doc) {
    var Waterfall = function (el, opt) {
        this.el = document.getElementsByClassName(el)[0];
        this.el.style.position = 'relative';
        this.column = opt.column;
        this.gap = opt.gap;
        this.itemWidth = (this.el.offsetWidth - this.gap * (this.column - 1)) / this.column;
        this.heightArr = [];
        this.onBottomFun = opt.onBottomFun;
    }

    /**
     * 页面初始化
     * @param {Array} data 第一页渲染的数据
     * @param {Number} pageIndex 初始页码
     */
    Waterfall.prototype.init = function (data, pageIndex) {
        this.renderList(data, pageIndex)
        this.bindEvent()
    }

    Waterfall.prototype.bindEvent = function () {
        window.addEventListener('scroll', this.scrollToBottom.bind(this), false)
    }

    Waterfall.prototype.scrollToBottom = function () {
        if (this.getScrollTop() + this.getWindowHeight() === this.getScrollHeight()) {
            this.onBottomFun();
        }
    }

    Waterfall.prototype.getScrollTop = function () {
        var scrollTop = 0,
            bodyScrollTop = 0,
            documentScrollTop = 0;
        if (document.body) {
            bodyScrollTop = document.body.scrollTop;
        }
        if (document.documentElement) {
            documentScrollTop = document.documentElement.scrollTop;
        }
        scrollTop = (bodyScrollTop - documentScrollTop > 0) ? bodyScrollTop : documentScrollTop;
        console.log('scrollTop', scrollTop)
        return scrollTop
    }

    Waterfall.prototype.getScrollHeight = function () {
        var scrollHeight = 0,
            bodyScrollHeight = 0,
            documentScrollHeight = 0;
        if (document.body) {
            bodyScrollHeight = document.body.scrollHeight;
        }
        if (document.documentElement) {
            documentScrollHeight = document.documentElement.scrollHeight;
        }
        scrollHeight = (bodyScrollHeight - documentScrollHeight > 0) ? bodyScrollHeight : documentScrollHeight;
        console.log('scrollHeight', scrollHeight)
        return scrollHeight;
    }

    Waterfall.prototype.getWindowHeight = function () {
        var windowHeight = 0;
        if (document.compatMode === 'CSS1Compat') {
            windowHeight = document.documentElement.clientHeight;
        } else {
            windowHeight = document.body.clientHeight;
        }
        console.log('windowHeight', windowHeight)
        return windowHeight;
    }

    // 渲染列表
    Waterfall.prototype.renderList = function (data, pageIndex) {
        /**
         * DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,
         * 然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。
         * 因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。
         * 因此,使用文档片段通常会带来更好的性能。
         */
        var oFrag = doc.createDocumentFragment(); // 创建文档片段

        data.forEach(function (elm, index) {
            var oItem = doc.createElement('div'),
                oImg = new Image();

            // 设置基本样式
            oItem.style.position = 'absolute';
            oItem.style.overflow = 'hidden';
            oImg.style.width = '100%';
            oImg.style.display = 'block';

            // 设置每个元素的宽高
            oItem.style.width = this.itemWidth + 'px';
            oItem.style.height = elm.height * this.itemWidth / elm.width + 'px';

            oImg.src = elm.img;
            oItem.appendChild(oImg);

            // 第一行元素
            if (index < this.column && pageIndex === 0) {
                oItem.style.top = '0px';
                oItem.style.left = index * (this.itemWidth + this.gap) + 'px';

                // 存储每一列元素的高度
                this.heightArr.push(elm.height * this.itemWidth / elm.width + this.gap);
                // 存储每一列元素的left值
            } else {
                // 非第一行元素

                // 获取目前高度最矮的那一列下标
                var minIndex = getMinIndex(this.heightArr);

                // 将当前元素插入到高度最矮的那一列下方
                oItem.style.top = this.heightArr[minIndex] + 'px';
                oItem.style.left = minIndex * (this.itemWidth + this.gap) + 'px';

                // 更新最矮一列元素的高度
                this.heightArr[minIndex] = this.heightArr[minIndex] + elm.height * this.itemWidth / elm.width + this.gap
            }

            // 将元素插入到文档片段中
            oFrag.appendChild(oItem);
        }, this)

        // 将文档片段插入到DOM树中
        this.el.appendChild(oFrag)
    }

    // 获取数组中最小数值的下标
    function getMinIndex(arr) {
        return arr.indexOf(Math.min.apply(null, arr));
    }

    // 将Waterfall构造函数挂载在window上
    window.Waterfall = Waterfall;

})(document);
复制代码

使用方式:

new Waterfall('J_waterfall', { // J_waterfall 为容器的类名
    column: 2,
    gap: 10,
    onBottomFun: function () { // 当页面触底时触发
        // 触底后在这里进行传入数据等操作
        this.renderList(arr2, pageIndex);
    },
}).init(arr1, 0) // 在初始化的时候传入第一页需要展示的数据及页码0


// 传入的数据必须如下格式
// var arr1 = [{
//    width: 360,
//    height: 460,
//    img: 'https://picsum.photos/360/460?random=1'
// }]
复制代码

瀑布流的实现原理(建议结合上面的手写源码一起看)

  • 首先需要根据配置的column和gap计算出每一个元素的固定宽度

  • 这个时候我们就可以布局第一行元素了

    我们是通过absolute进行布局的,所以第一行元素的top都为0px,每个元素的left为index * (this.itemWidth + this.gap) + 'px'

    index为每个元素的下标

  • 我个人的理解是瀑布流中不存在行的概念(第一行除外),在创建第一行元素的时候需要保存每一列目前的高度。接下去的元素需要插入在高度最短的那一列中,插入后需要更新此列高度,依次循环

    总结

    网络上已经有很多瀑布流布局的插件了,但是最好还是自己写一遍,有利于你理解它,以后碰到问题也更好解决。

文章分类
前端
文章标签