列表视图性能优化

前言

滚动列表,这种东西在游戏中很常见。而cocos creator 中的 ScrollView + Layout 基本可以实现大部分需求,但如果是 背包系统、排行榜、好友列表功能等数据量庞大的列表,ScrollView + Layout 会严重影响性能。


# 一. 实现方式

在创建多个类表项的时候,只需要创建超过屏幕范围数量的 Item,然后,只需要增加 content 的长度,到所有 item 的长度之后,就可以了。

在滑动的时候,按照上面的逻辑处理,就可以实现循环滚动。


# 二. 表现效果

# 1. 布局方式:

支持: 垂直布局,水平布局,网格布局(水平网格、垂直网格)

# 2. 功能:

  • 在末尾增加一项;
  • 在末尾增加一列;
  • 在指定位置添加一项;
  • 删除某一项;
  • 更改某一项;
  • 手动滚动到顶部;
  • 手动滚动到底部;
  • 手动滚动到某一项;
  • 监听滚动到顶部;
  • 监听滚动到底部;

# 三. 代码逻辑

自定义组件: ListView.js:

/** ============================= 枚举值 ============================= */
/**
 * 列表排列方式
 */
const ListType = cc.Enum({

    // 水平排列
    Horizontal: 1,
    // 垂直排列
    Vertical: 2,
    // 网格排列
    Grid: 3
});

/**
 * 网格布局中的方向
 */
const GridAxisType = cc.Enum({

    // 水平排列
    Horizontal: 1,
    // 垂直排列
    Vertical: 2,
});
/** ================================================================ */


