From 432cd56542f7ee6de8ad94233f2962980e144bd7 Mon Sep 17 00:00:00 2001 From: spe <853517173@qq.com> Date: Mon, 27 Oct 2025 10:28:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0README=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A02025-10-27=E6=9B=B4=E6=96=B0=E6=97=A5=E5=BF=97?= =?UTF-8?q?=EF=BC=9B=E4=BC=98=E5=8C=96=E5=B1=82=E7=BA=A7=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E7=AE=97=E6=B3=95=EF=BC=8C=E6=94=AF=E6=8C=81=E5=A4=9A=E7=BA=A7?= =?UTF-8?q?=E5=88=86=E5=B1=82=E6=B8=B2=E6=9F=93=EF=BC=8C=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=99=9A=E6=8B=9F=E5=88=97=E8=A1=A8=E7=9B=B8=E5=85=B3bug?= =?UTF-8?q?=EF=BC=9B=E4=BF=AE=E6=94=B9LevelRender=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E4=BB=A5=E5=90=AF=E7=94=A8=E5=B1=82=E7=BA=A7=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 + assets/demo/CCCExtension.ts | 251 +++++++++-------- assets/demo/LevelRender.ts | 2 +- assets/demo/RecycleScroll.ts | 389 ++++++++++++++++++++++---- settings/v2/packages/information.json | 4 +- 5 files changed, 485 insertions(+), 167 deletions(-) diff --git a/README.md b/README.md index dc1748d..b52a110 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,9 @@ > * mask组件并不能在层级渲染节点下使用,在开启层级渲染的节点下使用mask则无法达到相应效果 > * 层级渲染合批需满足合批条件 > * 引擎3.7开始合批原生化了,如果原生平台需要层级渲染,则需要改c++代码。 + +## 更新日志 + +### 2025-10-27 +* 优化算法,支持多级分层渲染,如A子节点B,B的子节点C,,A->B->C,当B和C都是用LevelRender组件后,会批量合并C,然后批量合并B。 +* 修复虚拟列表相关bug。 \ No newline at end of file diff --git a/assets/demo/CCCExtension.ts b/assets/demo/CCCExtension.ts index 07232be..2ebb7f2 100644 --- a/assets/demo/CCCExtension.ts +++ b/assets/demo/CCCExtension.ts @@ -25,122 +25,149 @@ export default class CCCExtension { } private static _extendRender3_x() { - const batch2d = cclegacy[`internal`][`Batcher2D`]; - let __renderQueue: Node[][] = []; + const Batcher2D = this.getBatcher2D(); + const batchQueue: Node[][] = []; - const levelSplit = (node: Node, lv: number, itemIndex) => { - if (!__renderQueue[lv]) { - __renderQueue[lv] = []; - } - __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; + const processNode = (batcher2D, node: Node) => { + node["__levelRenderFlag"] = true; + batcher2D.walk(node, 0); + node["__levelRenderFlag"] = false; } - 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 - - 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(); - } - } - - 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; + //入队 + const enqueue = (node: Node, layer = 0) => { + if (!batchQueue[layer]) { + batchQueue[layer] = []; } - }); + + 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() { diff --git a/assets/demo/LevelRender.ts b/assets/demo/LevelRender.ts index 7a15112..ae64f63 100644 --- a/assets/demo/LevelRender.ts +++ b/assets/demo/LevelRender.ts @@ -5,7 +5,7 @@ const { ccclass, property, menu } = _decorator; @menu("性能优化/LevelRender") export class LevelRender extends Component { onLoad() { - this.node[`__enableLevelRender`] = true; + this.node[`__levelRender`] = true; } } diff --git a/assets/demo/RecycleScroll.ts b/assets/demo/RecycleScroll.ts index d959b1f..4a20a60 100644 --- a/assets/demo/RecycleScroll.ts +++ b/assets/demo/RecycleScroll.ts @@ -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 { EDITOR } from "cc/env"; const { ccclass, property, menu } = _decorator; -let createFlag = 0; +// let createFlag = 0; /** * 循环+分帧滑动面板 @@ -17,6 +20,14 @@ export default class RecycleScroll extends Component { @property(Vec2) spacing: Vec2 = v2(); + /** 上下间隔 */ + @property(CCFloat) + paddingTop: number = 0; + + /** 子节点缩放 */ + @property(CCFloat) + itemScale: number = 1; + /** item数量 */ private _numItems: number = 0; public get numItems() { @@ -30,6 +41,8 @@ export default class RecycleScroll extends Component { this.updateAllItems(); } + frameLoadParama = { fun: null, count: 0, index: 0 }; + /** 视图内显示列数 */ private _viewCol: number = 0; /** 视图内显示行数 */ @@ -48,7 +61,46 @@ export default class RecycleScroll extends Component { private _isInit: boolean = false; /** item的index */ 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 _fleshInterval: number = 0.2; @@ -59,6 +111,10 @@ export default class RecycleScroll extends Component { private _itemStartPos: Vec2 = v2(); private _isResizeFinish: boolean = false; private _lineIndex: number = -1; + private _isLongPress: boolean = false; + + public longPressTime: number = 0.5; + public longPressTimer: number = 0; /** item列表 */ public itemList: Node[] = []; @@ -68,7 +124,35 @@ export default class RecycleScroll extends Component { /** item刷新回调 */ public onItemRender(index: number, node: Node) { } /** item点击回调 */ + public onItemLongPress: (index: number, node: Node) => void = null;; + /** item点击回调 */ 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 */ public updateAllItems() { @@ -76,14 +160,35 @@ export default class RecycleScroll extends Component { } public scrollToIndexVertical(index: number, duration: number = 0.2) { + const scrollComp = this.node.getComponent(ScrollView); + scrollComp.stopAutoScroll(); const contentUTF = this._getContentUTF(); - const p = (this._itemH * index) / (contentUTF.height - this._viewH); - this.node.getComponent(ScrollView).scrollToPercentVertical(1 - p, duration); + const p = (this._itemH * Math.floor(index / this._viewCol)) / (contentUTF.height - this._viewH); + 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) { 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 wpos = contentUTF.convertToWorldSpaceAR(v3(x, y)); const parentUTF = this._getContentUTF().node.parent.getComponent(UITransform); @@ -97,19 +202,30 @@ export default class RecycleScroll extends Component { protected onLoad(): void { 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() { this._isResize = true; this._initCounter = 0; this._itemsUUIDToIndex = {}; + this._indexToItem = {}; // this.itemList = []; - this._getContentUTF().node.removeAllChildren(); - } - - protected onDestroy(): void { - this.node.targetOff(this); - // screen.off(`window-resize`, this.onWindowResize, this); + if (!EDITOR) + this._getContentUTF().node.removeAllChildren(); } private _hideAllItems() { @@ -122,28 +238,32 @@ export default class RecycleScroll extends Component { } /** 初始化 */ - private _initialize() { - if (this._isInit) return; + private _initialize(force: boolean = false) { + if (this._isInit && !force) return; const scroll = this.node.getComponent(ScrollView); scroll.enabled = false; this._isInit = true; const content = this._getContentUTF(); this.content = content; - content.node.removeAllChildren() + if (!EDITOR) + content.node.removeAllChildren() this.itemList = []; const viewUTF = content.node.parent.getComponent(UITransform); this._viewW = viewUTF.width; this._viewH = viewUTF.height; const itemData = this.itemPrefab.data.getComponent(UITransform); - this._itemW = itemData.width + this.spacing.x; - this._itemH = itemData.height + this.spacing.y; + this._itemW = itemData.width * this.itemScale + this.spacing.x; + this._itemH = itemData.height * this.itemScale + this.spacing.y; this._lastPosY = content.node.position.y; - this._viewRow = Math.ceil(this._viewH / this._itemH) + 1; - this._viewCol = Math.floor(this._viewW / this._itemW); + this._viewRow = Math.ceil((this._viewH - this.spacing.y) / this._itemH) + 1; + this._viewCol = Math.floor((this._viewW + this.spacing.x) / 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; 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 pos = v3(x + startPos.x, y + startPos.y); 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, () => { + if (this._isLongPress) return; + if (this.longPressTimer) { + clearTimeout(this.longPressTimer); + this.longPressTimer = 0; + } 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.itemList[index] = item; @@ -168,23 +330,28 @@ export default class RecycleScroll extends Component { this._updateItem(index, item); this._itemsUUIDToIndex[item.uuid] = index; + this._indexToItem[index] = item; createNum++; if (createNum == cNum) { scroll.enabled = true; + + director.emit('recycle_init_success'); } } - createFlag++; + // createFlag++; /** 分帧创建item */ - frameLoad(cNum, createFunc, 16, 0, createFlag); + // frameLoad(cNum, createFunc, 16, 0, 0); + this.frameLoadParama = { fun: createFunc, count: cNum, index: 0 }; } /** 更新centent高度 */ private _updateContentHeight() { 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); - 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坐标系的对标 */ @@ -204,11 +371,61 @@ export default class RecycleScroll extends Component { if (item["needRender"] || force) { this.onItemRender(index, item); 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) { + 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) { this._initCounter += dt; if (this._initCounter >= this._initTimer) { @@ -228,14 +445,15 @@ export default class RecycleScroll extends Component { const isDown = dtY < 0; const viewHalfH = this._viewH >> 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; + if (!isLineChange && !this._isResizeFinish) return; this._isResizeFinish = false; this._lineIndex = lineIndex; const pageHeight = this._itemH * this._viewRow; 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; for (let i = 0; i < itemsLen; ++i) { const index = i; @@ -271,34 +489,101 @@ export default class RecycleScroll extends Component { this._updateItem(currIndex, item); } this._itemsUUIDToIndex[item.uuid] = currIndex; - } - } -} -/** 分帧执行 */ -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; + this._indexToItem[currIndex] = item; } } -} + + /** 获取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; + } +} \ No newline at end of file diff --git a/settings/v2/packages/information.json b/settings/v2/packages/information.json index 96d7a95..2cbe6a1 100644 --- a/settings/v2/packages/information.json +++ b/settings/v2/packages/information.json @@ -6,7 +6,7 @@ "label": "customSplash", "enable": true, "customSplash": { - "complete": false, + "complete": true, "form": "https://creator-api.cocos.com/api/form/show?sid=fa6ec2019b107316262fad6f726e3d76" } }, @@ -15,7 +15,7 @@ "label": "removeSplash", "enable": true, "removeSplash": { - "complete": false, + "complete": true, "form": "https://creator-api.cocos.com/api/form/show?sid=fa6ec2019b107316262fad6f726e3d76" } }