cat
Shioho

# 前言

众所周知,在使用滚动列表 (ScrollView) 时通常都伴随着需要创建大量预制体的场景,如背包系统,由于背包内的数据量其实是十分大的,所以需要创建出大量格子来显示物品。但在运行时进行节点的创建(cc.instantiate)和销毁(node.destroy)操作是非常耗费性能的,因此为了降低性能消耗,那么就做一个可重复利用的循环列表吧~

# 原理分析

既然创建和销毁是十分消耗性能的,那我不销毁不就好了吗,在开始时一次性创建大概量的预制体,在需要的时候显示,在不需要的时候隐藏就好啦。作用于滚动列表,就相当于先一次性创建滚动列表大小可显示数量 + 2(用于列表循环)的预制,当节点预制超出遮罩范围外的时候隐藏,需要新增显示的从已经隐藏的预制中选出一个来重新设置位置坐标并显示即可😆~

# 代码

# 对象池

为了避免频繁创建和销毁预制,对象池肯定是少不了的啦,那么就封装一个泛型对象池吧

  • 构造函数

createFunc : 创建泛型 T 的方法
putBackFunc : 回收泛型 T 时想要执行的方法
clearFunc : 销毁泛型 T 时执行的方法
initNum : 创建对象池时需要初始化 T 类型的数量

constructor(createFunc: () => T, putBackFunc: (t: T) => void, clearFunc: (t: T) => void, initNum: number = 0) {
        ......
    }
  • 公有方法

allocate(): T : 从对象池中取一个对象
recycle(script: T) : 回收一个对象
recycleAll() : 全部回收
clearAll() : 全部销毁

  • 完整代码
export default class ObjectPool<T>{
    private _pool: Array<T>;
    private _createFunc: () => T;
    private _putBackFunc: (t: T) => void;
    private _clearFunc: (t: T) => void;
    private _usedArr: Array<T>;
    constructor(createFunc: () => T, putBackFunc: (t: T) => void, clearFunc: (t: T) => void, initNum: number = 0) {
        this._pool = new Array<T>();
        this._usedArr = new Array<T>();
        this._createFunc = createFunc;
        this._putBackFunc = putBackFunc;
        this._clearFunc = clearFunc;
        this.preloadPool(initNum);
    }
    public allocate(): T {
        if (this._pool.length <= 0)
            this.createNewOne2Pool();
        let script = this._pool.shift();
        this._usedArr.push(script);
        return script;
    }
    public recycle(script: T) {
        if (!script)
            return;
        let usedIdx = this._usedArr.indexOf(script);
        if (usedIdx != -1) {
            this._usedArr.splice(usedIdx, 1);
        }
        this._pool.push(script);
        this._putBackFunc && this._putBackFunc(script);
    }
    public recycleAll() {
        for (let script of this._usedArr) {
            this._pool.push(script);
            this._putBackFunc && this._putBackFunc(script);
        }
        this._usedArr.length = 0;
    }
    public clearAll() {
        for (let script of this._usedArr) {
            this._clearFunc && this._clearFunc(script);
        }
        this._usedArr.length = 0;
        for (let script of this._pool) {
            this._clearFunc && this._clearFunc(script);
        }
        this._pool.length = 0;
    }
    private preloadPool(cnt: number) {
        for (let i = 0; i < cnt; i++) {
            this.createNewOne2Pool();
        }
    }
    private createNewOne2Pool() {
        let script = this._createFunc();
        this._pool.push(script);
        return script;
    }
}

# 循环列表组件

  • 方法介绍

onLoad : 初始化对象池,根据预制体大小和遮罩大小计算显示数量,绑定滚动事件
updateList : 根据数据长度刷新 content 节点高度
onScrolling : 滚动事件,当当前头索引与上一次记录的索引不一致时,判断上滑或下滑,回收超出遮罩外的预制,生成新的显示预制,并刷新节点
refreshItemNode : 刷新节点位置,执行节点刷新回调

  • 完整代码
