CocosCreator ScrollView 性能优化技巧

原理

ScrollView是比较常用的UI组件之一,游戏中的任务榜、排行榜都少不了它,也是UI界面最费的地方。当数据量大的时候,但是效率不好。

为了解决这个问题也有很多方案:

  • 方案一: 用摄像机代替panel进行裁切和移动

  • 方案二: 显示区域外的Item的Active关闭。这个做法不治本,但是确实能让流畅度提高很多。

  • 方案三: 用脚本实现了循环改变子物体位置的功能。

方法1: 效果实在弊端也多,毕竟多了一个摄像机,割裂了UI间的层次关系,斟酌使用。

方法2: 简单实用,效果有限。

方法3:做法复杂,从根本解决问题,应该在开发早期就写好功能,直接使用。(本文采用这种方法)

先看原理图:

1、屏幕可见区。指屏幕上玩家可看可操作的列表区域。

2、缓冲区。指内存中真正创建了的列表所占的区域。

3、content区。指整个ScrollView要显示的区域。

原理就是这样,但实际上为了方便,我改成了这样。

什么都不说了。实干才是硬道理。直接上源码。

ScrollView性能优化实现

const { ccclass, property } = cc._decorator;
@ccclass
export default class BaseScrollView extends cc.ScrollView {

    @property(cc.Prefab)
    cellItemPrefab: cc.Prefab = null;

    @property(cc.ScrollView)
    scrollView: cc.ScrollView = null;

    @property({ tooltip: "是否是垂直滚动" } || cc.Boolean)
    _horizontal: boolean = false;

    @property({ tooltip: "是否是垂直滚动" })
    set horizontal(value) {
        this._horizontal = value;
        this._vertical = !value;
    }
    @property({ tooltip: "是否是垂直滚动" })
    get horizontal() {
        return this._horizontal;
    }


    @property({ tooltip: "是否是水平滚动" } || cc.Boolean)
    _vertical: boolean = true;

    @property({ tooltip: "是否是水平滚动" })
    set vertical(value) {
        this._horizontal = !value;
        this._vertical = value;
    }

    @property({ tooltip: "是否是水平滚动" })
    get vertical() {
        return this._vertical;
    }


    @property(cc.Float)
    spacing: number = 10;

    /** 存放 cell 的列表 */
    cellItemList: cc.Node[] = [];

    /** cell 大小 */
    cellItemTempSize: cc.Size = null;

    /** 滑动之前的 content 的位置 */
    lastContentPosition: cc.Vec2 = cc.v2(0, 0);

    cellDatalist: any[] = [];

    isUpdateFrame: boolean = true;
    start() {

        this.scrollView.content.on("position-changed", this._updateContentView.bind(this));
        // if (this._vertical) {
        //     this.scrollView.content.on("position-changed", this._updateVerticalContentView.bind(this));
        // } else {
        //     this.scrollView.content.on("position-changed", this._updateHorizontalContentView.bind(this));
        // }

        this.initUI();

    }

    /** 初始化UI */
    initUI() {
        // TODO 由子类继承,并实现
    }

    /** 初始化cellData的数据 */
    initCellDataList(cellDataList: any[]) {
        this.cellDatalist = cellDataList;
    }

    /** 创建cell List列表 */
    createCellList() {
        if (this._vertical) {
            this._createVerticalCellList();
        } else {
            this._createHorizontalCellList();
        }
    }

    _createVerticalCellList() {
        let count = 10;
        for (var i = 0; i < this.cellDatalist.length; i++) {
            if (i > count - 1) {
                return;
            }
            var node = cc.instantiate(this.cellItemPrefab);
            if (i == 0) {
                this.cellItemTempSize = node.getContentSize();
                count = Math.ceil(this.node.height / node.height) * 2;
                let height = this.cellDatalist.length * (this.cellItemTempSize.height + this.spacing);
                this.scrollView.content.setContentSize(cc.size(this.scrollView.content.width, height));
            }

            node["cellID"] = i;

            this.scrollView.content.addChild(node);
            this.cellItemList.push(node);
            let logicComponent = this.cellItemList[i].getComponent(this.cellItemPrefab.name);
            if (logicComponent && logicComponent.updateView) {
                logicComponent.updateView(i, this.cellDatalist[i]);
            }

            node.y = - i * (this.cellItemTempSize.height + this.spacing);
        }
    }

    _createHorizontalCellList() {
        let count = 10;
        for (var i = 0; i < this.cellDatalist.length; i++) {
            if (i > count - 1) {
                return;
            }
            var node = cc.instantiate(this.cellItemPrefab);
            if (i == 0) {
                this.cellItemTempSize = node.getContentSize();
                count = Math.ceil(this.node.width / node.width) * 2;
                let width = this.cellDatalist.length * (this.cellItemTempSize.width + this.spacing);
                this.scrollView.content.setContentSize(cc.size(width, this.scrollView.content.height));
            }

            node["cellID"] = i;

            this.scrollView.content.addChild(node);
            this.cellItemList.push(node);

            let logicComponent = this.cellItemList[i].getComponent(this.cellItemPrefab.name);
            if (logicComponent && logicComponent.updateView) {
                logicComponent.updateView(i, this.cellDatalist[i]);
            }
            node.x = (this.cellItemTempSize.width + this.spacing) * i;
        }
    }