cc.Class({
    extends: cc.Component,

    editor:{
        menu:"自定义组件/ListView"
    },
    
    properties: {

        /** ============================= 属性面板 ============================= */

        listType: {
            type: ListType,
            default: ListType.Horizontal,
            tooltip: '排列方式',
            notify: function () {
                this.onListTypeChange();
            },
        },

        listItem: {
            type: cc.Node,
            default: null,
            tooltip: '列表项:Node节点',
            notify: function () {
                this.onListTypeChange();
            },
        },

        itemName: {
            default: 'itemName',
            tooltip: '列表项:脚本名称',
            notify: function () {
                this.onListTypeChange();
            },
        },

        gridAxisType: {
            type: GridAxisType,
            default: GridAxisType.Vertical,
            tooltip: '网格布局的方向',
            visible: false,
            notify: function () {
                this.onListTypeChange();
            },
        },

        spaceX: {
            type: cc.Float,
            default: 0,
            tooltip: '列表项: 之间的 x 间距',
            visible: true,
            notify: function () {
                this.onListTypeChange();
            },
        },

        spaceY: {
            type: cc.Float,
            default: 0,
            tooltip: '列表项: 之间的 y 间距',
            visible: false,
            notify: function () {
                this.onListTypeChange();
            },
        },

        paddingTop : {
            type: cc.Float,
            default: 0,
            tooltip: '列表: 上间距',
            visible: false,
            notify: function () {
                this.onListTypeChange();
            },
        },

        paddingBottom : {
            type: cc.Float,
            default: 0,
            tooltip: '列表: 下间距',
            visible: false,
            notify: function () {
                this.onListTypeChange();
            },
        },

        paddingLeft : {
            type: cc.Float,
            default: 0,
            tooltip: '列表: 左间距',
            visible: true,
            notify: function () {
                this.onListTypeChange();
            },
        },

        paddingRight : {
            type: cc.Float,
            default: 0,
            tooltip: '列表: 右间距',
            visible: true,
            notify: function () {
                this.onListTypeChange();
            },
        },

        scrollTopEvent: {
            type: cc.Component.EventHandler,
            default: [],
            tooltip: '列表: 滚动到顶部时会触发该回调'
        },

        scrollBottomEvent: {
            type: cc.Component.EventHandler,
            default: [],
            tooltip: '列表: 滚动到底部时会触发该回调'
        },
    },

    /** ============================= 系统方法 ============================= */

    ctor () {

        this.onListTypeChange();
    },

    onLoad () {

        // ====================== 滚动容器 ===============================
        // 列表容器
        this.scrollView = this.node.getComponent(cc.ScrollView);

        // 列表内容容器
        this.content = this.scrollView.content;
        this.content.anchorX = 0;
        this.content.anchorY = 1;
        this.content.removeAllChildren();

        this._initListener();

        //======================== 列表项 ===========================
        // 列表项数据
        this.itemDataList = [];
        // 应创建的实例数量
        this.spawnCount = 0;
        // 存放列表项实例的数组
        this.itemList = [];
        // item的高度
        this.itemHeight = this.listItem.height;
        // item的宽度
        this.itemWidth = this.listItem.width;
        // 存放不再使用中的列表项 */
        this.itemPool = [];

        //======================= 计算参数 ==========================

        // 距离scrollView中心点的距离,超过这个距离的item会被重置,
        // 一般设置为 scrollVIew.height/2 + item.heigt/2 + space,因为这个距离item正好超出scrollView显示范围
        this.halfScrollView = 0;

        // 上一次content的X值
        // 用于和现在content的X值比较,得出是向左还是向右滚动
        this.lastContentPosX = 0;

        //上一次content的Y值
        // 用于和现在content的Y值比较,得出是向上还是向下滚动
        this.lastContentPosY = 0;

        // 网格行数
        this.gridRow = 0;
        // 网格列数
        this.gridCol = 0;
        // 刷新时间,单位s
        this.updateTimer = 0;
        // 刷新间隔,单位s
        this.updateInterval = 0.05;
        // 是否滚动容器
        this.bScrolling = false;
    },

    update (dt) {

        if (this.bScrolling == false) {
            return;
        }

        this.updateTimer += dt;
        if (this.updateTimer < this.updateInterval) {
            return;
        }

        this.updateTimer = 0;
        this.bScrolling = false;
        this._updateArrange();
    },

    onDestroy () {

        // 清理列表项
        for (let i = 0; i < this.itemList.length; i++) {
            if (cc.isValid(this.itemList[i], true)) {
                this.itemList[i].destory();
            }
        }
        this.itemList.length = 0;

        // 清理对象池
        for (let i = 0; i < this.itemPool.length; i++) {
            if (cc.isValid(this.itemList[i], true)) {
                this.itemPool[i].destory();
            }
        }
        this.itemPool.length = 0;


        // 清理列表数据
        this.itemDataList.length = 0;
    },

    /** ============================= 公共方法 ============================= */

    /**
     * 设置列表数据
     * (列表数据重复使用,如果列表数据改变,则需要重新设置一遍数据)
     *
     * @param data  item 数据列表
     */
    setData (data) {

        this.itemDataList = data.slice();
        this._updateContent();
    },

    /**
     * 获取列表数据
     */
    getListData () {

        return this.itemDataList;
    },


    /**
     * 添加一项数据到列表的末尾
     *
     * @param data      数据
     */
    addItem (data) {

        this.itemDataList.push(data);
        this._updateContent();
    },

    /**
     * 添加一组数据到列表的末尾
     *
     * @param dataList  数据列表
     */
    addItems (dataList) {

        for (let i = 0; i < dataList.length; i++) {

            let data = dataList[i];
            this.itemDataList.push(data);
        }
        this._updateContent();
    },


    /**
     * 添加一项数据到列表的指定位置
     *
     * @param data      数据
     * @param index     索引 (从 0 开始)
     */
    addItemAtIndex (data, index) {

        if (this.itemDataList[index] != null || this.itemDataList.length == index) {
            this.itemDataList.splice(index, 1, data);
            this._updateContent();
        }
    },


    /**
     * 删除一项数据
     *
     * @param index     索引 (从 0 开始)
     */
    deleteItem (index) {

        if (this.itemDataList[index] != null) {
            this.itemDataList.splice(index, 1);
            this._updateContent();
        }
    },


    /**
     * 改变一项数据
     *
     * @param data      数据
     * @param index     索引 (从 0 开始)
     */
    changeItem (data, index) {

        if (this.itemDataList[index] != null) {
            this.itemDataList[index] = data;
            this._updateContent();
        }
    },


    /**
     * 滚动到顶部
     */
    scrollToTop () {

        this.scrollToIndex(0);
    },

    /**
     * 滚动到底部
     */
    scrollToBottom () {

        this.scrollToIndex(this.itemDataList.length-1);
    },


    /**
     * 滚动到指定索引
     *
     * @param index     索引
     */
    scrollToIndex (index) {

        if (this.startScroll) {
            console.log('列表还在滚动中');
            return;
        }

        if (this.listType == ListType.Horizontal) {
            this._createList(index, cc.v2(index * (this.itemWidth + this.spaceX), 0));
            return;
        }

        if (this.listType == ListType.Vertical) {
            this._createList(index, cc.v2(0,index * (this.itemHeight + this.spaceY)));
            return;
        }

        if (this.gridAxisType == GridAxisType.Horizontal) {

            let gridCol = parseInt(index / this.gridRow);
            let width   = gridCol * this.itemWidth + (gridCol - 1) * this.spaceX + this.paddingLeft + this.paddingRight;
            this._createList(gridCol * this.gridRow, cc.v2(width, 0));
            return;
        }

        if (this.gridAxisType == GridAxisType.Vertical) {

            let gridRow = parseInt(index / this.gridCol);
            let height = gridRow * this.itemHeight + (gridRow - 1) * this.spaceY + this.paddingTop + this.paddingBottom;
            this._createList(gridRow*this.gridCol, cc.v2(0, height));
            return;
        }
    },


    /** ============================= 私有方法 ============================= */

    _initListener () {

        this.scrollView.node.on('scroll-began', this.onScrollBegin, this);
        this.scrollView.node.on('scroll-ended', this.onScrollEnded, this);

        this.scrollView.node.on('scrolling', this.onScrolling, this);


        if (this.listType == ListType.Horizontal) {
            this.scrollView.node.on('scroll-to-left', this.onScrollToTop, this);
            this.scrollView.node.on('scroll-to-right', this.onScrollToBottom, this);

        } else if (this.listType == ListType.Vertical) {
            this.scrollView.node.on('scroll-to-top', this.onScrollToTop, this);
            this.scrollView.node.on('scroll-to-bottom', this.onScrollToBottom, this);

        } else if (this.listType == ListType.Grid) {

            if (this.gridAxisType == GridAxisType.Horizontal) {
                this.scrollView.node.on('scroll-to-left', this.onScrollToTop, this);
                this.scrollView.node.on('scroll-to-right', this.onScrollToBottom, this);

            } else if (this.gridAxisType == GridAxisType.Vertical) {
                this.scrollView.node.on('scroll-to-top', this.onScrollToTop, this);
                this.scrollView.node.on('scroll-to-bottom', this.onScrollToBottom, this);
            }
        }
    },

    /** ok 获取第一个 item 位置 */
    _updateContent () {

        // 显示列表实例为 0 个
        if (this.itemList.length == 0) {

            this._countListParam();
            this._createList(0, cc.v2(0, 0));
            return;
        }


        if (this.listType == ListType.Horizontal) {

            this.itemList.sort(function (a, b) {
                return a.x - b.x;
            });

        } else if (this.listType == ListType.Vertical) {

            this.itemList.sort(function (a, b) {
                return b.y - a.y;
            });

        } else if (this.listType == ListType.Grid) {

            if (this.gridAxisType == GridAxisType.Horizontal) {

                this.itemList.sort(function (a, b) {
                    return b.y - a.y;
                });

                this.itemList.sort(function (a, b) {
                    return a.x - b.x;
                });

            } else if (this.gridAxisType == GridAxisType.Vertical) {

                this.itemList.sort(function (a, b) {
                    return a.x - b.x;
                });

                this.itemList.sort(function (a, b) {
                    return b.y - a.y;
                });
            }
        }

        this._countListParam();

        let item = this.itemList[0];
        let startIndex = item.getComponent(this.itemName).itemIndex;

        if (this.listType == ListType.Grid && this.gridAxisType == GridAxisType.Horizontal) {
            startIndex += (startIndex + this.spawnCount) % this.gridRow;

        } else if (this.listType == ListType.Grid && this.gridAxisType == GridAxisType.Vertical) {
            startIndex += (startIndex + this.spawnCount) % this.gridCol;
        }

        let offset = this.scrollView.getScrollOffset();
        offset.x = -offset.x;

        console.log('--------', offset);

        this._createList(startIndex, offset);
    },


    /** ok 计算列表参数 */
    _countListParam () {

        let dataLen = this.itemDataList.length;

        // spawnCount, 比当前scrollView容器能放下的item数量再加上2个

        if (this.listType == ListType.Horizontal) {

            let padding = this.paddingLeft + this.paddingRight;

            this.scrollView.horizontal  = true;
            this.scrollView.vertical    = false;
            this.content.width          = dataLen * this.itemWidth + (dataLen - 1) * this.spaceX + padding;
            this.content.height         = this.content.parent.height;
            this.spawnCount             = Math.round(this.scrollView.node.width / (this.itemWidth + this.spaceX)) + 2;
            this.halfScrollView         = this.scrollView.node.width / 2 + this.itemWidth / 2 + this.spaceX;
            return;
        }


        if (this.listType == ListType.Vertical) {

            let padding = this.paddingTop + this.paddingBottom;

            this.scrollView.horizontal  = false;
            this.scrollView.vertical    = true;
            this.content.width          = this.content.parent.width;
            this.content.height         = dataLen * this.itemHeight + (dataLen - 1) * this.spaceY + padding;
            this.spawnCount             = Math.round(this.scrollView.node.height / (this.itemHeight + this.spaceY)) + 2;
            this.halfScrollView         = this.scrollView.node.height / 2 + this.itemHeight / 2 + this.spaceY;
            return;
        }


        if (this.gridAxisType == GridAxisType.Horizontal) {

            let paddingL = this.paddingLeft + this.paddingRight;
            let paddingT = this.paddingTop + this.paddingBottom;

            this.scrollView.horizontal  = true;
            this.scrollView.vertical    = false;
            this.content.height         = this.content.parent.height;

            if (paddingT + this.itemHeight + this.spaceY > this.content.height) {
                this.paddingTop     = 0;
                this.paddingBottom  = 0;
                console.error("paddingTop 或 paddingBottom 过大");
            }

            this.gridRow        = Math.floor((this.content.height - paddingT) / (this.itemHeight + this.spaceY));
            this.gridCol        = Math.ceil(dataLen / this.gridRow);
            this.content.width  = this.gridCol * this.itemWidth + (this.gridCol - 1) * this.spaceX + paddingL;
            this.spawnCount     = Math.round(this.scrollView.node.width / (this.itemWidth + this.spaceX)) * this.gridRow + this.gridRow * 2;
            this.halfScrollView = this.scrollView.node.width / 2 + this.itemWidth / 2 + this.spaceX;
            return;
        }


        if (this.gridAxisType == GridAxisType.Vertical) {

            let paddingL = this.paddingLeft + this.paddingRight;
            let paddingT = this.paddingTop + this.paddingBottom;

            this.scrollView.horizontal  = false;
            this.scrollView.vertical    = true;
            this.content.width         = this.content.parent.width;

            if (paddingL + this.itemWidth + this.spaceX > this.content.width) {
                this.paddingLeft   = 0;
                this.paddingRight  = 0;
                console.error("paddingTop 或 paddingBottom 过大");
            }

            this.gridCol        = Math.floor((this.content.width - paddingL) / (this.itemWidth + this.spaceX));
            this.gridRow        = Math.ceil(dataLen / this.gridCol);
            this.content.height = this.gridRow * this.itemHeight + (this.gridRow - 1) * this.spaceY + paddingT;
            this.spawnCount     = Math.round(this.scrollView.node.height / (this.itemHeight + this.spaceY)) * this.gridCol + this.gridCol * 2;
            this.halfScrollView = this.scrollView.node.height / 2 + this.itemHeight / 2 + this.spaceY;
            return;
        }
    },


    /**
     * ok 创建列表
     *
     * @param startIndex    起始显示的数据索引 0 表示第一项
     * @param offset        scrollView 偏移量
     */
    _createList (startIndex, offset) {

        if (this.itemDataList.length > this.spawnCount && (startIndex + this.spawnCount - 1) >= this.itemDataList.length) {
            // 当需要显示的长度 > 虚拟列表长度, 删除末尾几个数据时,列表需要重置位置到最低端

            startIndex = this.itemDataList.length - this.spawnCount;
            offset = this.scrollView.getMaxScrollOffset();

        } else if (this.itemDataList.length <= this.spawnCount) {
            // 当需要显示的长度 <= 虚拟列表长度,隐藏多余的虚拟列表项

            startIndex = 0;
        }

        for (let i = 0; i < this.spawnCount; i++) {

            let item = null;

            if (i + startIndex < this.itemDataList.length) {
                // 需要显示的数据索引在范围内: item显示

                item = this.itemList[i];
                if (item == null) {
                    item = this._getItem();
                    this.itemList.push(item);
                    item.parent = this.content;
                }

            } else {
                // 需要显示的数据索引超过了数据范围,item 隐藏

                if (this.itemList.length > (this.itemDataList.length - startIndex)) {
                    item = this.itemList.pop();
                    item.removeFromParent();
                    this.itemPool.push(item);
                }
                continue;
            }

            let itemComponent = item.getComponent(this.itemName);
            itemComponent.itemIndex = i + startIndex;
            itemComponent.data = this.itemDataList[i + startIndex];
            itemComponent.dataChanged();

            // 因为content的锚点X是0 :
            // 所以item的x值: 是content.with/2表示居中,锚点Y是1,
            // 所以item的y值: 从content顶部向下是0到负无穷。所以item.y= -item.height/2时,是在content的顶部。

            if (this.listType == ListType.Horizontal) {

                let x = item.width * (0.5 + i + startIndex) + this.spaceX * (i + startIndex) + this.paddingLeft;
                let y = -this.content.height / 2;
                item.setPosition(x, y);

            } else if (this.listType == ListType.Vertical) {

                let x = this.content.width / 2;
                let y = -item.height * (0.5 + i + startIndex) - this.spaceY * (i + startIndex) - this.paddingTop;
                item.setPosition(x, y);

            } else if (this.listType == ListType.Grid) {

                if (this.gridAxisType == GridAxisType.Horizontal) {

                    let row = (i + startIndex) % this.gridRow;
                    let col = Math.floor((i + startIndex) / this.gridRow);
                    let x   = item.width * (0.5 + col) + this.spaceX * col + this.paddingLeft;
                    let y   = -item.height * (0.5 + row) - this.spaceY * row - this.paddingTop;
                    item.setPosition(x, y);
                    item.opacity = 255;

                } else if (this.gridAxisType == GridAxisType.Vertical) {

                    let row = Math.floor((i + startIndex) / this.gridCol);
                    let col = (i + startIndex) % this.gridCol;
                    let x   = item.width * (0.5 + col) + this.spaceX * col + this.paddingLeft;
                    let y   = -item.height * (0.5 + row) - this.spaceY * row - this.paddingTop;
                    item.setPosition(x, y);
                    item.opacity = 255;
                }
            }
        }

        this.scrollView.scrollToOffset(offset);
    },


    /** ok 从对象池中获取节点 */
    _getItem () {

        if (this.itemPool.length == 0) {
            return cc.instantiate(this.listItem);
        } else {
            return this.itemPool.pop();
        }
    },


    /** ok 获取item在scrollView的局部坐标 */
    _getPositionInView(item) {

        let worldPos = item.parent.convertToWorldSpaceAR(item.position);
        let viewPos = this.scrollView.node.convertToNodeSpaceAR(worldPos);
        return viewPos;
    },


    /** ok 更新排列 */
    _updateArrange () {

        if (this.listType == ListType.Horizontal) {
            this._updateHorizontal();
            return;
        }

        if (this.listType == ListType.Vertical) {
            this._updateVertical();
            return;
        }

        if (this.gridAxisType == GridAxisType.Horizontal) {
            this._updateGridHorizontal();
            return;
        }

        if (this.gridAxisType == GridAxisType.Vertical) {
            this._updateGridVertical();
            return;
        }
    },


    /** ok 更新水平排列 */
    _updateHorizontal () {

        let items       = this.itemList;
        let item        = null;
        let bufferZone  = this.halfScrollView;
        let isRight     = this.scrollView.content.x > this.lastContentPosX;
        let offset      = (this.itemWidth + this.spaceX) * items.length;

        for (let i = 0; i < items.length; i++) {

            item = items[i];
            let viewPos = this._getPositionInView(item);
            if (isRight) {
                //item右滑时,超出了scrollView右边界,将item移动到左方复用,item移动到左方的位置必须不超过content的左边界
                if (viewPos.x > bufferZone && item.x - offset - this.paddingLeft > 0) {
                    let itemComponent = item.getComponent(this.itemName);
                    let itemIndex = itemComponent.itemIndex - items.length;
                    itemComponent.itemIndex = itemIndex;
                    itemComponent.data = this.itemDataList[itemIndex];
                    itemComponent.dataChanged();
                    item.x = item.x - offset;
                }

            } else {
                //item左滑时,超出了scrollView左边界,将item移动到右方复用,item移动到右方的位置必须不超过content的右边界
                if (viewPos.x < -bufferZone && item.x + offset + this.paddingRight < this.content.width) {
                    let itemComponent = item.getComponent(this.itemName);
                    let itemIndex = itemComponent.itemIndex + items.length;
                    itemComponent.itemIndex = itemIndex;
                    itemComponent.data = this.itemDataList[itemIndex];
                    itemComponent.dataChanged();
                    item.x = item.x + offset;
                }
            }
        }
        this.lastContentPosX = this.scrollView.content.x;
    },


    /** ok 更新垂直排列 */
    _updateVertical () {

        let items       = this.itemList;
        let item        = null;
        let bufferZone  = this.halfScrollView;
        let isUp        = this.scrollView.content.y > this.lastContentPosY;
        let offset      = (this.itemHeight + this.spaceY) * items.length;

        for (let i = 0; i < items.length; i++) {

            item = items[i];
            let viewPos = this._getPositionInView(item);
            if (isUp) {

                //item上滑时,超出了scrollView上边界,将item移动到下方复用,item移动到下方的位置必须不超过content的下边界
                if (viewPos.y > bufferZone && item.y - offset - this.paddingBottom > -this.content.height) {
                    let itemComponent = item.getComponent(this.itemName);
                    let itemIndex = itemComponent.itemIndex + items.length;
                    itemComponent.itemIndex = itemIndex;
                    itemComponent.data = this.itemDataList[itemIndex];
                    itemComponent.dataChanged();
                    item.y = item.y - offset;
                }

            } else {
                //item下滑时,超出了scrollView下边界,将item移动到上方复用,item移动到上方的位置必须不超过content的上边界
                if (viewPos.y < -bufferZone && item.y + offset + this.paddingTop < 0) {
                    let itemComponent = item.getComponent(this.itemName);
                    let itemIndex = itemComponent.itemIndex - items.length;
                    itemComponent.itemIndex = itemIndex;
                    itemComponent.data = this.itemDataList[itemIndex];
                    itemComponent.dataChanged();
                    item.y = item.y + offset;
                }
            }
        }
        this.lastContentPosY = this.scrollView.content.y;
    },


    /** ok 更新网格水平排列 */
    _updateGridHorizontal () {

        let items       = this.itemList;
        let item        = null;
        let bufferZone  = this.halfScrollView;
        let isRight     = this.scrollView.content.x > this.lastContentPosX;
        let offset      = (this.itemWidth + this.spaceX) * (this.spawnCount / this.gridRow);

        for (let i = 0; i < items.length; i++) {
            item = items[i];
            let viewPos = this._getPositionInView(item);
            if (isRight) {

                //item右滑时,超出了scrollView右边界,将item移动到左方复用,item移动到左方的位置必须不超过content的左边界
                if (viewPos.x > bufferZone && item.x - offset - this.paddingLeft > 0) {

                    let itemComponent = item.getComponent(this.itemName);
                    let itemIndex = itemComponent.itemIndex - (this.spawnCount / this.gridRow) * this.gridRow;
                    if (this.itemDataList[itemIndex] != null) {
                        item.x          = item.x - offset;
                        itemComponent.itemIndex = itemIndex;
                        itemComponent.data      = this.itemDataList[itemIndex];
                        itemComponent.dataChanged();
                        item.opacity    = 255;

                    } else {
                        item.x          = item.x - offset;
                        itemComponent.itemIndex = itemIndex;
                        item.opacity    = 0;
                    }
                }

            } else {

                //item左滑时,超出了scrollView左边界,将item移动到右方复用,item移动到右方的位置必须不超过content的右边界
                if (viewPos.x < -bufferZone && item.x + offset + this.paddingRight < this.content.width) {

                    let itemComponent = item.getComponent(this.itemName);
                    let itemIndex = itemComponent.itemIndex + (this.spawnCount / this.gridRow) * this.gridRow;
                    if (this.itemDataList[itemIndex] != null) {
                        item.x          = item.x + offset;
                        itemComponent.itemIndex = itemIndex;
                        itemComponent.data      = this.itemDataList[itemIndex];
                        itemComponent.dataChanged();
                        item.opacity    = 255;

                    } else {
                        item.x          = item.x + offset;
                        itemComponent.itemIndex = itemIndex;
                        item.opacity    = 0;
                    }
                }
            }
        }
        this.lastContentPosX = this.scrollView.content.x;
    },


    /** ok 更新网格垂直排列 */
    _updateGridVertical () {

        let items       = this.itemList;
        let item        = null;
        let bufferZone  = this.halfScrollView;
        let isUp        = this.scrollView.content.y > this.lastContentPosY;
        let offset      = (this.itemHeight + this.spaceY) * (this.spawnCount / this.gridCol);

        for (let i = 0; i < items.length; i++) {

            item = items[i];
            let viewPos = this._getPositionInView(item);

            if (isUp) {

                //item上滑时,超出了scrollView上边界,将item移动到下方复用,item移动到下方的位置必须不超过content的下边界
                if (viewPos.y > bufferZone && item.y - offset - this.paddingBottom > -this.content.height) {

                    let itemComponent = item.getComponent(this.itemName);
                    let itemIndex = itemComponent.itemIndex + (this.spawnCount / this.gridCol) * this.gridCol;

                    if (this.itemDataList[itemIndex] != null) {
                        item.y          = item.y - offset;
                        itemComponent.itemIndex = itemIndex;
                        itemComponent.data      = this.itemDataList[itemIndex];
                        itemComponent.dataChanged();
                        item.opacity    = 255;
                    } else {
                        item.y          = item.y - offset;
                        itemComponent.itemIndex = itemIndex;
                        item.opacity    = 0;
                    }
                }

            } else {

                //item下滑时,超出了scrollView下边界,将item移动到上方复用,item移动到上方的位置必须不超过content的上边界
                if (viewPos.y < -bufferZone && item.y + offset + this.paddingTop < 0) {

                    let itemComponent = item.getComponent(this.itemName);
                    let itemIndex = itemComponent.itemIndex - (this.spawnCount / this.gridCol) * this.gridCol;

                    if (this.itemDataList[itemIndex] != null) {
                        item.y          = item.y + offset;
                        itemComponent.itemIndex = itemIndex;
                        itemComponent.data      = this.itemDataList[itemIndex];
                        itemComponent.dataChanged();
                        item.opacity    = 255;

                    } else {
                        item.y          = item.y + offset;
                        itemComponent.itemIndex = itemIndex;
                        item.opacity    = 0;
                    }
                }
            }
        }

        this.lastContentPosY = this.scrollView.content.y;
    },



    /** ============================= 属性面板 ============================= */


    /**
     * 列表排列方式改变
     */
    onListTypeChange () {

        cc.Class.Attr.setClassAttr(this, 'gridAxisType', 'visible', false);
        cc.Class.Attr.setClassAttr(this, 'spaceX', 'visible', false);
        cc.Class.Attr.setClassAttr(this, 'spaceY', 'visible', false);
        cc.Class.Attr.setClassAttr(this, 'paddingTop', 'visible', false);
        cc.Class.Attr.setClassAttr(this, 'paddingBottom', 'visible', false);
        cc.Class.Attr.setClassAttr(this, 'paddingLeft', 'visible', false);
        cc.Class.Attr.setClassAttr(this, 'paddingRight', 'visible', false);

        if (this.listType == ListType.Grid) {
            cc.Class.Attr.setClassAttr(this, 'gridAxisType', 'visible', true);
            cc.Class.Attr.setClassAttr(this, 'spaceX', 'visible', true);
            cc.Class.Attr.setClassAttr(this, 'spaceY', 'visible', true);
            cc.Class.Attr.setClassAttr(this, 'paddingLeft', 'visible', true);
            cc.Class.Attr.setClassAttr(this, 'paddingRight', 'visible', true);
            cc.Class.Attr.setClassAttr(this, 'paddingTop', 'visible', true);
            cc.Class.Attr.setClassAttr(this, 'paddingBottom', 'visible', true);
            return;
        }

        if (this.listType == ListType.Horizontal) {
            cc.Class.Attr.setClassAttr(this, 'spaceX', 'visible', true);
            cc.Class.Attr.setClassAttr(this, 'paddingLeft', 'visible', true);
            cc.Class.Attr.setClassAttr(this, 'paddingRight', 'visible', true);
            return;
        }

        if (this.listType == ListType.Vertical) {
            cc.Class.Attr.setClassAttr(this, 'spaceY', 'visible', true);
            cc.Class.Attr.setClassAttr(this, 'paddingTop', 'visible', true);
            cc.Class.Attr.setClassAttr(this, 'paddingBottom', 'visible', true);
            return;
        }
    },


    /** 开始滚动 */
    onScrollBegin () {

        this.startScroll = true;
    },

    /** 滚动结束 */
    onScrollEnded () {

        this.startScroll = false;
    },

    /** 列表滚动 */
    onScrolling () {

        this.bScrolling = true;
    },

    /** 列表滚动到顶部 */
    onScrollToTop () {

        if (this.scrollTopEvent.length > 0) {
            this.scrollTopEvent[0].emit();
        }
    },

    /** 列表滚动到底部 */
    onScrollToBottom () {

        if (this.scrollBottomEvent.length > 0) {
            this.scrollBottomEvent[0].emit();
        }
    },
});


