mirror of
https://gitee.com/ccc_28/level-render
synced 2025-11-15 10:38:09 +00:00
更新README,添加2025-10-27更新日志;优化层级渲染算法,支持多级分层渲染,修复虚拟列表相关bug;修改LevelRender组件以启用层级渲染;
This commit is contained in:
@@ -14,3 +14,9 @@
|
|||||||
> * mask组件并不能在层级渲染节点下使用,在开启层级渲染的节点下使用mask则无法达到相应效果
|
> * mask组件并不能在层级渲染节点下使用,在开启层级渲染的节点下使用mask则无法达到相应效果
|
||||||
> * 层级渲染合批需满足合批条件
|
> * 层级渲染合批需满足合批条件
|
||||||
> * 引擎3.7开始合批原生化了,如果原生平台需要层级渲染,则需要改c++代码。
|
> * 引擎3.7开始合批原生化了,如果原生平台需要层级渲染,则需要改c++代码。
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
### 2025-10-27
|
||||||
|
* 优化算法,支持多级分层渲染,如A子节点B,B的子节点C,,A->B->C,当B和C都是用LevelRender组件后,会批量合并C,然后批量合并B。
|
||||||
|
* 修复虚拟列表相关bug。
|
||||||
@@ -25,122 +25,149 @@ export default class CCCExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static _extendRender3_x() {
|
private static _extendRender3_x() {
|
||||||
const batch2d = cclegacy[`internal`][`Batcher2D`];
|
const Batcher2D = this.getBatcher2D();
|
||||||
let __renderQueue: Node[][] = [];
|
const batchQueue: Node[][] = [];
|
||||||
|
|
||||||
const levelSplit = (node: Node, lv: number, itemIndex) => {
|
const processNode = (batcher2D, node: Node) => {
|
||||||
if (!__renderQueue[lv]) {
|
node["__levelRenderFlag"] = true;
|
||||||
__renderQueue[lv] = [];
|
batcher2D.walk(node, 0);
|
||||||
}
|
node["__levelRenderFlag"] = false;
|
||||||
__renderQueue[lv].push(node);
|
|
||||||
lv++;
|
|
||||||
node["__renderLv"] = lv;
|
|
||||||
node["__levelRender"] = true;
|
|
||||||
node["__itemIndex"] = itemIndex;
|
|
||||||
const cs = node.children;
|
|
||||||
for (let i = 0; i < cs.length; ++i) {
|
|
||||||
const c = cs[i];
|
|
||||||
if (!__renderQueue[lv]) {
|
|
||||||
__renderQueue[lv] = [];
|
|
||||||
}
|
|
||||||
lv = levelSplit(c, lv, itemIndex);
|
|
||||||
}
|
|
||||||
return lv;
|
|
||||||
}
|
}
|
||||||
Object.defineProperty(batch2d.prototype, "walk", {
|
|
||||||
value: function (node: Node, level = 0) {
|
|
||||||
if (!node[`activeInHierarchy`]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const children = node.children;
|
|
||||||
const uiProps = node._uiProps;
|
|
||||||
const render = uiProps.uiComp as UIRenderer;
|
|
||||||
// Save opacity
|
|
||||||
let parentOpacity = this._pOpacity === undefined ? 1 : this._pOpacity;
|
|
||||||
if (node.parent) {
|
|
||||||
parentOpacity = node.parent._uiProps.opacity;
|
|
||||||
}
|
|
||||||
let opacity = parentOpacity;
|
|
||||||
// TODO Always cascade ui property's local opacity before remove it
|
|
||||||
const selfOpacity = render && render.color ? render.color.a / 255 : 1;
|
|
||||||
this._pOpacity = opacity = opacity * selfOpacity * uiProps.localOpacity;
|
|
||||||
|
|
||||||
// TODO Set opacity to ui property's opacity before remove it
|
//入队
|
||||||
|
const enqueue = (node: Node, layer = 0) => {
|
||||||
if (uiProps[`setOpacity`]) {
|
if (!batchQueue[layer]) {
|
||||||
uiProps[`setOpacity`](opacity);
|
batchQueue[layer] = [];
|
||||||
}
|
|
||||||
uiProps[`_opacity`] = opacity;
|
|
||||||
if (!approx(opacity, 0, EPSILON)) {
|
|
||||||
if (uiProps.colorDirty) {
|
|
||||||
// Cascade color dirty state
|
|
||||||
this._opacityDirty++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render assembler update logic
|
|
||||||
if (render && render.enabledInHierarchy) {
|
|
||||||
render.fillBuffers(this);// for rendering
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cascaded opacity to vertex buffer
|
|
||||||
if (this._opacityDirty && render && !render.useVertexOpacity && render.renderData && render.renderData.vertexCount > 0) {
|
|
||||||
// HARD COUPLING
|
|
||||||
updateOpacity(render.renderData, opacity);
|
|
||||||
const buffer = render.renderData.getMeshBuffer();
|
|
||||||
if (buffer) {
|
|
||||||
buffer.setDirty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (children.length > 0 && !node._static) {
|
|
||||||
if (!node[`__levelRender`]) {
|
|
||||||
__renderQueue = [];
|
|
||||||
for (let i = 0; i < children.length; ++i) {
|
|
||||||
const child = children[i];
|
|
||||||
if (node.parent)
|
|
||||||
child._uiProps.colorDirty = child._uiProps.colorDirty || node.parent._uiProps.colorDirty;
|
|
||||||
const enableLevelRender = node[`__enableLevelRender`];
|
|
||||||
if (!enableLevelRender) {
|
|
||||||
this.walk(child, level);
|
|
||||||
} else {
|
|
||||||
levelSplit(child, 0, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let i = 0; i < __renderQueue.length; ++i) {
|
|
||||||
const list = __renderQueue[i];
|
|
||||||
for (let j = 0; j < list.length; ++j) {
|
|
||||||
const n = list[j];
|
|
||||||
this.walk(n, level);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
__renderQueue = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiProps.colorDirty) {
|
|
||||||
// Reduce cascaded color dirty state
|
|
||||||
this._opacityDirty--;
|
|
||||||
// Reset color dirty
|
|
||||||
uiProps.colorDirty = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Restore opacity
|
|
||||||
this._pOpacity = parentOpacity;
|
|
||||||
|
|
||||||
// Post render assembler update logic
|
|
||||||
// ATTENTION: Will also reset colorDirty inside postUpdateAssembler
|
|
||||||
if (render && render.enabledInHierarchy) {
|
|
||||||
render.postUpdateAssembler(this);
|
|
||||||
if ((render.stencilStage as any === Stage.ENTER_LEVEL || render.stencilStage as any === Stage.ENTER_LEVEL_INVERTED)
|
|
||||||
&& (StencilManager.sharedManager!.getMaskStackSize() > 0)) {
|
|
||||||
this.autoMergeBatches(this._currComponent!);
|
|
||||||
this.resetRenderStates();
|
|
||||||
StencilManager.sharedManager!.exitMask();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
level += 1;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
if (node["__levelRender"]) {
|
||||||
|
node["__levelLayer"] = layer;
|
||||||
|
} else {
|
||||||
|
if (node.parent && node.parent["__levelLayer"]) {
|
||||||
|
layer = node.parent["__levelLayer"] + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.activeInHierarchy) {
|
||||||
|
const queue = batchQueue[layer];
|
||||||
|
queue.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
layer++;
|
||||||
|
for (let i = 0; i < node.children.length; ++i) {
|
||||||
|
const child = node.children[i];
|
||||||
|
layer = enqueue(child, layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
//出队
|
||||||
|
const dequeue = (batcher2D) => batchQueue.forEach(queue => queue.forEach(n => processNode(batcher2D, n)));
|
||||||
|
|
||||||
|
// 层级渲染
|
||||||
|
const levelRender = (batcher2D, node: Node, layer = 0) => {
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
processNode(batcher2D, node);
|
||||||
|
node.children.forEach(children => {
|
||||||
|
enqueue(children, layer)
|
||||||
|
})
|
||||||
|
|
||||||
|
dequeue(batcher2D);
|
||||||
|
batchQueue.forEach(q => q.length = 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Batcher2D.prototype.walk = function (node: Node, level = 0) {
|
||||||
|
if (!node.activeInHierarchy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = node.children;
|
||||||
|
const uiProps = node._uiProps;
|
||||||
|
const render = uiProps.uiComp as UIRenderer;
|
||||||
|
|
||||||
|
// Save opacity
|
||||||
|
let parentOpacity = this._pOpacity;
|
||||||
|
if (node.parent) {
|
||||||
|
parentOpacity = node.parent._uiProps.opacity;
|
||||||
|
}
|
||||||
|
let opacity = parentOpacity;
|
||||||
|
// TODO Always cascade ui property's local opacity before remove it
|
||||||
|
const selfOpacity = render && render.color ? render.color.a / 255 : 1;
|
||||||
|
this._pOpacity = opacity *= selfOpacity * uiProps.localOpacity;
|
||||||
|
// TODO Set opacity to ui property's opacity before remove it
|
||||||
|
if (uiProps[`setOpacity`]) {
|
||||||
|
uiProps[`setOpacity`](opacity);
|
||||||
|
}
|
||||||
|
uiProps[`_opacity`] = opacity;
|
||||||
|
if (!approx(opacity, 0, EPSILON)) {
|
||||||
|
if (uiProps.colorDirty) {
|
||||||
|
// Cascade color dirty state
|
||||||
|
this._opacityDirty++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render assembler update logic
|
||||||
|
if (render && render.enabledInHierarchy) {
|
||||||
|
render.fillBuffers(this);// for rendering
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cascaded opacity to vertex buffer
|
||||||
|
if (this._opacityDirty && render && !render.useVertexOpacity && render.renderData && render.renderData.vertexCount > 0) {
|
||||||
|
// HARD COUPLING
|
||||||
|
updateOpacity(render.renderData, opacity);
|
||||||
|
const buffer = render.renderData.getMeshBuffer();
|
||||||
|
if (buffer) {
|
||||||
|
buffer.setDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLevelRender = node["__levelRender"] || node["__levelRenderFlag"];
|
||||||
|
if (!isLevelRender) {
|
||||||
|
if (children.length > 0 && !node._static) {
|
||||||
|
for (let i = 0; i < children.length; ++i) {
|
||||||
|
const child = children[i];
|
||||||
|
this.walk(child, level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!node["__levelRenderFlag"]) {
|
||||||
|
levelRender(this, node, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiProps.colorDirty) {
|
||||||
|
// Reduce cascaded color dirty state
|
||||||
|
this._opacityDirty--;
|
||||||
|
// Reset color dirty
|
||||||
|
uiProps.colorDirty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Restore opacity
|
||||||
|
this._pOpacity = parentOpacity;
|
||||||
|
|
||||||
|
// Post render assembler update logic
|
||||||
|
// ATTENTION: Will also reset colorDirty inside postUpdateAssembler
|
||||||
|
if (render && render.enabledInHierarchy) {
|
||||||
|
render.postUpdateAssembler(this);
|
||||||
|
if ((render.stencilStage as any === Stage.ENTER_LEVEL || render.stencilStage as any === Stage.ENTER_LEVEL_INVERTED)
|
||||||
|
&& (StencilManager.sharedManager!.getMaskStackSize() > 0)) {
|
||||||
|
this.autoMergeBatches(this._currComponent!);
|
||||||
|
this.resetRenderStates();
|
||||||
|
StencilManager.sharedManager!.exitMask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
level += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getBatcher2D() {
|
||||||
|
return cclegacy["internal"]["Batcher2D"];
|
||||||
}
|
}
|
||||||
|
|
||||||
private static _extendEditBoxTemp() {
|
private static _extendEditBoxTemp() {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const { ccclass, property, menu } = _decorator;
|
|||||||
@menu("性能优化/LevelRender")
|
@menu("性能优化/LevelRender")
|
||||||
export class LevelRender extends Component {
|
export class LevelRender extends Component {
|
||||||
onLoad() {
|
onLoad() {
|
||||||
this.node[`__enableLevelRender`] = true;
|
this.node[`__levelRender`] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { director } from "cc";
|
||||||
|
import { CCFloat } from "cc";
|
||||||
import { Component, Node, Prefab, ScrollView, UITransform, Vec2, _decorator, error, instantiate, isValid, log, v2, v3 } from "cc";
|
import { Component, Node, Prefab, ScrollView, UITransform, Vec2, _decorator, error, instantiate, isValid, log, v2, v3 } from "cc";
|
||||||
|
import { EDITOR } from "cc/env";
|
||||||
const { ccclass, property, menu } = _decorator;
|
const { ccclass, property, menu } = _decorator;
|
||||||
|
|
||||||
let createFlag = 0;
|
// let createFlag = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 循环+分帧滑动面板
|
* 循环+分帧滑动面板
|
||||||
@@ -17,6 +20,14 @@ export default class RecycleScroll extends Component {
|
|||||||
@property(Vec2)
|
@property(Vec2)
|
||||||
spacing: Vec2 = v2();
|
spacing: Vec2 = v2();
|
||||||
|
|
||||||
|
/** 上下间隔 */
|
||||||
|
@property(CCFloat)
|
||||||
|
paddingTop: number = 0;
|
||||||
|
|
||||||
|
/** 子节点缩放 */
|
||||||
|
@property(CCFloat)
|
||||||
|
itemScale: number = 1;
|
||||||
|
|
||||||
/** item数量 */
|
/** item数量 */
|
||||||
private _numItems: number = 0;
|
private _numItems: number = 0;
|
||||||
public get numItems() {
|
public get numItems() {
|
||||||
@@ -30,6 +41,8 @@ export default class RecycleScroll extends Component {
|
|||||||
this.updateAllItems();
|
this.updateAllItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frameLoadParama = { fun: null, count: 0, index: 0 };
|
||||||
|
|
||||||
/** 视图内显示列数 */
|
/** 视图内显示列数 */
|
||||||
private _viewCol: number = 0;
|
private _viewCol: number = 0;
|
||||||
/** 视图内显示行数 */
|
/** 视图内显示行数 */
|
||||||
@@ -48,7 +61,46 @@ export default class RecycleScroll extends Component {
|
|||||||
private _isInit: boolean = false;
|
private _isInit: boolean = false;
|
||||||
/** item的index */
|
/** item的index */
|
||||||
private _itemsUUIDToIndex: { [uuid: string]: number } = {};
|
private _itemsUUIDToIndex: { [uuid: string]: number } = {};
|
||||||
|
/**index对应的item */
|
||||||
|
private _indexToItem: { [index: number]: Node } = {};
|
||||||
|
|
||||||
|
/**是否能选中 */
|
||||||
|
private _isSelectable: boolean = false;
|
||||||
|
public get isSelectable() {
|
||||||
|
return this._isSelectable;
|
||||||
|
}
|
||||||
|
public set isSelectable(value: boolean) {
|
||||||
|
this._isSelectable = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**选中索引 */
|
||||||
|
private _selectedIndex: number = -1;
|
||||||
|
public get selectedIndex() {
|
||||||
|
return this._selectedIndex;
|
||||||
|
}
|
||||||
|
public set selectedIndex(value: number) {
|
||||||
|
this._selectedIndex = value;
|
||||||
|
this.updateAllItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**能否多选 */
|
||||||
|
private _isMultiSelect: boolean = false;
|
||||||
|
public get isMultiSelect() {
|
||||||
|
return this._isMultiSelect;
|
||||||
|
}
|
||||||
|
public set isMultiSelect(value: boolean) {
|
||||||
|
this._isMultiSelect = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**选中索引列表 */
|
||||||
|
private _selectedIndexes: number[] = [];
|
||||||
|
public get selectedIndexes() {
|
||||||
|
return this._selectedIndexes;
|
||||||
|
}
|
||||||
|
public set selectedIndexes(value: number[]) {
|
||||||
|
this._selectedIndexes = value;
|
||||||
|
this.updateAllItems();
|
||||||
|
}
|
||||||
// private _itemsIndexToNode: { [index: string]: Node } = {};
|
// private _itemsIndexToNode: { [index: string]: Node } = {};
|
||||||
|
|
||||||
private _fleshInterval: number = 0.2;
|
private _fleshInterval: number = 0.2;
|
||||||
@@ -59,6 +111,10 @@ export default class RecycleScroll extends Component {
|
|||||||
private _itemStartPos: Vec2 = v2();
|
private _itemStartPos: Vec2 = v2();
|
||||||
private _isResizeFinish: boolean = false;
|
private _isResizeFinish: boolean = false;
|
||||||
private _lineIndex: number = -1;
|
private _lineIndex: number = -1;
|
||||||
|
private _isLongPress: boolean = false;
|
||||||
|
|
||||||
|
public longPressTime: number = 0.5;
|
||||||
|
public longPressTimer: number = 0;
|
||||||
|
|
||||||
/** item列表 */
|
/** item列表 */
|
||||||
public itemList: Node[] = [];
|
public itemList: Node[] = [];
|
||||||
@@ -68,7 +124,35 @@ export default class RecycleScroll extends Component {
|
|||||||
/** item刷新回调 */
|
/** item刷新回调 */
|
||||||
public onItemRender(index: number, node: Node) { }
|
public onItemRender(index: number, node: Node) { }
|
||||||
/** item点击回调 */
|
/** item点击回调 */
|
||||||
|
public onItemLongPress: (index: number, node: Node) => void = null;;
|
||||||
|
/** item点击回调 */
|
||||||
public onItemClicked(index: number, node: Node) { }
|
public onItemClicked(index: number, node: Node) { }
|
||||||
|
/**选中回调 */
|
||||||
|
public onItemSelected: (index: number, node: Node) => void = null;
|
||||||
|
/** 清除某选中 */
|
||||||
|
public clearSelection(index: number) {
|
||||||
|
if (this._isMultiSelect && this._selectedIndexes.indexOf(index) != -1) {
|
||||||
|
this._selectedIndexes.splice(this._selectedIndexes.indexOf(index), 1);
|
||||||
|
|
||||||
|
if (this.onItemSelected) {
|
||||||
|
this.onItemSelected(index, this._indexToItem[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._isSelectable && this._selectedIndex == index) {
|
||||||
|
this._selectedIndex = -1;
|
||||||
|
|
||||||
|
if (this.onItemSelected) {
|
||||||
|
this.onItemSelected(index, this._indexToItem[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** 清除所有选中 */
|
||||||
|
public clearSelectionAll() {
|
||||||
|
this._selectedIndexes = [];
|
||||||
|
this._selectedIndex = -1;
|
||||||
|
this.updateAllItems();
|
||||||
|
}
|
||||||
|
|
||||||
/** 刷新所有item */
|
/** 刷新所有item */
|
||||||
public updateAllItems() {
|
public updateAllItems() {
|
||||||
@@ -76,14 +160,35 @@ export default class RecycleScroll extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public scrollToIndexVertical(index: number, duration: number = 0.2) {
|
public scrollToIndexVertical(index: number, duration: number = 0.2) {
|
||||||
|
const scrollComp = this.node.getComponent(ScrollView);
|
||||||
|
scrollComp.stopAutoScroll();
|
||||||
const contentUTF = this._getContentUTF();
|
const contentUTF = this._getContentUTF();
|
||||||
const p = (this._itemH * index) / (contentUTF.height - this._viewH);
|
const p = (this._itemH * Math.floor(index / this._viewCol)) / (contentUTF.height - this._viewH);
|
||||||
this.node.getComponent(ScrollView).scrollToPercentVertical(1 - p, duration);
|
scrollComp.scrollToPercentVertical(1 - p, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 使指定索引物品处于可视范围 */
|
||||||
|
public scrollItemVisible(index: number, duration: number = 0.2) {
|
||||||
|
if (index < 0 || index >= this.numItems) return;
|
||||||
|
const scrollView = this.node.getComponent(ScrollView);
|
||||||
|
scrollView.stopAutoScroll();
|
||||||
|
const itemRow = Math.floor(index / this._viewCol);
|
||||||
|
const itemPosY = -this._itemH * (itemRow + 0.5);
|
||||||
|
const itemWposy = itemPosY + this.content.node.worldPosition.y;
|
||||||
|
const viewWposy = this.content.node.parent.getWorldPosition().y;
|
||||||
|
const posH = (this._viewH - this._itemH) >> 1;
|
||||||
|
const deltaY = viewWposy - itemWposy;
|
||||||
|
if (Math.abs(deltaY) < posH) return;
|
||||||
|
const sign = deltaY > 0 ? -1 : 1;
|
||||||
|
|
||||||
|
const offset = scrollView.getScrollOffset().clone();
|
||||||
|
offset.y += deltaY + (sign * posH);
|
||||||
|
scrollView.scrollToOffset(offset, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getItemDirPos(itemIndex: number) {
|
public getItemDirPos(itemIndex: number) {
|
||||||
const x = (itemIndex % this._viewCol) * this._itemW;
|
const x = (itemIndex % this._viewCol) * this._itemW;
|
||||||
const y = -Math.floor(itemIndex / this._viewCol) * this._itemH + (this.spacing.y >> 1);
|
const y = -Math.floor(itemIndex / this._viewCol) * this._itemH + (this.spacing.y >> 1) - this.paddingTop;
|
||||||
const contentUTF = this._getContentUTF();
|
const contentUTF = this._getContentUTF();
|
||||||
const wpos = contentUTF.convertToWorldSpaceAR(v3(x, y));
|
const wpos = contentUTF.convertToWorldSpaceAR(v3(x, y));
|
||||||
const parentUTF = this._getContentUTF().node.parent.getComponent(UITransform);
|
const parentUTF = this._getContentUTF().node.parent.getComponent(UITransform);
|
||||||
@@ -97,19 +202,30 @@ export default class RecycleScroll extends Component {
|
|||||||
|
|
||||||
protected onLoad(): void {
|
protected onLoad(): void {
|
||||||
this.node.on(Node.EventType.SIZE_CHANGED, this.onSizeChange, this);
|
this.node.on(Node.EventType.SIZE_CHANGED, this.onSizeChange, this);
|
||||||
|
this._initContentPos();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onDestroy(): void {
|
||||||
|
this.node.targetOff(this);
|
||||||
|
// screen.off(`window-resize`, this.onWindowResize, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 初始化content位置 */
|
||||||
|
private _initContentPos() {
|
||||||
|
const scroll = this.node.getComponent(ScrollView);
|
||||||
|
const content = scroll.content.getComponent(UITransform);
|
||||||
|
const view = content.node.parent.getComponent(UITransform);
|
||||||
|
content.node.position = v3(0, (view.height / 2) - (content.height * (1 - content.anchorY)));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onSizeChange() {
|
protected onSizeChange() {
|
||||||
this._isResize = true;
|
this._isResize = true;
|
||||||
this._initCounter = 0;
|
this._initCounter = 0;
|
||||||
this._itemsUUIDToIndex = {};
|
this._itemsUUIDToIndex = {};
|
||||||
|
this._indexToItem = {};
|
||||||
// this.itemList = [];
|
// this.itemList = [];
|
||||||
this._getContentUTF().node.removeAllChildren();
|
if (!EDITOR)
|
||||||
}
|
this._getContentUTF().node.removeAllChildren();
|
||||||
|
|
||||||
protected onDestroy(): void {
|
|
||||||
this.node.targetOff(this);
|
|
||||||
// screen.off(`window-resize`, this.onWindowResize, this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _hideAllItems() {
|
private _hideAllItems() {
|
||||||
@@ -122,28 +238,32 @@ export default class RecycleScroll extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 初始化 */
|
/** 初始化 */
|
||||||
private _initialize() {
|
private _initialize(force: boolean = false) {
|
||||||
if (this._isInit) return;
|
if (this._isInit && !force) return;
|
||||||
const scroll = this.node.getComponent(ScrollView);
|
const scroll = this.node.getComponent(ScrollView);
|
||||||
scroll.enabled = false;
|
scroll.enabled = false;
|
||||||
this._isInit = true;
|
this._isInit = true;
|
||||||
const content = this._getContentUTF();
|
const content = this._getContentUTF();
|
||||||
this.content = content;
|
this.content = content;
|
||||||
content.node.removeAllChildren()
|
if (!EDITOR)
|
||||||
|
content.node.removeAllChildren()
|
||||||
this.itemList = [];
|
this.itemList = [];
|
||||||
const viewUTF = content.node.parent.getComponent(UITransform);
|
const viewUTF = content.node.parent.getComponent(UITransform);
|
||||||
this._viewW = viewUTF.width;
|
this._viewW = viewUTF.width;
|
||||||
this._viewH = viewUTF.height;
|
this._viewH = viewUTF.height;
|
||||||
|
|
||||||
const itemData = this.itemPrefab.data.getComponent(UITransform);
|
const itemData = this.itemPrefab.data.getComponent(UITransform);
|
||||||
this._itemW = itemData.width + this.spacing.x;
|
this._itemW = itemData.width * this.itemScale + this.spacing.x;
|
||||||
this._itemH = itemData.height + this.spacing.y;
|
this._itemH = itemData.height * this.itemScale + this.spacing.y;
|
||||||
|
|
||||||
this._lastPosY = content.node.position.y;
|
this._lastPosY = content.node.position.y;
|
||||||
this._viewRow = Math.ceil(this._viewH / this._itemH) + 1;
|
this._viewRow = Math.ceil((this._viewH - this.spacing.y) / this._itemH) + 1;
|
||||||
this._viewCol = Math.floor(this._viewW / this._itemW);
|
this._viewCol = Math.floor((this._viewW + this.spacing.x) / this._itemW);
|
||||||
const surplusW = this._viewW - (this._viewCol * this._itemW);
|
const surplusW = this._viewW - (this._viewCol * this._itemW);
|
||||||
const startPos = v2((-this._viewW >> 1) + (this._itemW >> 1) + (surplusW >> 1), -this._itemH >> 1);
|
const startPos = v2(
|
||||||
|
(-this._viewW >> 1) + (this._itemW >> 1) + (surplusW >> 1),
|
||||||
|
(-this._itemH >> 1) - this.paddingTop
|
||||||
|
);
|
||||||
this._itemStartPos = startPos;
|
this._itemStartPos = startPos;
|
||||||
|
|
||||||
const cNum = this._viewRow * this._viewCol;
|
const cNum = this._viewRow * this._viewCol;
|
||||||
@@ -158,8 +278,50 @@ export default class RecycleScroll extends Component {
|
|||||||
const y = -Math.floor(index / this._viewCol) * this._itemH + (this.spacing.y >> 1);
|
const y = -Math.floor(index / this._viewCol) * this._itemH + (this.spacing.y >> 1);
|
||||||
const pos = v3(x + startPos.x, y + startPos.y);
|
const pos = v3(x + startPos.x, y + startPos.y);
|
||||||
item.setPosition(pos);
|
item.setPosition(pos);
|
||||||
|
item.setScale(v3(this.itemScale, this.itemScale, 1));
|
||||||
|
item.on(Node.EventType.TOUCH_START, () => {
|
||||||
|
this._isLongPress = false;
|
||||||
|
if (this.onItemLongPress) {
|
||||||
|
this.longPressTimer = Number(setTimeout(() => {
|
||||||
|
this._isLongPress = true;
|
||||||
|
this.longPressTimer = 0;
|
||||||
|
this.onItemLongPress(this._itemsUUIDToIndex[item.uuid], item);
|
||||||
|
}, this.longPressTime * 1000))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
item.on(Node.EventType.TOUCH_MOVE, () => {
|
||||||
|
if (this.longPressTimer) {
|
||||||
|
clearTimeout(this.longPressTimer);
|
||||||
|
this.longPressTimer = 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
item.on(Node.EventType.TOUCH_END, () => {
|
item.on(Node.EventType.TOUCH_END, () => {
|
||||||
|
if (this._isLongPress) return;
|
||||||
|
if (this.longPressTimer) {
|
||||||
|
clearTimeout(this.longPressTimer);
|
||||||
|
this.longPressTimer = 0;
|
||||||
|
}
|
||||||
this.onItemClicked(this._itemsUUIDToIndex[item.uuid], item);
|
this.onItemClicked(this._itemsUUIDToIndex[item.uuid], item);
|
||||||
|
|
||||||
|
if (this._isSelectable && this.onItemSelected) {
|
||||||
|
const lastSelectedIndex = this._selectedIndex;
|
||||||
|
this._selectedIndex = this._itemsUUIDToIndex[item.uuid];
|
||||||
|
this.onItemSelected(this._itemsUUIDToIndex[item.uuid], item);
|
||||||
|
const lastItem = this._indexToItem[lastSelectedIndex];
|
||||||
|
if (lastItem && !this._isMultiSelect) {
|
||||||
|
this.onItemSelected(lastSelectedIndex, lastItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._isMultiSelect && this.onItemSelected) {
|
||||||
|
const selectedIndex = this._itemsUUIDToIndex[item.uuid];
|
||||||
|
if (this._selectedIndexes.indexOf(selectedIndex) == -1) {
|
||||||
|
this._selectedIndexes.push(selectedIndex);
|
||||||
|
} else {
|
||||||
|
this._selectedIndexes.splice(this._selectedIndexes.indexOf(selectedIndex), 1);
|
||||||
|
}
|
||||||
|
this.onItemSelected(selectedIndex, item);
|
||||||
|
}
|
||||||
}, this);
|
}, this);
|
||||||
|
|
||||||
this.itemList[index] = item;
|
this.itemList[index] = item;
|
||||||
@@ -168,23 +330,28 @@ export default class RecycleScroll extends Component {
|
|||||||
this._updateItem(index, item);
|
this._updateItem(index, item);
|
||||||
|
|
||||||
this._itemsUUIDToIndex[item.uuid] = index;
|
this._itemsUUIDToIndex[item.uuid] = index;
|
||||||
|
this._indexToItem[index] = item;
|
||||||
|
|
||||||
createNum++;
|
createNum++;
|
||||||
if (createNum == cNum) {
|
if (createNum == cNum) {
|
||||||
scroll.enabled = true;
|
scroll.enabled = true;
|
||||||
|
|
||||||
|
director.emit('recycle_init_success');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
createFlag++;
|
// createFlag++;
|
||||||
/** 分帧创建item */
|
/** 分帧创建item */
|
||||||
frameLoad(cNum, createFunc, 16, 0, createFlag);
|
// frameLoad(cNum, createFunc, 16, 0, 0);
|
||||||
|
this.frameLoadParama = { fun: createFunc, count: cNum, index: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 更新centent高度 */
|
/** 更新centent高度 */
|
||||||
private _updateContentHeight() {
|
private _updateContentHeight() {
|
||||||
const content = this._getContentUTF();
|
const content = this._getContentUTF();
|
||||||
const col = Math.floor(this._viewW / this._itemW);
|
const col = Math.floor((this._viewW + this.spacing.x) / this._itemW);
|
||||||
const row = Math.ceil(this.numItems / col);
|
const row = Math.ceil(this.numItems / col);
|
||||||
content.height = row * (this.itemPrefab.data.getComponent(UITransform).height + this.spacing.y) - (this.spacing.y);
|
const itemH = this.itemPrefab.data.getComponent(UITransform).height * this.itemScale + this.spacing.y;
|
||||||
|
content.height = (row * itemH) - (this.spacing.y) + (this.paddingTop * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取item在view坐标系的对标 */
|
/** 获取item在view坐标系的对标 */
|
||||||
@@ -204,11 +371,61 @@ export default class RecycleScroll extends Component {
|
|||||||
if (item["needRender"] || force) {
|
if (item["needRender"] || force) {
|
||||||
this.onItemRender(index, item);
|
this.onItemRender(index, item);
|
||||||
item["needRender"] = false;
|
item["needRender"] = false;
|
||||||
|
|
||||||
|
if (this._isSelectable && this.onItemSelected) {
|
||||||
|
this.onItemSelected(index, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._isMultiSelect && this.onItemSelected) {
|
||||||
|
this.onItemSelected(index, item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据索引刷新单个Item
|
||||||
|
* @param index 要刷新的Item索引
|
||||||
|
*/
|
||||||
|
public refreshItem(index: number) {
|
||||||
|
// 检查索引是否有效
|
||||||
|
if (index < 0 || index >= this.numItems) {
|
||||||
|
console.warn(`刷新Item失败: 无效的索引 ${index}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找对应的Item节点
|
||||||
|
const item = this.itemList.find(item => this._itemsUUIDToIndex[item.uuid] === index);
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
// 强制更新Item
|
||||||
|
this._updateItem(index, item, true);
|
||||||
|
} else {
|
||||||
|
console.warn(`刷新Item失败: 未找到索引 ${index} 对应的Item节点`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新所有可见的Items
|
||||||
|
*/
|
||||||
|
public refreshVisibleItems() {
|
||||||
|
this.itemList.forEach(item => {
|
||||||
|
const index = this._itemsUUIDToIndex[item.uuid];
|
||||||
|
if (item.active) {
|
||||||
|
this._updateItem(index, item, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public update(dt: number) {
|
public update(dt: number) {
|
||||||
|
if (this.frameLoadParama?.count) {
|
||||||
|
const fun = this.frameLoadParama.fun;
|
||||||
|
fun && fun(this.frameLoadParama.index);
|
||||||
|
this.frameLoadParama.index++;
|
||||||
|
if (this.frameLoadParama.index >= this.frameLoadParama.count) {
|
||||||
|
this.frameLoadParama = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (this._isResize) {
|
if (this._isResize) {
|
||||||
this._initCounter += dt;
|
this._initCounter += dt;
|
||||||
if (this._initCounter >= this._initTimer) {
|
if (this._initCounter >= this._initTimer) {
|
||||||
@@ -228,14 +445,15 @@ export default class RecycleScroll extends Component {
|
|||||||
const isDown = dtY < 0;
|
const isDown = dtY < 0;
|
||||||
const viewHalfH = this._viewH >> 1;
|
const viewHalfH = this._viewH >> 1;
|
||||||
const itemHalfH = this._itemH >> 1;
|
const itemHalfH = this._itemH >> 1;
|
||||||
const lineIndex = Math.floor((currY - viewHalfH) / this._itemH);
|
const lineIndex = Math.floor((currY - viewHalfH + (this.spacing.y >> 1) - this.paddingTop) / (this._itemH));
|
||||||
let isLineChange = this._lineIndex != lineIndex;
|
let isLineChange = this._lineIndex != lineIndex;
|
||||||
|
|
||||||
if (!isLineChange && !this._isResizeFinish) return;
|
if (!isLineChange && !this._isResizeFinish) return;
|
||||||
this._isResizeFinish = false;
|
this._isResizeFinish = false;
|
||||||
this._lineIndex = lineIndex;
|
this._lineIndex = lineIndex;
|
||||||
const pageHeight = this._itemH * this._viewRow;
|
const pageHeight = this._itemH * this._viewRow;
|
||||||
const pageLen = this._viewRow * this._viewCol;
|
const pageLen = this._viewRow * this._viewCol;
|
||||||
const pageIndex = Math.floor((currY - viewHalfH) / pageHeight);
|
const pageIndex = Math.floor((currY - viewHalfH - this.paddingTop) / pageHeight);
|
||||||
const itemsLen = this.itemList.length;
|
const itemsLen = this.itemList.length;
|
||||||
for (let i = 0; i < itemsLen; ++i) {
|
for (let i = 0; i < itemsLen; ++i) {
|
||||||
const index = i;
|
const index = i;
|
||||||
@@ -271,34 +489,101 @@ export default class RecycleScroll extends Component {
|
|||||||
this._updateItem(currIndex, item);
|
this._updateItem(currIndex, item);
|
||||||
}
|
}
|
||||||
this._itemsUUIDToIndex[item.uuid] = currIndex;
|
this._itemsUUIDToIndex[item.uuid] = currIndex;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 分帧执行 */
|
this._indexToItem[currIndex] = item;
|
||||||
function frameLoad(loopTimes: number, func: Function, frameTime: number = 16, __index: number = 0, flag = 0) {
|
|
||||||
let loop = loopTimes;
|
|
||||||
let start = new Date().getTime();
|
|
||||||
let end = 0;
|
|
||||||
let dt = 0;
|
|
||||||
for (let i = 0; i < loop; ++i) {
|
|
||||||
if (flag != createFlag) break;
|
|
||||||
if (__index >= loop) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
func && func(__index);
|
|
||||||
} catch (e) {
|
|
||||||
error(e);
|
|
||||||
}
|
|
||||||
__index++;
|
|
||||||
end = new Date().getTime();
|
|
||||||
dt = end - start;
|
|
||||||
if (dt > frameTime) {
|
|
||||||
setTimeout(() => {
|
|
||||||
frameLoad(loop, func, frameTime, __index, flag);
|
|
||||||
}, 10);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/** 获取item */
|
||||||
|
getItemByIndex(index: number): Node | undefined {
|
||||||
|
if (index < 0 || index >= this.numItems) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let uiid = ``;
|
||||||
|
for (const key in this._itemsUUIDToIndex) {
|
||||||
|
if (this._itemsUUIDToIndex[key] == index) {
|
||||||
|
uiid = key;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of this.itemList) {
|
||||||
|
if (node.uuid == uiid) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 强制显示指定索引的Item
|
||||||
|
* @param index 索引
|
||||||
|
*/
|
||||||
|
forcedDisplayItemByIndex(index: number) {
|
||||||
|
// 获取滚动视图组件
|
||||||
|
const scrollView = this.getComponent(ScrollView);
|
||||||
|
if (!scrollView) {
|
||||||
|
error("RecycleScroll: 找不到 ScrollView 组件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//停止滑动滑动视图的滑动和惯性滑动
|
||||||
|
scrollView.stopAutoScroll();
|
||||||
|
// 检查索引是否有效
|
||||||
|
if (index < 0 || index >= this._numItems) {
|
||||||
|
error("RecycleScroll: 无效的索引值");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算目标位置
|
||||||
|
const content = scrollView.content;
|
||||||
|
const viewHeight = this.node.getComponent(UITransform).height;
|
||||||
|
|
||||||
|
const contentHeight = content.getComponent(UITransform).height;
|
||||||
|
const contentAnchor = content.getComponent(UITransform).anchorPoint;
|
||||||
|
|
||||||
|
|
||||||
|
// 处理ScrollView锚点问题
|
||||||
|
const scrollViewAnchor = scrollView.node.getComponent(UITransform).anchorPoint;
|
||||||
|
let initPosY = 0 + scrollViewAnchor.y * viewHeight;
|
||||||
|
|
||||||
|
// 计算目标行数和目标Y坐标
|
||||||
|
const targetRow = Math.floor(index / this._viewCol);
|
||||||
|
let targetY = targetRow * this._itemH + initPosY;
|
||||||
|
|
||||||
|
content.setPosition(v3(content.position.x, targetY, content.position.z));
|
||||||
|
|
||||||
|
// 计算content底部的y坐标
|
||||||
|
const contentBottomY = content.worldPosition.y - contentHeight * contentAnchor.y;
|
||||||
|
//计算scrollview底部的坐标
|
||||||
|
const scrollViewBottomY = this.node.worldPosition.y - viewHeight * scrollViewAnchor.y;
|
||||||
|
|
||||||
|
//如果content底部超出scrollview底部,则将content位置调整到底部对齐
|
||||||
|
if (contentBottomY > scrollViewBottomY) {
|
||||||
|
content.setPosition(v3(content.position.x, targetY - (contentBottomY - scrollViewBottomY), content.position.z));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 强制更新一次显示
|
||||||
|
this.updateAllItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 检查索引是否正在渲染
|
||||||
|
* @param index
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
isRanderingItemByIndex(index: number) {
|
||||||
|
// 检查索引是否有效
|
||||||
|
if (index < 0 || index >= this._numItems) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in this._itemsUUIDToIndex) {
|
||||||
|
if (this._itemsUUIDToIndex[key] == index) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"label": "customSplash",
|
"label": "customSplash",
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"customSplash": {
|
"customSplash": {
|
||||||
"complete": false,
|
"complete": true,
|
||||||
"form": "https://creator-api.cocos.com/api/form/show?sid=fa6ec2019b107316262fad6f726e3d76"
|
"form": "https://creator-api.cocos.com/api/form/show?sid=fa6ec2019b107316262fad6f726e3d76"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
"label": "removeSplash",
|
"label": "removeSplash",
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"removeSplash": {
|
"removeSplash": {
|
||||||
"complete": false,
|
"complete": true,
|
||||||
"form": "https://creator-api.cocos.com/api/form/show?sid=fa6ec2019b107316262fad6f726e3d76"
|
"form": "https://creator-api.cocos.com/api/form/show?sid=fa6ec2019b107316262fad6f726e3d76"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user