    _getPositionInView(item: cc.Node) {
        let worldPos = item.parent.convertToWorldSpaceAR(item.position);
        let viewPos = this.node.convertToNodeSpaceAR(worldPos);
        return viewPos;
    }

    _updateContentView() {
        if (this._vertical) {
            if (this.isUpdateFrame) {
                this.isUpdateFrame = false;
                this.scheduleOnce(this._updateVerticalContentView.bind(this), 0);
            }

        } else {
            if (this.isUpdateFrame) {
                this.isUpdateFrame = false;
                this.scheduleOnce(this._updateHorizontalContentView.bind(this), 0);
            }

        }

    }

    _updateVerticalContentView() {

        let isDown = this.scrollView.content.y < this.lastContentPosition.y;


        let offsetY = (this.cellItemTempSize.height + this.spacing) * this.cellItemList.length;
        let offset = offsetY / 4;
        let newY = 0;

        for (var i = 0; i < this.cellItemList.length; i++) {
            let viewPos = this._getPositionInView(this.cellItemList[i]);
            if (isDown) {
                newY = this.cellItemList[i].y + offsetY;
                if (viewPos.y < -(offset * 3) && newY <= 0) {
                    this.cellItemList[i].y = newY;
                    let idx = this.cellItemList[i]["cellID"] - this.cellItemList.length;
                    let logicComponent = this.cellItemList[i].getComponent(this.cellItemPrefab.name);
                    if (logicComponent && logicComponent.updateView) {
                        logicComponent.updateView(idx, this.cellDatalist[idx]);
                    }
                    this.cellItemList[i]["cellID"] = idx;
                }
            } else {
                newY = this.cellItemList[i].y - offsetY;
                if (viewPos.y > offset && newY > -this.scrollView.content.height) {

                    this.cellItemList[i].y = newY;
                    let idx = this.cellItemList[i]["cellID"] + this.cellItemList.length;
                    let logicComponent = this.cellItemList[i].getComponent(this.cellItemPrefab.name);
                    if (logicComponent && logicComponent.updateView) {
                        logicComponent.updateView(idx, this.cellDatalist[idx]);
                    }
                    this.cellItemList[i]["cellID"] = idx;
                }
            }
        }

        this.lastContentPosition = this.scrollView.content.position;
        this.isUpdateFrame = true;
    }

    _updateHorizontalContentView() {
        let isLeft = this.scrollView.content.x < this.lastContentPosition.x;


        let offsetX = (this.cellItemTempSize.width + this.spacing) * this.cellItemList.length;
        let offset = offsetX / 4;
        let newX = 0;

        for (var i = 0; i < this.cellItemList.length; i++) {
            let viewPos = this._getPositionInView(this.cellItemList[i]);
            if (isLeft) {
                newX = this.cellItemList[i].x + offsetX;
                if (viewPos.x < -offset && newX < this.scrollView.content.width) {
                    this.cellItemList[i].x = newX;
                    let idx = this.cellItemList[i]["cellID"] + this.cellItemList.length;
                    let logicComponent = this.cellItemList[i].getComponent(this.cellItemPrefab.name);
                    if (logicComponent && logicComponent.updateView) {
                        logicComponent.updateView(idx, this.cellDatalist[idx]);
                    }
                    this.cellItemList[i]["cellID"] = idx;
                }
            } else {
                newX = this.cellItemList[i].x - offsetX;
                if (viewPos.x > offset * 3 && newX >= 0) {

                    this.cellItemList[i].x = newX;
                    let idx = this.cellItemList[i]["cellID"] - this.cellItemList.length;
                    let logicComponent = this.cellItemList[i].getComponent(this.cellItemPrefab.name);
                    if (logicComponent && logicComponent.updateView) {
                        logicComponent.updateView(idx, this.cellDatalist[idx]);
                    }
                    this.cellItemList[i]["cellID"] = idx;
                }
            }
        }

        this.lastContentPosition = this.scrollView.content.position;
        this.isUpdateFrame = true;
    }
}
const {ccclass, property} = cc._decorator;
@ccclass
export default class BaseCell extends cc.Component {
    updateView(idx: number, data: any) {
        // TODO 子类继承
    }
}

言外话题:此次实现检查并刷新ScrollView cell 位置,我并没有放在update里面,而是当ScrollView滑动时才每帧刷新。有人会说,为啥不直接放在update里面做更新。考虑到ScrollView在没有滑动是至少update会有一次判断(其实此处的性能也可以直接忽略)。

Demo1(水平滑动)