# 四. 使用方式

# 1. 控制面板

属性 功能
listType 排列方式:水平、垂直、网格三种方式
listItem 列表项:node 节点
itemName 列表项:脚本名称
gridAxisType 网格布局方向:(水平、垂直) (lsitType = ListType.Grid 时会显示)
spaceX 列表项间的 X 轴间距 (lsitType = ListType.HorizontallsitType = ListType.Grid 时会显示)
spaceY 列表项间的 Y 轴间距 (lsitType = ListType.VerticallsitType = ListType.Grid 时会显示)
paddingTop 列表上间距 (lsitType = ListType.VerticallsitType = ListType.Grid 时会显示)
paddingBottom 列表下间距 (lsitType = ListType.VerticallsitType = ListType.Grid 时会显示)
paddingLeft 列表上间距 (lsitType = ListType.HorizontallsitType = ListType.Grid 时会显示)
paddingRight 列表上间距 (lsitType = ListType.HorizontallsitType = ListType.Grid 时会显示)
scrollTopEvent 列表: 滚动到 顶部/左部 时会触发该回调
scrollBottomEvent 列表: 滚动到 底部/右部 时会触发该回调

# 2. 代码

# a. 导入自定义组件:

import ListView from "../public_util/ListView";

# b. 定义组件:

