首页 > 技术文章 > CocosCreate之ScrollView动态更新数据的实现原理

foupwang 2017-11-03 17:53 原文

  ScrollView是比较常用的UI组件之一,游戏中的任务榜、排行榜都少不了它,实际使用中存在一个问题,例如:在排行榜中要显示前100名玩家,如果真的把这100名玩家的信息全部创建,并加载进ScrollView,对移动设备的宝贵内存会是巨大的浪费。其实玩家在屏幕上总能看到的最多只有7、8项而已,所以实际上只用创建比显示多一点的数量,再通过缓冲区实时动态更新数据,就可以给玩家呈现出尽可能多数量的列表(依内存而定,理论上无限),即节省内存,也不影响性能。

        在CocosCreate官方提供的example中,有个ScrollView的例子,就使用了动态更新数据,我把它摘出来做为一个单独的工程,加上中文注释,并对代码稍做改动,运行截图如下所示:

  可以看到,列表中显示一共有100行,但一屏最多展示7.5行,而实际上在内存中只真正创建了15项,所有看到的这100行,都是在上下滚动事件中,通过动态更新这15项的坐标和内容来实现的。要了解它的运行原理,先看下图:

                         

  如图把ScrollView分成三部分,按区域从小到大依次是:

1、屏幕可见区。指屏幕上玩家可看可操作的列表区域,在此demo中有7.5行;

2、缓冲区。指内存中真正创建了的列表所占的区域,在此demo中有15行;

3、content区。指整个ScrollView要显示的区域,在此demo中有100行;

  下面再分三种情况讲解:

1、刚初始化完成时:此时在右侧按钮上提示有100行,实际上只创建了第1-15行,而玩家能看到的是第1-7行。如果玩家想要看到更多,必然会向上或向下滚动屏幕;

2、向上滚动时:在移动设备上,如果玩家想要看到下面的行,所做的操作是触摸往上滑动,则整个content区往上移动,也带动content区的item往上移动,update函数会不断遍历所创建的15项item,如果检测到某item的y坐标超出了缓冲区的上边界(该item已经被玩家看过或不想再看),则把该item往下移动一个缓冲区的高度(移动该item到玩家即将看到的位置),并更新它的显示ID;

3、向下滚动时:同理,content区的item往下移动,update不断遍历所创建的15项item,如果检测到某item的y坐标越过了缓冲区的下边界(该item已经被玩家看过或不想再看),则把该item往上移动一个缓冲区的高度(移动该item到玩家即将看到的位置),并更新它的显示ID;

  关键代码如下所示:

    // 返回item在ScrollView空间的坐标值
    getPositionInView: function (item) {
        let worldPos = item.parent.convertToWorldSpaceAR(item.position);
        let viewPos = this.scrollView.node.convertToNodeSpaceAR(worldPos);
        return viewPos;
    },

    // 每帧调用一次。根据滚动位置动态更新item的坐标和显示(所以spawnCount可以比totalCount少很多)
    update: function(dt) {
        this.updateTimer += dt;
        if (this.updateTimer < this.updateInterval) {
            return; // we don't need to do the math every frame
        }
        this.updateTimer = 0;
        let items = this.items;
        // 如果当前content的y坐标小于上次记录值,则代表往下滚动,否则往上。
        let isDown = this.scrollView.content.y < this.lastContentPosY;
        // 实际创建项占了多高(即它们的高度累加)
        let offset = (this.itemTemplate.height + this.spacing) * items.length;
        let newY = 0;

        // 遍历数组,更新item的位置和显示
        for (let i = 0; i < items.length; ++i) {
            let viewPos = this.getPositionInView(items[i]);
            if (isDown) {
                // 提前计算出该item的新的y坐标
                newY = items[i].y + offset;
                // 如果往下滚动时item已经超出缓冲矩形,且newY未超出content上边界,
                // 则更新item的坐标(即上移了一个offset的位置),同时更新item的显示内容
                if (viewPos.y < -this.bufferZone && newY < 0) {
                    items[i].setPositionY(newY);
                    let item = items[i].getComponent('Item');
                    let itemId = item.itemID - items.length; // update item id
                    item.updateItem(i, itemId);
                }
            } else {
                // 提前计算出该item的新的y坐标
                newY = items[i].y - offset;
                // 如果往上滚动时item已经超出缓冲矩形,且newY未超出content下边界,
                // 则更新item的坐标(即下移了一个offset的位置),同时更新item的显示内容
                if (viewPos.y > this.bufferZone && newY > -this.content.height) {
                    items[i].setPositionY(newY);
                    let item = items[i].getComponent('Item');
                    let itemId = item.itemID + items.length;
                    item.updateItem(i, itemId);
                }
            }
        }

        // 更新lastContentPosY和总项数显示
        this.lastContentPosY = this.scrollView.content.y;
        this.lblTotalItems.string = "Total Items: " + this.totalCount;
},

  在此demo中,ScrollView列表显示的item其实是个按钮,而它做为预制资源,其实可以在Creator中编辑成各种UI,并不局限于按钮形式。

  最后,附上该Creator工程完整源代码的github地址:https://github.com/foupwang/CocosCreatorScrollViewDemo.git

推荐阅读