import BaseScrollView from "./BaseScrollView";
const {ccclass, property} = cc._decorator;
@ccclass
export default class RankScrollView extends BaseScrollView {
    /** 继承父类的方法 */
    initUI() {
        let list: number[] = [];
        for (let i = 0; i < 100; i++) {
            list.push(i);
        }
        this.initCellDataList(list);
        this.createCellList();
    }
}


import BaseCell from "./BaseCell";
const {ccclass, property} = cc._decorator;
@ccclass
export default class Ranktem extends BaseCell {
    @property(cc.Label)
    lable: cc.Label = null;

    updateView(idx: number, data: any) {
        this.lable.string = (data as number).toString();
    }
}

http://www.wazhlh.com/content/uploadfile/201807/682a1532078710.png

Demo2(垂直滑动)

import BaseCell from "./BaseCell";
const {ccclass, property} = cc._decorator;
@ccclass
export default class Ranktem extends BaseCell {
    @property(cc.Label)
    lable: cc.Label = null;

    updateView(idx: number, data: any) {
        this.lable.string = (data as number).toString();
    }
}


import BaseCell from "./BaseCell";

const {ccclass, property} = cc._decorator;
@ccclass
export default class ShopItem extends BaseCell {
    @property(cc.Label)
    lable: cc.Label = null;

    updateView(idx: number, data: any) {
        this.lable.string = (data as number).toString();
    }
}

最终效果图:

本文转自:虣虣博客,让游戏开发更简单
声明:若无特殊注明,本文皆为( 虣虣 )原创,转载请保留文章出处(http://www.wazhlh.com/?post=15)。

20赞

大神有没有cell不一样大小的方案?很多时候cell的大小不一样,或者加载内容变多,大小也要变化

https://github.com/caochao/cocos_creator_proj_base/blob/master/components/scrollview.ts
可以用我这个呀,支持item尺寸不一致。屏幕可见区域外自动回收,对象复用。

2赞

https://github.com/caochao/cocos_creator_proj_base 看我这个循环滚动列表,有说明文档

4赞

我看了你的代码,scrollView set_data的时候,如果传入200个数据,就创建了200个模板。但其实可见的最多10个模板,只要在10个模反来复用即可
建义,先不创建模板,到需要显示的进候再创建。除了item_setter可以再创建另外一个回调B,item_setter本身的功能是获取item的宽高和更新模板,可以将更新模板的功能提取到回调B里面

是不是可以优化一下@caochao

并不是你说的这样。set_data传入200个数据并不会创建200个节点,只是创建数据对象而已。其实最需要优化的就是避免创建大量重复显示节点。数据对象并不会占大量内存。
这里数据对象只是记录了每个显示节点的位置和尺寸,方便滚动的时候判断是否在可见范围内并决定是否回收而已。
当然你要是有好的意见可以说出来看看,也是可以优化的
@522722951

1赞

我看到的是set_data时,你确实调用pack_item,然后调用了spawn_node,假如数据是200个,就是生成了200个预制体的实例。
那生成200个预制体实例这个动作,不会导致卡顿吗。就像我前面所说的,其实我只需要10个预制体的实例就够了。并不需要创建200个@caochao

仔细看代码,这里有回收操作, 所以并不会创建200个prefab.
至于为什么set_data的时候要调用这个函数,在于要记录每个item的尺寸。当然这里可以做个优化,模板的item尺寸可以保存起来,下次遇到同样的模板可以从缓存里取而不执行此函数。。。

pack_item(index:number, data:ScrollItemData):ScrollItem
    {
        let node:cc.Node = this.spawn_node(data.key);
        let [width, height]:[number, number] = this.item_setter.call(this.cb_host, node, data.key, data.data, index);
        let item:ScrollItem = {x:0, y:0, width:width, height:height, data:data, node:node};
        this.recycle_item(item);
        return item;
    }
1赞

如果对你有帮助,麻烦在github上给一个star啦,感谢

1赞

嗯,少看了一行代码。正好有帮助呢,满足我的需求,谢谢。给星星了

有问题及时联系,我可以提供技术支持

好的,感谢

一点赞收藏

大神请受我一拜,Star + Follow了。
有点没看懂,item_setter 是要我们自己实现吗?
如果cell因为赋值导致cell变大,应该调用哪个方法使布局刷新呢?
其实主要是有一个问题一直解决不了,Creator不把节点加载到节点树里面就不能确定节点的具体大小,直接赋值后得到的大小是不对的。

item_setter是根据数据刷新item,返回设置数据后的尺寸,这个当然是你自己实现了。你可以参考仓库readme.md说明文件有相关例子。
如果cell因为赋值导致变大,你需要返回变化后的尺寸,然后调用一次layout_items方法对该cell及其之后的所有cell重新排列位置

大佬 有没有使用的demo 我比较菜 没看懂怎么用

赞赞赞,mark

https://github.com/caochao/cocos_creator_proj_base 这里有使用方法啊

大佬 这个

是代表什么的呢

item模板及对应的key,key你可以自定义的。