properties: {
	titleL: 	cc.Label,
    listView: 	ListView,
 },

# c. 初始化列表:

	this.listView.setData(this.list);

# d.Item代码:

cc.Class({
    extends: cc.Component,

    properties: {

        indexL: cc.Label,
        nameL: cc.Label,

		// 数据
        data: {
            default: null,
            visible: false,
        },

		// 索引
        itemIndex: {
            default: 0,
            visible: false
        }
    },

    /** 列表数据改变时触发 */
    dataChanged () {

        this.indexL.string = '第' + this.itemIndex + '位';
        this.nameL.string  = '名称: ' + this.data.name;
    },
});

# e. 常用方法:

    /** 在末尾添加一项 */
    onClickAddOne () {

        let data = {name: window.Utils.getRandomInt(0, 100)};
        this.list.push(data);

        this.listView.addItem(data);
    },

    /** 在末尾添加一组 */
    onClickAddList () {

        let list = [];
        for (let i = 0; i < window.Utils.getRandomInt(2, 6); i++) {
            let data = {name: window.Utils.getRandomInt(0, 100)};
            list.push(data);
            this.list.push(data);
        }

        this.listView.addItems(list);
    },

    /** 随机移除一项 */
    onClickRemove () {

        let index = window.Utils.getRandomInt(0, this.list.length);
        if (this.list[index] != null) {
            this.list.splice(index, 1);
        }

        this.listView.deleteItem(index);
    },

    /** 更新某项数据 */
    onClickUpdate () {

        let data = {name: window.Utils.getRandomInt(0, 100)};
        this.list[7] = data;
        this.listView.changeItem(data, 7);
    },

    /** 手动滚动到顶部 */
    onClickScrollToTop () {

        this.listView.scrollToTop();
    },

    /** 手动滚动到底部 */
    onClickScrollToBottom () {
        this.listView.scrollToBottom();
    },

    /** 手动滚动到某一项 */
    onClickScrollToIndex () {

        this.listView.scrollToIndex(8);
    },

    /** 回调: 滑到底部 */
    onScrollBottom () {

        // 做上拉加载逻辑
        this.onClickAddList();

        console.log('滑到底部了。。。。。。');
    },

    /** 回调: 滑到顶部 */
    onScrollTop () {

        console.log('滑到顶部了。。。。。。');
    },