示例项目:https://download.csdn.net/download/K86338236/86860248
效果:999条数据,drawcall稳定在15
使用:ScrollViewCtrl挂载到滑动列表上,将滑动的预制体拖入节点itemPrefab属性上
//传入的数据,需要是数组形式
let arr = [{ name: "这是" + 1, id: 1 },{ name: "这是" + 2, id: 2 },{ name: "这是" + 3, id: 3 }]
this.scrollView.getComponent(ScrollViewCtrl).init(arr);
预制体根节点上不能挂载组件
预制体上的脚本:
const { ccclass, property } = cc._decorator;
@ccclass
export default class ItemCtrl extends cc.Component {
@property(cc.Label)
desc: cc.Label = null;
start() {
}
/**
* 传入数据
* @param data 数据
* @param index 顺序
* @param extData 额外数据
*/
initData(data, index, extData) {
this.desc.string = data.name + " " + data.id;
}
}
代码预览:
ScrollViewCtrl:
import NodePool from "./NodePool";
const { ccclass, property, menu } = cc._decorator;
@ccclass
@menu('自定义组件/ScrollViewCtrl')
export default class ScrollViewCtrl extends cc.Component {
@property(cc.Prefab)
itemPrefab: cc.Prefab = null;
bindIndexList: {};
scrollView: cc.ScrollView = null;
content: cc.Node = null;
view: cc.Node = null;
layout: cc.Layout = null;
itemName: string = null;
mat4: cc.Mat4 = null;
isInit: boolean = null;
data: any[] = null;
callbackList: any[] = null;
extData: any = null;
firstX: number = null;
firstY: number = null;
itemCache: any[] = null;
itemBuffer: any = null;
_tmpV2: any = null;
viewRect: any = null;
start() {
this.initOnce();
}
initOnce() {
this.bindIndexList = {}; /**记录哪个index需要绑定Item */
this.scrollView = this.node.getComponent(cc.ScrollView);
this.content = this.scrollView.content
this.view = this.content.parent;
this.layout = this.content.getComponent(cc.Layout);
this.itemName = this.itemPrefab.name;
this.mat4 = cc.mat4();
this.initOnce = function () { }
}
onDestroy() {
this.content.off(cc.Node.EventType.POSITION_CHANGED, this.scrollEvent, this);
this.recycle();
}
/**注册滚动事件 */
registerScrollEvent(handler, target) {
if (!handler.name) {
return;
}
var scrollView = this.node.getComponent(cc.ScrollView);
var eventHandler = new cc.Component.EventHandler();
eventHandler.target = target.node;
eventHandler.component = cc.js.getClassName(target);
eventHandler.handler = handler.name;
var index = scrollView.scrollEvents.length;
scrollView.scrollEvents[index] = eventHandler;
}
/**
* 初始化数据和表现
* @param {Array} data 所有的列表数据组成的数组
* @param {any} param 要传递到item的额外参数
*/
init(data, param?) {
this.initOnce();
if (!Array.isArray(data)) {
console.error("传进来的数据不为数组!");
return;
}
if (!data.length) {
this.recycle();
this.content.off(cc.Node.EventType.POSITION_CHANGED, this.scrollEvent, this);
return;
}
param = param || {};
this.isInit = true;
this.data = data;
this.callbackList = [];
this.extData = param.extData;
if (param.onChanged) {
this.onItemChanged(param.onChanged);
}
this.layout.enabled = false;
this.scrollView.stopAutoScroll();
if (!NodePool.Instance.hasPool(this.itemName)) {
NodePool.Instance.initPool(this.itemPrefab);
}
/**模板 */
var template = this.itemPrefab.data;
var paddingLeft = this.layout.paddingLeft; /**左边距 */
var paddingRight = this.layout.paddingRight; /**右边距 */
var paddingTop = this.layout.paddingTop; /**上边距 */
var paddingBottom = this.layout.paddingBottom; /**下边距 */
var spacingX = this.layout.spacingX; /**水平间隔距离 */
var spacingY = this.layout.spacingY; /**垂直间隔距离 */
let firstX = template.x;
let firstY = template.y;
if (this.scrollView.horizontal) {
firstX = -template.width / 2
firstX -= paddingLeft;
}
if (this.scrollView.vertical) {
firstY = -template.height / 2;
firstY -= paddingTop;
}
this.firstX = firstX;
this.firstY = firstY;
this.itemCache = [];
this.itemBuffer = this.itemBuffer || [];
//初始化itemBuffer
let i = 0
this.itemBuffer.forEach(buffer => {
buffer.index = -1;
if (i >= data.length) {
buffer.item.x = -9999999;
buffer.item.y = -9999999;
buffer.item.opacity = 0;
}
i++;
});
var initCache = (i) => {
this.itemCache[i] = this.itemCache[i] || {};
this.itemCache[i].x = firstX;
this.itemCache[i].y = firstY;
this.itemCache[i].width = template.width;
this.itemCache[i].height = template.height;
this.itemCache[i].scaleX = template.scaleX;
this.itemCache[i].scaleY = template.scaleY;
this.itemCache[i].visible = false;
}
initCache(0);
for (let i = 1; i < this.data.length; i++) {
initCache(i);
if (this.scrollView.horizontal) {
this.itemCache[i].x = this.itemCache[i - 1].x - (this.itemCache[i - 1].width / 2 + this.itemCache[i].width / 2 + spacingX);
}
if (this.scrollView.vertical) {
this.itemCache[i].y = this.itemCache[i - 1].y - (this.itemCache[i - 1].height / 2 + this.itemCache[i].height / 2 + spacingY);
}
}
let lastItem = this.itemCache[this.itemCache.length - 1];
if (this.scrollView.horizontal) {
this.content.width = Math.abs(lastItem.x + lastItem.width / 2 + paddingRight);
}
if (this.scrollView.vertical) {
this.content.height = Math.abs(lastItem.y - lastItem.height / 2 - paddingBottom);
}
this.content.on(cc.Node.EventType.POSITION_CHANGED, this.scrollEvent, this);
this.scheduleOnce(this.updateListView);
}
updateItemView(item, index) {
var js = this.getScript(item);
if (js && js.initData) {
js.initData(this.data[index], index, this.extData);
js.updateView && js.updateView();
}
}
//创建item预制体
getItem() {
let item = NodePool.Instance.getNode(this.itemName);
item.x = this.firstX;
item.y = this.firstY;
let data = {
item: item,
index: -1,
}
//将创建的item预制体放入data数据结构,加入itemBuffer,方便在更新视图时操作
this.itemBuffer.push(data);
item.on(cc.Node.EventType.SIZE_CHANGED, this.onItemSizeChanged.bind(this, item), this);
item.on(cc.Node.EventType.SCALE_CHANGED, this.onItemSizeChanged.bind(this, item), this);
return data;
}
scrollEvent() {
if (!this.content || !this.isInit) return;
this.updateListView();
}
/**
* 在显示区域的itemx x,y坐标相对于content的位置是固定的,这部分数据存于itemCache. 监听content位置改变时,
* 遍历所有itemCache,根据itemCache中每个item相对于content的位置计算出每个item相对于世界坐标的rect区域,与当前mask相对世界坐标
* 的rect区域判断是否相交,若相交则该item显示,根据itemCache里面存的坐标给item设置位置,层级和父节点,不显示的item给
* 移到下方
*/
updateListView() {
if (!this.itemCache) {
return;
}
var attach = (buffer, i) => {
buffer.index = i;
buffer.item.x = this.itemCache[i].x;
buffer.item.y = this.itemCache[i].y;
buffer.item.scaleX = this.itemCache[i].scaleX;
buffer.item.scaleY = this.itemCache[i].scaleY;
buffer.item.opacity = 255;
if (this.scrollView.horizontal) {
buffer.item.width = this.itemCache[i].width;
}
if (this.scrollView.vertical) {
buffer.item.height = this.itemCache[i].height;
}
buffer.item.parent = this.content;
this.updateItemView(buffer.item, i);
}
for (var i = 0; i < this.itemCache.length; i++) {
var cache = this.itemCache[i];
var visible = this.isItemInView(i);
var buffer = this.itemBuffer.find((v) => {
return v.index == i;
});
//创建当前滑动区域内可见的item预制体
if (visible) {
//当前下标有绑定的item,直接使用该item
if (this.bindIndexList[i]) {
buffer = this.itemBuffer.find((v) => {
return v.bindIndex == i;
});
}
//创建item
if (!buffer) {
//如果itemBuffer里面有初始化缓存的预制体,则使用之
buffer = this.itemBuffer.find((v) => {
return v.index == -1 && v.bindIndex == undefined;
});
//没有缓存的话,则使用getIItem创建后,并放入itemBuffer方便后面使用
buffer = buffer || this.getItem();
}
//根据item绑定的序列号重新设置层级以及位置
if (buffer.index != i) {
attach(buffer, i);
}
} else if (buffer) { //将不可见item移到非常靠下的位置,使content大小足够大,能够向下滑动
buffer.index = -1;
buffer.item.x = -9999999;
buffer.item.y = -9999999;
buffer.item.opacity = 0; //active会影响layout大小,所以不可见将透明度设置为0
}
//执行每个item可见变化的回调,可见->不可见 回调, 不可见->可见回调
if (cache.visible != visible) {
this.runItemChangedCallback(i, visible);
}
cache.visible = visible;
}
/**调整层级 */
this.itemBuffer.sort((a, b) => {
if (a.index < 0 || b.index < 0) {
return 1;
}
return a.index - b.index;
});
//按照index对item进行排序
for (var i = 0; i < this.itemBuffer.length; i++) {
this.itemBuffer[i].item.setSiblingIndex(i);
}
}
/**item是否在可视区域内 */
isItemInView(index) {
//当前滑动列表区域的坐标
this._tmpV2 = this._tmpV2 || cc.v2(0, 0);
//view为mask裁剪区域
this.view.getWorldMatrix(this.mat4);
let scale = this.mat4.m[0];
let wposx = this.mat4.m[12];
let wposy = this.mat4.m[13];
let width1 = this.view.width * scale;
let height1 = this.view.height * scale;
let wpos = this.view.convertToWorldSpaceAR(cc.Vec2.ZERO, this._tmpV2);
//rect比那辆没有初始化或则进行了缩放或则横坐标或纵坐标变动了,则将rect变量重赋值
if (!this.viewRect || scale != 1 || (this.viewRect.x + width1 / 2) != wposx || (this.viewRect.y + height1 / 2) != wposy) {
this.viewRect = new cc.Rect(wpos.x - width1 / 2, wpos.y - height1 / 2, width1, height1);
}
//获取当前item缓存的数据
let data = this.itemCache[index];
//转成世界坐标
let wpos2 = this.content.convertToWorldSpaceAR(cc.v2(data.x, data.y));
//获取item的宽高
let width2 = data.width * data.scaleX;
let height2 = data.height * data.scaleY;
let rect = new cc.Rect(wpos2.x - width2 / 2, wpos2.y - height2 / 2, width2, height2);
//判断两个矩形区域是否相交
let ret = this.viewRect.intersects(rect);
return ret;
}
onItemSizeChanged(item) {
if (!this.itemCache) {
return;
}
let data = this.itemBuffer.find((v) => {
return v.item == item;
});
if (data && data.index >= 0) {
var itemData = this.itemCache[data.index];
if (this.scrollView.horizontal && itemData.width == item.width && itemData.scaleX == item.scaleX) {
return;
}
if (this.scrollView.vertical && itemData.height == item.height && itemData.scaleY == item.scaleY) {
return;
}
itemData.width = item.width;
itemData.scaleX = item.scaleX;
itemData.height = item.height;
itemData.scaleY = item.scaleY;
this.updateBuffer();
this.scheduleOnce(this.updateListView);
}
}
setItemProperty(index, property, value) {
if (!this.itemCache || !this.itemBuffer) {
return;
}
var buffer = this.itemBuffer.find(v => {
return v.index == index;
});
if (buffer) {
buffer.item[property] = value;
} else {
var itemData = this.itemCache[index];
itemData[property] = value;
}
this.updateBuffer();
this.scheduleOnce(this.updateListView);
}
/**刷新itemCache的缓存里面每个item缓存数据的y坐标,刷新itemBuffer缓存预制体的纵坐标,并更新content的高度
* 每个显示的item相对于content的位置不变,只在content容器内的item大小变化时,刷新itemCache数据
**/
updateBuffer() {
let lastItem = this.itemCache[this.itemCache.length - 1];
if (this.scrollView.vertical) {
this.itemCache[0].y = -this.itemCache[0].height / 2 - this.layout.paddingTop;
if (this.itemCache[0].scaleY != 1) {
this.itemCache[0].y = -Math.abs(this.itemCache[0].scaleY * this.itemCache[0].height) / 2 - this.layout.paddingTop;
}
this.itemBuffer.find((v) => {
if (v.index == 0) {
v.item.y = this.itemCache[0].y;
}
});
for (var i = 1; i < this.data.length; i++) {
var data1 = this.itemCache[i - 1];
var data2 = this.itemCache[i];
var h1 = data1.height;
var h2 = data2.height;
if (data1.scaleY != 1) {
h1 = Math.abs(data1.scaleY * data1.height);
}
if (data2.scaleY != 1) {
h2 = Math.abs(data2.scaleY * data2.height);
}
data2.y = data1.y - (h1 / 2 + h2 / 2 + this.layout.spacingY);
this.itemBuffer.find((v) => {
if (v.index == i) {
v.item.y = data2.y;
}
});
}
var lastRealHeight = lastItem.height / 2;
if (lastItem.scaleY != 1) {
lastRealHeight = Math.abs(lastItem.scaleY * lastItem.height) / 2;
}
this.content.height = Math.abs(lastItem.y - lastRealHeight - this.layout.paddingBottom);
}
}
/**
* 滑动到指定Item位置
* @param {*} index
*/
scrollToItem(index, t?, extParams?) {
if (!this.itemCache || !this.itemCache.length) {
return;
}
if (index < 0) index = 0;
if (index >= this.itemCache.length) index = this.itemCache.length - 1;
var cache = this.itemCache[index];
if (!cache) {
return;
}
extParams = extParams || {};
t = t || 0;
if (this.scrollView) {
var toY;
if (extParams.customTween) {
// toY = -(cache.y + Math.abs(cache.height * cache.scaleY) / 2);
// this.content.runAction(cc.moveTo(t, cc.v2(0, toY)));
} else {
toY = -(cache.y + Math.abs(cache.height * cache.scaleY) / 2);
this.scrollView.scrollToOffset(cc.v2(0, toY), t)
}
}
}
/**
* 获取Item
*/
getItemByIndex(index) {
if (!this.itemBuffer) {
return;
}
var buffer = this.itemBuffer.find(v => {
return v.index == index;
}) || {};
return buffer.item;
}
removeItemByIndex(index) {
if (!this.itemBuffer) {
return;
}
var idx = this.itemBuffer.findIndex(v => {
return v.index == index;
});
if (idx >= 0) {
var buffer = this.itemBuffer.splice(idx, 1);
buffer.item.destroy();
}
}
/**
* 下标和Item绑定
* item只能指定index, item将不可复用
*/
bindItemWithIndex(item, bindIndex, isBind) {
if (!this.itemBuffer) {
return;
}
isBind = isBind || true;
var buffer = this.itemBuffer.find(v => {
return v.item == item;
});
if (buffer) {
buffer.bindIndex = bindIndex;
if (isBind) {
this.bindIndexList[bindIndex] = isBind;
} else {
delete this.bindIndexList[bindIndex];
delete buffer.bindIndex;
}
}
}
onItemChanged(callback) {
if (typeof (callback) == "function") {
this.callbackList.push(callback);
}
}
runItemChangedCallback(index, visible) {
try {
for (let i = 0; i < this.callbackList.length; i++) {
this.callbackList[i](index, visible);
}
} catch (e) {
console.error(e);
}
}
recycle() {
if (this.itemBuffer) {
this.itemBuffer.forEach(element => {
if (element && cc.isValid(element.item)) {
element.item.off(cc.Node.EventType.SIZE_CHANGED, this.onItemSizeChanged.bind(this, element.item), this);
element.item.off(cc.Node.EventType.SCALE_CHANGED, this.onItemSizeChanged.bind(this, element.item), this);
NodePool.Instance.putNode(this.itemName, element.item);
}
});
}
this.itemCache = null;
this.itemBuffer = null;
}
getScript(node: cc.Node) {
if (!node) return null;
//@ts-ignore
let arr = node._components;
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
if (element && arr[i].hasOwnProperty("_super")) {
return arr[i];
}
}
return null;
}
}
/**
* 获取Item
*/
getItemByIndex(index) {
if (!this.itemBuffer) {
return;
}
var buffer = this.itemBuffer.find(v => {
return v.index == index;
}) || {};
return buffer.item;
}
removeItemByIndex(index) {
if (!this.itemBuffer) {
return;
}
var idx = this.itemBuffer.findIndex(v => {
return v.index == index;
});
if (idx >= 0) {
var buffer = this.itemBuffer.splice(idx, 1);
EngineUtil.destroyNode(buffer.item);
}
}
/**
* 下标和Item绑定
* item只能指定index, item将不可复用
*/
bindItemWithIndex(item, bindIndex, isBind) {
if (!this.itemBuffer) {
return;
}
isBind = isBind || true;
var buffer = this.itemBuffer.find(v => {
return v.item == item;
});
if (buffer) {
buffer.bindIndex = bindIndex;
if (isBind) {
this.bindIndexList[bindIndex] = isBind;
} else {
delete this.bindIndexList[bindIndex];
delete buffer.bindIndex;
}
}
}
onItemChanged(callback) {
if (typeof (callback) == "function") {
this.callbackList.push(callback);
}
}
runItemChangedCallback(index, visible) {
try {
for (let i = 0; i < this.callbackList.length; i++) {
this.callbackList[i](index, visible);
}
} catch (e) {
console.error(e);
}
}
recycle() {
if (this.itemBuffer) {
this.itemBuffer.forEach(element => {
if (element && cc.isValid(element.item)) {
element.item.off(cc.Node.EventType.SIZE_CHANGED, this.onItemSizeChanged.bind(this, element.item), this);
element.item.off(cc.Node.EventType.SCALE_CHANGED, this.onItemSizeChanged.bind(this, element.item), this);
NodePool.Instance.putNode(this.itemName, element.item);
}
});
}
this.itemCache = null;
this.itemBuffer = null;
}
getScript(node: cc.Node) {
if (!node) return null;
//@ts-ignore
let arr = node._components;
for (let i = 0; i < arr.length; i++) {
const element = arr[i];
if (element && arr[i].hasOwnProperty("_super")) {
return arr[i];
}
}
return null;
}
}
NodePool:
var _nodePool = {};
var _prefabList = {};
export default class NodePool {
protected static _instance: NodePool = null;
public static get Instance(): NodePool {
if (NodePool._instance == null) {
NodePool._instance = new NodePool();
}
return NodePool._instance;
}
private _pathList: {};
/**
* 添加路径,调用loadPool的时候就不需要传完整路径了
* @param {*} path 路径数组或者字符串
*/
addPath(path) {
this._pathList = this._pathList || {};
if (!Array.isArray(path)) {
path = [path];
}
for (let i = 0; i < path.length; i++) {
let name = cc.path.basename(path[i]);
this._pathList[name] = this._pathList[name] || path[i];
}
cc.log(this._pathList);
}
/**
* 加载预制,保存到节点池中
* @param {*} name 预制名字,不需要带路径
* @param {*} callback
*/
loadPool(name, callback) {
if (!Array.isArray(name)) {
name = [name];
}
var paths = [];
for (let i = 0; i < name.length; i++) {
paths.push(this._pathList[name[i]]);
}
cc.resources.load(paths, cc.Prefab, (res: any) => {
if (!Array.isArray(res)) {
res = [res];
}
for (let i = 0; i < res.length; i++) {
let poolName = res[i].name;
if (!this.hasPool(poolName)) {
this.initPool(poolName, res[i]);
}
}
if (callback) callback();
});
}
/**
* 初始化节点池
* @param {*} name 缓存名字,一般和预制体名字一致
* @param {*} object 预制对象
* @param {*} count 缓存个数,默认为1
*/
initPool(object, count = 1, name = "") {
if (!name) name = object.name;
if (!this.hasPool(name)) {
if (_nodePool[name]) {
for (const n of _nodePool[name]) {
this.destroyNode(n);
}
}
_nodePool[name] = [];
_prefabList[name] = object;
for (var i = 0; i < count; i++) {
var node = cc.instantiate(object);
node.active = false;
_nodePool[name].push(node);
}
}
}
/**
* 是否已经缓存在节点池
* @param {*} name 预制名字
*/
hasPool(name) {
return _prefabList[name] && _prefabList[name].isValid;
}
/**
* 回收节点
* @param {*} name 预制名字
* @param {*} node 预制节点实例
*/
putNode(name, node) {
if (!cc.isValid(node)) {
console.error("putNode: node param is invalid");
return;
}
var pool = _nodePool[name];
if (!pool) {
console.error("putNode: pool %s not found", name);
return;
}
if (pool.findIndex((item) => {
return item == node
}) >= 0) {
return;
}
node.stopAllActions();
node.removeFromParent(true);
node.x = 0;
node.y = 0;
node.scale = 1;
node.opacity = 255;
node.active = false;
pool.push(node);
}
/**
* 获取缓存的节点实例
* @param {*} name 预制名字
*/
getNode(name) {
var pool = _nodePool[name];
if (!pool) {
console.error("getNode: pool %s not found", name);
return null;
}
var node = pool.length > 0 ? pool.pop() : cc.instantiate(_prefabList[name]);
node = cc.isValid(node) ? node : cc.instantiate(_prefabList[name]);
node.active = true;
node.x = 0;
node.y = 0;
// ant.FontTools.updateFont(node);
return node;
}
/**重置节点池 */
reset() {
if (!_nodePool) {
return;
}
var list = _nodePool;
for (var key in list) {
var pool = list[key];
while (pool.length > 0) {
this.destroyNode(pool.pop());
}
}
}
getPool() {
return _nodePool;
}
destroyNode(node) {
if (!cc.isValid(node)) {
console.error("Tools: destroyNode error, param is invalid");
return;
}
node.removeFromParent(false);
node.destroy();
}
}