原理

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();
    }
} 

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();
}

}


最终效果图:



关注【游戏讲坛】微信公众号,获取最新动态!