import { _decorator, Component, Node, Prefab, Mask, instantiate, CCFloat, UITransform, math, ScrollView } from 'cc';
import objectPool from './ObjectPool';
const { ccclass, property, requireComponent } = _decorator;
@ccclass('UIVerticalLoopScrollViewComp')
@requireComponent(ScrollView)
export class UIVerticalLoopScrollViewComp extends Component {
    @property(Prefab)
    itemPref: Prefab
    @property(CCFloat)
    spacing: number
    public OnRefreshItem: (item: Node, idx: number) => void;
    public OverAddShowCnt: number = 1;
    private _itemPool: objectPool<Node>;
    private _itemSize: math.Size;
    private _maskSize: math.Size;
    private _contentSize: math.Size;
    private _content: Node;
    private _maxShowCnt: number;
    private _curShowCnt: number;
    private _curFirstShowIdx: number;
    private _showNodeList: Node[];
    private _totalLength: number;
    init() {
        this._content = this.getComponent(ScrollView).content;
        this._itemSize = this.itemPref.data.getComponent(UITransform).contentSize;
        this._maskSize = this.getComponentInChildren(Mask).node.getComponent(UITransform).contentSize;
        this._maxShowCnt = Math.floor(this._maskSize.height / (this._itemSize.height + this.spacing)) + this.OverAddShowCnt;
        this._itemPool = new objectPool<Node>(() => {
            let node = instantiate(this.itemPref);
            node.setParent(this._content);
            node.setRotationFromEuler(0, 0, 0);
            node.setScale(1, 1, 1);
            node.active = false;
            return node;
        }, node => node.active = false, node => node.destroy(), this._maxShowCnt + 2);
        this.clearAll();
        this.node.on('scrolling', this.onScrolling, this);
    }
    public updateList(len: number) {
        this._totalLength = len;
        this._contentSize = new math.Size(this._maskSize.width, (this.spacing + this._itemSize.height) * this._totalLength + this._itemSize.height / 2);
        this._content.getComponent(UITransform).setContentSize(this._contentSize);
        this._curShowCnt = Math.min(this._maxShowCnt, len);
        if (this._showNodeList.length != this._curShowCnt) {
            this._itemPool.recycleAll();
            for (var i = this._curFirstShowIdx; i < this._curFirstShowIdx + this._curShowCnt; i++) {
                var node = this._itemPool.allocate();
                this._showNodeList.push(node);
                node.active = true;
            }
        }
        for (var i = this._curFirstShowIdx; i < this._curFirstShowIdx + this._curShowCnt; i++) {
            var node = this._showNodeList[i - this._curFirstShowIdx];
            this.refreshItemNode(node, i);
        }
    }
    public clearAll() {
        this._itemPool.recycleAll();
        this._showNodeList = [];
        this._curFirstShowIdx = 0;
        this._totalLength = 0;
    }
    private onScrolling(scrollView: ScrollView) {
        var currOffset = scrollView.getScrollOffset();
        // 范围限制
        var firstShowIdx = Math.floor(currOffset.y / (this.spacing + this._itemSize.height));
        firstShowIdx = firstShowIdx < 0 ? 0 : firstShowIdx;
        firstShowIdx = firstShowIdx > this._totalLength - this._curShowCnt ? this._totalLength - this._curShowCnt : firstShowIdx;
        if (this._curFirstShowIdx != firstShowIdx) {
            // 判断往上滑还是往下滑
            // 上滑
            if (this._curFirstShowIdx > firstShowIdx) {
                //console.log (`从 ${this._curFirstShowIdx} 要变到 ${firstShowIdx}`);
                var cnt = this._curFirstShowIdx - firstShowIdx;
                for (var i = 0; i < cnt; i++) {
                    //console.log ("回收:", this._curFirstShowIdx + this._curShowCnt - i - 1)
                    this._itemPool.recycle(this._showNodeList[this._curShowCnt - i - 1]);
                }
                this._showNodeList.splice(this._curShowCnt - cnt, cnt);
                for (var i = cnt - 1; i >= 0; i--) {
                    var node = this._itemPool.allocate();
                    this._showNodeList.unshift(node);
                    node.active = true;
                    //console.log ("生成:", (firstShowIdx + i))
                    this.refreshItemNode(node, firstShowIdx + i);
                }
            } else {
                //console.log (`从 ${this._curFirstShowIdx} 要变到 ${firstShowIdx}`);
                var cnt = firstShowIdx - this._curFirstShowIdx;
                for (var i = 0; i < cnt; i++) {
                    //console.log ("回收:", (this._curFirstShowIdx + i));
                    this._itemPool.recycle(this._showNodeList[i]);
                }
                this._showNodeList.splice(0, cnt);
                for (var i = 0; i < cnt; i++) {
                    var node = this._itemPool.allocate();
                    this._showNodeList.push(node);
                    node.active = true;
                    //console.log ("生成:", (this._curFirstShowIdx + this._curShowCnt + i))
                    this.refreshItemNode(node, this._curFirstShowIdx + this._curShowCnt + i);
                }
            }
            this._curFirstShowIdx = firstShowIdx;
        }
    }
    private refreshItemNode(node: Node, idx: number) {
        node.position.set(0, -this._itemSize.height / 2 - (this.spacing + this._itemSize.height) * idx);
        this.OnRefreshItem && this.OnRefreshItem(node, idx);
    }
}

# 使用案例

private _datas: string[]
private _scroll: UIVerticalLoopScrollViewComp;
start() {
    // 键盘输入绑定
    input.on(Input.EventType.KEY_UP, this.onKeyDown.bind(this));
    this._scroll = this.getComponent(UIVerticalLoopScrollViewComp);
    this._scroll.OnRefreshItem = this.OnRefreshItem;
    this._scroll.OverAddShowCnt = 2;
    this._scroll.init();    
    
    // 测试数据
    this._datas = [];
    for (var i = 0; i < 30; i++) {
        this._datas.push("" + i);
    }
    // 刷新列表
    this._scroll.updateList(this._datas.length);
}
OnRefreshItem(node: Node, idx: number) {
    // 刷新文字显示
    node.getComponent(Label).string = "哈哈哈" + idx;
}
onKeyDown(evt: EventKeyboard) {
    // 数据扩容
    if (evt.keyCode == KeyCode.KEY_A) {
        var curCnt = this._datas.length;
        for (var i = curCnt; i < curCnt + 30; i++) {
            this._datas.push("" + i);
        }
        this._scroll.updateList(this._datas.length);
    }
}

# 总结

代码和原理还是十分简单的,横向逻辑也差不多,有需求就自己修改吧💛