├── example ├── 2.4.6 │ └── VirtualListProject │ │ ├── README.md │ │ ├── settings │ │ ├── services.json │ │ ├── builder.panel.json │ │ ├── project.json │ │ └── builder.json │ │ ├── template-banner.png │ │ ├── template.json │ │ ├── assets │ │ ├── Texture │ │ │ ├── HelloWorld.png │ │ │ ├── singleColor.png │ │ │ ├── singleColor.png.meta │ │ │ └── HelloWorld.png.meta │ │ ├── Scene │ │ │ ├── helloworld.fire.meta │ │ │ └── helloworld.fire │ │ ├── Prefab │ │ │ ├── Item.prefab.meta │ │ │ ├── Item.ts.meta │ │ │ ├── Item.ts │ │ │ └── Item.prefab │ │ ├── Script │ │ │ ├── HelloWorld.ts.meta │ │ │ ├── core │ │ │ │ ├── AItemRenerer.ts.meta │ │ │ │ ├── AVirtualScrollView.ts.meta │ │ │ │ ├── AItemRenerer.ts │ │ │ │ └── AVirtualScrollView.ts │ │ │ ├── core.meta │ │ │ └── HelloWorld.ts │ │ ├── migration │ │ │ ├── use_v2.1-2.2.1_cc.Toggle_event.js.meta │ │ │ └── use_v2.1-2.2.1_cc.Toggle_event.js │ │ ├── Prefab.meta │ │ ├── Scene.meta │ │ ├── Script.meta │ │ ├── Texture.meta │ │ └── migration.meta │ │ ├── project.json │ │ ├── jsconfig.json │ │ ├── tsconfig.json │ │ └── .gitignore └── 3.2.0 │ └── virtualList │ ├── settings │ └── v2 │ │ └── packages │ │ ├── builder.json │ │ ├── device.json │ │ ├── program.json │ │ ├── project.json │ │ ├── cocos-service.json │ │ └── engine.json │ ├── package.json │ ├── .creator │ └── asset-template │ │ └── typescript │ │ └── Custom Script Template Help Documentation.url │ ├── assets │ ├── VirtualListChildrenAutoSize.scene.meta │ ├── Scripts │ │ ├── HelloWorld.ts.meta │ │ ├── ItemGrid.ts.meta │ │ ├── ItemAutoSizeGrid.ts.meta │ │ ├── core │ │ │ ├── AItemRenderer.ts.meta │ │ │ ├── AVirtualScrollView.ts.meta │ │ │ ├── AItemRenderer.ts │ │ │ └── AVirtualScrollView.ts │ │ ├── core.meta │ │ ├── ItemGrid.ts │ │ ├── HelloWorld.ts │ │ └── ItemAutoSizeGrid.ts │ ├── VirtualList.scene.meta │ ├── Scripts.meta │ ├── ItemGrid.prefab.meta │ ├── ItemAutoSizeGrid.prefab.meta │ ├── ItemGrid.prefab │ └── ItemAutoSizeGrid.prefab │ ├── tsconfig.json │ └── .gitignore ├── README.md └── core ├── 3.2.0 ├── AItemRenderer.ts └── AVirtualScrollView.ts └── 2.4.6 ├── AItemRenerer.ts └── AVirtualScrollView.ts /example/2.4.6/VirtualListProject/README.md: -------------------------------------------------------------------------------- 1 | # hello-world 2 | Hello world new project template. 3 | -------------------------------------------------------------------------------- /example/3.2.0/virtualList/settings/v2/packages/builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "__version__": "1.3.2" 3 | } 4 | -------------------------------------------------------------------------------- /example/3.2.0/virtualList/settings/v2/packages/device.json: -------------------------------------------------------------------------------- 1 | { 2 | "__version__": "1.0.1" 3 | } 4 | -------------------------------------------------------------------------------- /example/3.2.0/virtualList/settings/v2/packages/program.json: -------------------------------------------------------------------------------- 1 | { 2 | "__version__": "1.0.0" 3 | } 4 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/settings/services.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": { 3 | "name": "未知游戏", 4 | "appid": "UNKNOW" 5 | } 6 | } -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/template-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuhaoyuxuan/cocos-virtual-list/HEAD/example/2.4.6/VirtualListProject/template-banner.png -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TEMPLATES.helloworld-ts.name", 3 | "desc": "TEMPLATES.helloworld-ts.desc", 4 | "banner": "template-banner.png" 5 | } -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Texture/HelloWorld.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuhaoyuxuan/cocos-virtual-list/HEAD/example/2.4.6/VirtualListProject/assets/Texture/HelloWorld.png -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Texture/singleColor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuhaoyuxuan/cocos-virtual-list/HEAD/example/2.4.6/VirtualListProject/assets/Texture/singleColor.png -------------------------------------------------------------------------------- /example/3.2.0/virtualList/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "creator": { 3 | "version": "3.5.2" 4 | }, 5 | "name": "virtualList", 6 | "uuid": "c91dc9b9-3df8-4ade-ab49-8d89d3abb3ac", 7 | "version": "3.5.2" 8 | } 9 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "engine": "cocos2d-html5", 3 | "packages": "packages", 4 | "version": "2.4.6", 5 | "id": "bccc8cdc-e144-40b9-a006-80cd9ca7a67e", 6 | "isNew": false 7 | } -------------------------------------------------------------------------------- /example/3.2.0/virtualList/.creator/asset-template/typescript/Custom Script Template Help Documentation.url: -------------------------------------------------------------------------------- 1 | [InternetShortcut] 2 | URL=https://docs.cocos.com/creator/manual/en/scripting/setup.html#custom-script-template -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/VirtualListChildrenAutoSize.scene.meta: -------------------------------------------------------------------------------- 1 | {"ver":"1.1.38","importer":"scene","imported":true,"uuid":"4d9b028e-d182-4788-9fff-eeaa2f45f753","files":[".json"],"subMetas":{},"userData":{}} 2 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Scene/helloworld.fire.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.3.2", 3 | "uuid": "2d2f792f-a40c-49bb-a189-ed176a246e49", 4 | "importer": "scene", 5 | "asyncLoadAssets": false, 6 | "autoReleaseAssets": false, 7 | "subMetas": {} 8 | } -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/settings/builder.panel.json: -------------------------------------------------------------------------------- 1 | { 2 | "excludeScenes": [], 3 | "packageName": "org.cocos2d.helloworld", 4 | "platform": "web-mobile", 5 | "startScene": "2d2f792f-a40c-49bb-a189-ed176a246e49", 6 | "title": "HelloWorld" 7 | } -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/Scripts/HelloWorld.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "4.0.23", 3 | "importer": "typescript", 4 | "imported": true, 5 | "uuid": "8629c107-11fb-4351-a470-818dae35f682", 6 | "files": [], 7 | "subMetas": {}, 8 | "userData": {} 9 | } 10 | -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/Scripts/ItemGrid.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "4.0.23", 3 | "importer": "typescript", 4 | "imported": true, 5 | "uuid": "ea76c9c0-ad8d-4e9c-ae1d-728679a3ac50", 6 | "files": [], 7 | "subMetas": {}, 8 | "userData": {} 9 | } 10 | -------------------------------------------------------------------------------- /example/3.2.0/virtualList/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Base configuration. Do not edit this field. */ 3 | "extends": "./temp/tsconfig.cocos.json", 4 | 5 | /* Add your custom configuration here. */ 6 | "compilerOptions": { 7 | "strict": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/Scripts/ItemAutoSizeGrid.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "4.0.23", 3 | "importer": "typescript", 4 | "imported": true, 5 | "uuid": "b5f2c00d-7e44-44a2-a93b-fbe57216ca91", 6 | "files": [], 7 | "subMetas": {}, 8 | "userData": {} 9 | } 10 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Prefab/Item.prefab.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.3.2", 3 | "uuid": "4d3d40d7-266e-49b6-9c68-51c55ca32e2e", 4 | "importer": "prefab", 5 | "optimizationPolicy": "AUTO", 6 | "asyncLoadAssets": false, 7 | "readonly": false, 8 | "subMetas": {} 9 | } -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/Scripts/core/AItemRenderer.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "4.0.23", 3 | "importer": "typescript", 4 | "imported": true, 5 | "uuid": "786078e7-c349-499d-af71-b3e3ecca3753", 6 | "files": [], 7 | "subMetas": {}, 8 | "userData": {} 9 | } 10 | -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/Scripts/core/AVirtualScrollView.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "4.0.23", 3 | "importer": "typescript", 4 | "imported": true, 5 | "uuid": "94d7cc1c-f0ca-4970-a0cc-207bceccbc50", 6 | "files": [], 7 | "subMetas": {}, 8 | "userData": {} 9 | } 10 | -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/VirtualList.scene.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.38", 3 | "importer": "scene", 4 | "imported": true, 5 | "uuid": "4897ed4b-4eda-4117-b039-a3d0423047cd", 6 | "files": [ 7 | ".json" 8 | ], 9 | "subMetas": {}, 10 | "userData": {} 11 | } 12 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs" 5 | }, 6 | "exclude": [ 7 | "node_modules", 8 | "library", 9 | "local", 10 | "settings", 11 | "temp" 12 | ] 13 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cocos-virtual-list 2 | 3 | cocos虚拟列表 支持子项动态宽高 (缺点第一次会刷新两次数据源) 4 | core内有2.x版本 和 3.x版本的 5 | example内有2.4.6版本和3.2.0版本 6 | 7 | demo:遮罩关闭了,方便查看,控制台 有打印修改数据的内容 8 | 9 | demo:http://82.156.86.250/test/virtualList/index.html 10 | demo子项动态宽高:http://82.156.86.250/test/virtualListAutoSize/index.html 11 | 12 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Prefab/Item.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.0", 3 | "uuid": "e7675566-2e61-4016-9409-c8969d7c9dc6", 4 | "importer": "typescript", 5 | "isPlugin": false, 6 | "loadPluginInWeb": true, 7 | "loadPluginInNative": true, 8 | "loadPluginInEditor": false, 9 | "subMetas": {} 10 | } -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/Scripts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.0", 3 | "importer": "directory", 4 | "imported": true, 5 | "uuid": "19f78b9a-dcfa-465b-85c1-6fb1d95049af", 6 | "files": [], 7 | "subMetas": {}, 8 | "userData": { 9 | "compressionType": {}, 10 | "isRemoteBundle": {} 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Script/HelloWorld.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.0", 3 | "uuid": "b3da8d4e-5dab-463e-ab09-337a08ec0d40", 4 | "importer": "typescript", 5 | "isPlugin": false, 6 | "loadPluginInWeb": true, 7 | "loadPluginInNative": true, 8 | "loadPluginInEditor": false, 9 | "subMetas": {} 10 | } -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/ItemGrid.prefab.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.38", 3 | "importer": "prefab", 4 | "imported": true, 5 | "uuid": "8ab4f841-9cd1-41cc-9c00-1f9544b4182d", 6 | "files": [ 7 | ".json" 8 | ], 9 | "subMetas": {}, 10 | "userData": { 11 | "syncNodeName": "ItemGrid" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/Scripts/core.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.0", 3 | "importer": "directory", 4 | "imported": true, 5 | "uuid": "ebefd0f7-8a01-4739-ba7c-e1b9f219a6f7", 6 | "files": [], 7 | "subMetas": {}, 8 | "userData": { 9 | "compressionType": {}, 10 | "isRemoteBundle": {} 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Script/core/AItemRenerer.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.0", 3 | "uuid": "0b715eda-404a-4ee3-8cd9-5b266c023f1d", 4 | "importer": "typescript", 5 | "isPlugin": false, 6 | "loadPluginInWeb": true, 7 | "loadPluginInNative": true, 8 | "loadPluginInEditor": false, 9 | "subMetas": {} 10 | } -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Script/core/AVirtualScrollView.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.0", 3 | "uuid": "46ab6d5a-36f3-4d3f-a17d-12c5c880e6bc", 4 | "importer": "typescript", 5 | "isPlugin": false, 6 | "loadPluginInWeb": true, 7 | "loadPluginInNative": true, 8 | "loadPluginInEditor": false, 9 | "subMetas": {} 10 | } -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/ItemAutoSizeGrid.prefab.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.38", 3 | "importer": "prefab", 4 | "imported": true, 5 | "uuid": "6eb9cd6d-59de-4988-866c-40cd8d8ffd8a", 6 | "files": [ 7 | ".json" 8 | ], 9 | "subMetas": {}, 10 | "userData": { 11 | "syncNodeName": "ItemAutoSizeGrid" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/migration/use_v2.1-2.2.1_cc.Toggle_event.js.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.0", 3 | "uuid": "23936a02-10e9-48cb-b359-5a0dc8b68cc5", 4 | "importer": "javascript", 5 | "isPlugin": false, 6 | "loadPluginInWeb": true, 7 | "loadPluginInNative": true, 8 | "loadPluginInEditor": false, 9 | "subMetas": {} 10 | } -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Prefab.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.3", 3 | "uuid": "43e51484-ff4d-4b2e-8f5e-ea8c34311368", 4 | "importer": "folder", 5 | "isBundle": false, 6 | "bundleName": "", 7 | "priority": 1, 8 | "compressionType": {}, 9 | "optimizeHotUpdate": {}, 10 | "inlineSpriteFrames": {}, 11 | "isRemoteBundle": {}, 12 | "subMetas": {} 13 | } -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Scene.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.3", 3 | "uuid": "29f52784-2fca-467b-92e7-8fd9ef8c57b7", 4 | "importer": "folder", 5 | "isBundle": false, 6 | "bundleName": "", 7 | "priority": 1, 8 | "compressionType": {}, 9 | "optimizeHotUpdate": {}, 10 | "inlineSpriteFrames": {}, 11 | "isRemoteBundle": {}, 12 | "subMetas": {} 13 | } -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Script.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.3", 3 | "uuid": "4734c20c-0db8-4eb2-92ea-e692f4d70934", 4 | "importer": "folder", 5 | "isBundle": false, 6 | "bundleName": "", 7 | "priority": 1, 8 | "compressionType": {}, 9 | "optimizeHotUpdate": {}, 10 | "inlineSpriteFrames": {}, 11 | "isRemoteBundle": {}, 12 | "subMetas": {} 13 | } -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Texture.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.3", 3 | "uuid": "7b81d4e8-ec84-4716-968d-500ac1d78a54", 4 | "importer": "folder", 5 | "isBundle": false, 6 | "bundleName": "", 7 | "priority": 1, 8 | "compressionType": {}, 9 | "optimizeHotUpdate": {}, 10 | "inlineSpriteFrames": {}, 11 | "isRemoteBundle": {}, 12 | "subMetas": {} 13 | } -------------------------------------------------------------------------------- /example/3.2.0/virtualList/settings/v2/packages/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "__version__": "1.0.2", 3 | "general": { 4 | "designResolution": { 5 | "width": 1920, 6 | "height": 1080, 7 | "fitWidth": false, 8 | "fitHeight": true 9 | } 10 | }, 11 | "fbx": { 12 | "legacyFbxImporter": { 13 | "visible": true 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Script/core.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.3", 3 | "uuid": "dd4c89b6-c8bb-43ce-b14f-c63762cf142f", 4 | "importer": "folder", 5 | "isBundle": false, 6 | "bundleName": "", 7 | "priority": 1, 8 | "compressionType": {}, 9 | "optimizeHotUpdate": {}, 10 | "inlineSpriteFrames": {}, 11 | "isRemoteBundle": {}, 12 | "subMetas": {} 13 | } -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/migration.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.3", 3 | "uuid": "35fa9cf7-c8a4-4a52-956b-580f1f7b3cbb", 4 | "importer": "folder", 5 | "isBundle": false, 6 | "bundleName": "", 7 | "priority": 1, 8 | "compressionType": {}, 9 | "optimizeHotUpdate": {}, 10 | "inlineSpriteFrames": {}, 11 | "isRemoteBundle": {}, 12 | "subMetas": {} 13 | } -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/Scripts/ItemGrid.ts: -------------------------------------------------------------------------------- 1 | import { _decorator, Component, Node, find, Label } from 'cc'; 2 | import { AItemRenderer } from './core/AItemRenderer'; 3 | const { ccclass, property } = _decorator; 4 | 5 | @ccclass('ItemGrid') 6 | export class ItemGrid extends AItemRenderer { 7 | 8 | protected dataChanged(): void { 9 | find("lblName",this.node).getComponent(Label).string = this.data; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": [ "es2015", "es2017", "dom" ], 5 | "target": "es5", 6 | "experimentalDecorators": true, 7 | "skipLibCheck": true, 8 | "outDir": "temp/vscode-dist", 9 | "forceConsistentCasingInFileNames": true 10 | }, 11 | "exclude": [ 12 | "node_modules", 13 | "library", 14 | "local", 15 | "temp", 16 | "build", 17 | "settings" 18 | ] 19 | } -------------------------------------------------------------------------------- /example/3.2.0/virtualList/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | #/////////////////////////// 3 | # Cocos Creator 3D Project 4 | #/////////////////////////// 5 | library/ 6 | temp/ 7 | local/ 8 | build/ 9 | profiles/ 10 | native 11 | #////////////////////////// 12 | # NPM 13 | #////////////////////////// 14 | node_modules/ 15 | 16 | #////////////////////////// 17 | # VSCode 18 | #////////////////////////// 19 | .vscode/ 20 | 21 | #////////////////////////// 22 | # WebStorm 23 | #////////////////////////// 24 | .idea/ -------------------------------------------------------------------------------- /example/3.2.0/virtualList/settings/v2/packages/cocos-service.json: -------------------------------------------------------------------------------- 1 | { 2 | "__version__": "3.0.3", 3 | "game": { 4 | "name": "未知游戏", 5 | "app_id": "UNKNOW", 6 | "c_id": "0" 7 | }, 8 | "appConfigMaps": [ 9 | { 10 | "app_id": "UNKNOW", 11 | "config_id": "7eb28d" 12 | } 13 | ], 14 | "configs": [ 15 | { 16 | "app_id": "UNKNOW", 17 | "config_id": "7eb28d", 18 | "config_name": "Default", 19 | "config_remarks": "", 20 | "services": [] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Prefab/Item.ts: -------------------------------------------------------------------------------- 1 | // Learn TypeScript: 2 | // - https://docs.cocos.com/creator/manual/en/scripting/typescript.html 3 | // Learn Attribute: 4 | // - https://docs.cocos.com/creator/manual/en/scripting/reference/attributes.html 5 | // Learn life-cycle callbacks: 6 | // - https://docs.cocos.com/creator/manual/en/scripting/life-cycle-callbacks.html 7 | 8 | import AItemRenderer from "../Script/core/AItemRenerer"; 9 | 10 | 11 | const {ccclass, property} = cc._decorator; 12 | 13 | @ccclass 14 | export default class item extends AItemRenderer { 15 | 16 | protected dataChanged(): void { 17 | cc.find("lblName",this.node).getComponent(cc.Label).string = this.data; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/migration/use_v2.1-2.2.1_cc.Toggle_event.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This script is automatically generated by Cocos Creator and is only used for projects compatible with the v2.1.0 ~ 2.2.1 version. 3 | * You do not need to manually add this script in any other project. 4 | * If you don't use cc.Toggle in your project, you can delete this script directly. 5 | * If your project is hosted in VCS such as git, submit this script together. 6 | * 7 | * 此脚本由 Cocos Creator 自动生成,仅用于兼容 v2.1.0 ~ 2.2.1 版本的工程, 8 | * 你无需在任何其它项目中手动添加此脚本。 9 | * 如果你的项目中没用到 Toggle,可直接删除该脚本。 10 | * 如果你的项目有托管于 git 等版本库,请将此脚本一并上传。 11 | */ 12 | 13 | if (cc.Toggle) { 14 | // Whether to trigger 'toggle' and 'checkEvents' events when modifying 'toggle.isChecked' in the code 15 | // 在代码中修改 'toggle.isChecked' 时是否触发 'toggle' 与 'checkEvents' 事件 16 | cc.Toggle._triggerEventInScript_isChecked = true; 17 | } 18 | -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/Scripts/HelloWorld.ts: -------------------------------------------------------------------------------- 1 | import { _decorator, Component, Node } from 'cc'; 2 | import AVirtualScrollView from './core/AVirtualScrollView'; 3 | const { ccclass, property } = _decorator; 4 | 5 | @ccclass('HelloWorld') 6 | export class HelloWorld extends Component { 7 | @property({ type: [AVirtualScrollView] }) 8 | public lists: AVirtualScrollView[] = []; 9 | private idx: number = 0; 10 | private dataL: string[] = []; 11 | start() { 12 | let datas: string[] = [] 13 | for (var i = 0; i < 100; i++) { 14 | datas.push(i + ""); 15 | } 16 | 17 | this.lists.forEach(list => { 18 | list?.refreshData(datas); 19 | }) 20 | } 21 | 22 | onAddItem(): void { 23 | this.idx++; 24 | this.dataL.push(this.idx + ""); 25 | this.lists.forEach(list => { 26 | list?.refreshData(this.dataL); 27 | }) 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/settings/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "collision-matrix": [ 3 | [ 4 | true 5 | ] 6 | ], 7 | "excluded-modules": [ 8 | "3D Physics/cannon.js", 9 | "3D Physics/Builtin", 10 | "3D Particle", 11 | "SafeArea" 12 | ], 13 | "group-list": [ 14 | "default" 15 | ], 16 | "start-scene": "2d2f792f-a40c-49bb-a189-ed176a246e49", 17 | "design-resolution-width": 960, 18 | "design-resolution-height": 640, 19 | "fit-width": false, 20 | "fit-height": true, 21 | "use-project-simulator-setting": false, 22 | "simulator-orientation": false, 23 | "use-customize-simulator": false, 24 | "simulator-resolution": { 25 | "width": 960, 26 | "height": 640 27 | }, 28 | "last-module-event-record-time": 1656670481785, 29 | "assets-sort-type": "name", 30 | "facebook": { 31 | "enable": false, 32 | "appID": "", 33 | "live": { 34 | "enable": false 35 | }, 36 | "audience": { 37 | "enable": false 38 | } 39 | }, 40 | "migrate-history": [ 41 | "cloud-function" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /core/3.2.0/AItemRenderer.ts: -------------------------------------------------------------------------------- 1 | import { _decorator, Component } from 'cc'; 2 | const { ccclass, property } = _decorator; 3 | 4 | 5 | /** 6 | * 单项渲染基类 T数据结构 7 | * @author slf 8 | * */ 9 | @ccclass('ItemRenderer') 10 | export abstract class AItemRenderer extends Component { 11 | /**调用列表 回调函数 回调作用域*/ 12 | protected callback: Function; 13 | protected cbThis: any; 14 | 15 | protected _data: T 16 | public get data(): T { 17 | return this._data; 18 | } 19 | 20 | public set data(value: T) { 21 | this._data = value; 22 | this.dataChanged(); 23 | } 24 | 25 | public refreshData(): void { 26 | this.dataChanged(); 27 | } 28 | 29 | /**数据发生变化子类重写 */ 30 | protected abstract dataChanged(): void 31 | /**注册回调 */ 32 | public registerCallback(cb: Function, cbT?: any): void { 33 | this.callback = cb; 34 | this.cbThis = cbT; 35 | } 36 | /**派发回调 */ 37 | protected emitCallback() { 38 | this.callback && this.callback.call(this.cbThis, this._data); 39 | } 40 | 41 | } 42 | 43 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Texture/singleColor.png.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "2.3.7", 3 | "uuid": "a8027877-d8d6-4645-97a0-52d4a0123dba", 4 | "importer": "texture", 5 | "type": "sprite", 6 | "wrapMode": "clamp", 7 | "filterMode": "bilinear", 8 | "premultiplyAlpha": false, 9 | "genMipmaps": false, 10 | "packable": true, 11 | "width": 2, 12 | "height": 2, 13 | "platformSettings": {}, 14 | "subMetas": { 15 | "singleColor": { 16 | "ver": "1.0.6", 17 | "uuid": "410fb916-8721-4663-bab8-34397391ace7", 18 | "importer": "sprite-frame", 19 | "rawTextureUuid": "a8027877-d8d6-4645-97a0-52d4a0123dba", 20 | "trimType": "auto", 21 | "trimThreshold": 1, 22 | "rotated": false, 23 | "offsetX": 0, 24 | "offsetY": 0, 25 | "trimX": 0, 26 | "trimY": 0, 27 | "width": 2, 28 | "height": 2, 29 | "rawWidth": 2, 30 | "rawHeight": 2, 31 | "borderTop": 0, 32 | "borderBottom": 0, 33 | "borderLeft": 0, 34 | "borderRight": 0, 35 | "subMetas": {} 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Texture/HelloWorld.png.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "2.3.7", 3 | "uuid": "6aa0aa6a-ebee-4155-a088-a687a6aadec4", 4 | "importer": "texture", 5 | "type": "sprite", 6 | "wrapMode": "clamp", 7 | "filterMode": "bilinear", 8 | "premultiplyAlpha": false, 9 | "genMipmaps": false, 10 | "packable": true, 11 | "width": 195, 12 | "height": 270, 13 | "platformSettings": {}, 14 | "subMetas": { 15 | "HelloWorld": { 16 | "ver": "1.0.6", 17 | "uuid": "31bc895a-c003-4566-a9f3-2e54ae1c17dc", 18 | "importer": "sprite-frame", 19 | "rawTextureUuid": "6aa0aa6a-ebee-4155-a088-a687a6aadec4", 20 | "trimType": "auto", 21 | "trimThreshold": 1, 22 | "rotated": false, 23 | "offsetX": 0, 24 | "offsetY": 0, 25 | "trimX": 0, 26 | "trimY": 0, 27 | "width": 195, 28 | "height": 270, 29 | "rawWidth": 195, 30 | "rawHeight": 270, 31 | "borderTop": 0, 32 | "borderBottom": 0, 33 | "borderLeft": 0, 34 | "borderRight": 0, 35 | "subMetas": {} 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/Scripts/core/AItemRenderer.ts: -------------------------------------------------------------------------------- 1 | import { _decorator, Component } from 'cc'; 2 | const { ccclass, property } = _decorator; 3 | 4 | 5 | /** 6 | * 单项渲染基类 T数据结构 7 | * @author slf 8 | * */ 9 | @ccclass('ItemRenderer') 10 | export abstract class AItemRenderer extends Component { 11 | /**调用列表 回调函数 回调作用域*/ 12 | protected callback: Function; 13 | protected cbThis: any; 14 | 15 | protected _data: T 16 | public get data(): T { 17 | return this._data; 18 | } 19 | 20 | public set data(value: T) { 21 | this._data = value; 22 | this.dataChanged(); 23 | } 24 | 25 | public refreshData(): void { 26 | this.dataChanged(); 27 | } 28 | 29 | /**数据发生变化子类重写 */ 30 | protected abstract dataChanged(): void 31 | /**注册回调 */ 32 | public registerCallback(cb: Function, cbT?: any): void { 33 | this.callback = cb; 34 | this.cbThis = cbT; 35 | } 36 | /**派发回调 */ 37 | protected emitCallback() { 38 | this.callback && this.callback.call(this.cbThis, this._data); 39 | } 40 | 41 | } 42 | 43 | -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/Scripts/ItemAutoSizeGrid.ts: -------------------------------------------------------------------------------- 1 | import { _decorator, Color, Component, find, Label, Node, Sprite } from 'cc'; 2 | import { AItemRenderer } from './core/AItemRenderer'; 3 | const { ccclass, property } = _decorator; 4 | 5 | const color = new Color(255, 207, 155, 255); 6 | 7 | @ccclass('ItemAutoSizeGrid') 8 | export class ItemAutoSizeGrid extends AItemRenderer { 9 | protected dataChanged(): void { 10 | find("lblName", this.node).getComponent(Label).string = this.data; 11 | let h = Math.floor(80 + Math.random() * 100); 12 | let w = Math.floor(80 + Math.random() * 100); 13 | this["h_" + this.data] = this["h_" + this.data] ?? h; 14 | this["w_" + this.data] = this["w_" + this.data] ?? w; 15 | if (!this["c_" + this.data]) { 16 | color.r = 255 * Math.random(); 17 | color.g = 255 * Math.random(); 18 | color.b = 255 * Math.random(); 19 | this["c_" + this.data] = color.clone(); 20 | } 21 | this.node.getComponent(Sprite).color = this["c_" + this.data]; 22 | this.node._uiProps.uiTransformComp.setContentSize(this["w_" + this.data], this["h_" + this.data]); 23 | } 24 | 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/.gitignore: -------------------------------------------------------------------------------- 1 | #///////////////////////////////////////////////////////////////////////////// 2 | # Fireball Projects 3 | #///////////////////////////////////////////////////////////////////////////// 4 | 5 | /library/ 6 | /temp/ 7 | /local/ 8 | /build/ 9 | 10 | #///////////////////////////////////////////////////////////////////////////// 11 | # npm files 12 | #///////////////////////////////////////////////////////////////////////////// 13 | 14 | npm-debug.log 15 | node_modules/ 16 | 17 | #///////////////////////////////////////////////////////////////////////////// 18 | # Logs and databases 19 | #///////////////////////////////////////////////////////////////////////////// 20 | 21 | *.log 22 | *.sql 23 | *.sqlite 24 | 25 | #///////////////////////////////////////////////////////////////////////////// 26 | # files for debugger 27 | #///////////////////////////////////////////////////////////////////////////// 28 | 29 | *.sln 30 | *.pidb 31 | *.suo 32 | 33 | #///////////////////////////////////////////////////////////////////////////// 34 | # OS generated files 35 | #///////////////////////////////////////////////////////////////////////////// 36 | 37 | .DS_Store 38 | ehthumbs.db 39 | Thumbs.db 40 | 41 | #///////////////////////////////////////////////////////////////////////////// 42 | # WebStorm files 43 | #///////////////////////////////////////////////////////////////////////////// 44 | 45 | .idea/ 46 | 47 | #////////////////////////// 48 | # VS Code files 49 | #////////////////////////// 50 | 51 | .vscode/ 52 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/settings/builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "excludeScenes": [], 3 | "orientation": { 4 | "landscapeLeft": true, 5 | "landscapeRight": true, 6 | "portrait": false, 7 | "upsideDown": false 8 | }, 9 | "packageName": "org.cocos2d.helloworld", 10 | "startScene": "2d2f792f-a40c-49bb-a189-ed176a246e49", 11 | "title": "hello_world", 12 | "webOrientation": "auto", 13 | "inlineSpriteFrames": true, 14 | "inlineSpriteFrames_native": true, 15 | "mainCompressionType": "default", 16 | "mainIsRemote": false, 17 | "optimizeHotUpdate": false, 18 | "md5Cache": true, 19 | "nativeMd5Cache": true, 20 | "encryptJs": true, 21 | "xxteaKey": "a953ba66-e34b-47", 22 | "zipCompressJs": true, 23 | "fb-instant-games": {}, 24 | "android": { 25 | "packageName": "org.cocos2d.demo", 26 | "REMOTE_SERVER_ROOT": "" 27 | }, 28 | "ios": { 29 | "packageName": "org.cocos2d.demo", 30 | "REMOTE_SERVER_ROOT": "", 31 | "ios_enable_jit": true 32 | }, 33 | "mac": { 34 | "packageName": "org.cocos2d.demo", 35 | "REMOTE_SERVER_ROOT": "", 36 | "width": 1280, 37 | "height": 720 38 | }, 39 | "win32": { 40 | "REMOTE_SERVER_ROOT": "", 41 | "width": 1280, 42 | "height": 720 43 | }, 44 | "android-instant": { 45 | "packageName": "org.cocos2d.demo", 46 | "REMOTE_SERVER_ROOT": "", 47 | "pathPattern": "", 48 | "scheme": "https", 49 | "host": "", 50 | "skipRecord": false, 51 | "recordPath": "" 52 | }, 53 | "appBundle": false, 54 | "agreements": {} 55 | } 56 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Script/HelloWorld.ts: -------------------------------------------------------------------------------- 1 | // Learn TypeScript: 2 | // - https://docs.cocos.com/creator/manual/en/scripting/typescript.html 3 | // Learn Attribute: 4 | // - https://docs.cocos.com/creator/manual/en/scripting/reference/attributes.html 5 | // Learn life-cycle callbacks: 6 | // - https://docs.cocos.com/creator/manual/en/scripting/life-cycle-callbacks.html 7 | 8 | import AVirtualScrollView from "./core/AVirtualScrollView"; 9 | 10 | const {ccclass, property} = cc._decorator; 11 | 12 | @ccclass 13 | export default class NewClass extends cc.Component { 14 | 15 | @property(AVirtualScrollView) 16 | test1: AVirtualScrollView = null; 17 | @property(AVirtualScrollView) 18 | test2: AVirtualScrollView = null; 19 | @property(AVirtualScrollView) 20 | test3: AVirtualScrollView = null; 21 | @property(AVirtualScrollView) 22 | test4: AVirtualScrollView = null; 23 | 24 | start () { 25 | cc.debug.setDisplayStats(true); 26 | var dataL:string[]=[]; 27 | for(var i = 0;i<100;i++){ 28 | dataL.push(i+""); 29 | } 30 | 31 | this.test1.refreshData(dataL); 32 | this.test2.refreshData(dataL); 33 | this.test3.refreshData(dataL); 34 | this.test4.refreshData(dataL); 35 | 36 | setTimeout(()=>{ 37 | dataL[1]="666"; 38 | this.test1.refreshData(dataL); 39 | this.test2.refreshData(dataL); 40 | this.test3.refreshData(dataL); 41 | this.test4.refreshData(dataL); 42 | },3000) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /core/2.4.6/AItemRenerer.ts: -------------------------------------------------------------------------------- 1 | const { ccclass, property } = cc._decorator; 2 | /** 3 | * 单项渲染基类 T数据结构 4 | * @author slf 5 | * */ 6 | @ccclass 7 | export default class AItemRenderer extends cc.Component { 8 | @property({displayName:"是否添加点击事件"}) 9 | isClick:boolean = false; 10 | 11 | protected callback: Function; //回调函数 12 | protected cbThis: any; //回调作用域 13 | 14 | private _data: T;//数据结构 15 | public get data(): T { 16 | return this._data; 17 | } 18 | public set data(v: T) { 19 | this._data = v; 20 | this.dataChanged(); 21 | } 22 | 23 | /**数据发生变化 子类重写*/ 24 | protected dataChanged(): void { } 25 | 26 | /**刷新数据 */ 27 | public refreshData(): void { 28 | this.dataChanged(); 29 | } 30 | 31 | /**销毁 */ 32 | public onDestroy(): void { 33 | this._data = null; 34 | } 35 | 36 | /** 37 | * 设置点击回调 38 | * @param cb 回调函数 39 | * @param cbT 回调作用域 40 | */ 41 | public setTouchCallback(cb?: Function, cbT?: any): void { 42 | this.callback = cb; 43 | this.cbThis = cbT; 44 | if (this.node) { 45 | if (this.node.hasEventListener(cc.Node.EventType.TOUCH_END)) { 46 | this.node.off(cc.Node.EventType.TOUCH_END, this.onClickCallback, this); 47 | } 48 | this.node.on(cc.Node.EventType.TOUCH_END, this.onClickCallback, this); 49 | } 50 | } 51 | 52 | /** 53 | * 预制体点击回调 会携带data 54 | * @param e 55 | */ 56 | protected onClickCallback(e: cc.Event): void { 57 | this.callback && this.callback.call(this.cbThis, this.data); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Script/core/AItemRenerer.ts: -------------------------------------------------------------------------------- 1 | const { ccclass, property } = cc._decorator; 2 | /** 3 | * 单项渲染基类 T数据结构 4 | * @author slf 5 | * */ 6 | @ccclass 7 | export default class AItemRenderer extends cc.Component { 8 | @property({displayName:"是否添加点击事件"}) 9 | isClick:boolean = false; 10 | 11 | protected callback: Function; //回调函数 12 | protected cbThis: any; //回调作用域 13 | 14 | private _data: T;//数据结构 15 | public get data(): T { 16 | return this._data; 17 | } 18 | public set data(v: T) { 19 | this._data = v; 20 | this.dataChanged(); 21 | } 22 | 23 | /**数据发生变化 子类重写*/ 24 | protected dataChanged(): void { } 25 | 26 | /**刷新数据 */ 27 | public refreshData(): void { 28 | this.dataChanged(); 29 | } 30 | 31 | /**销毁 */ 32 | public onDestroy(): void { 33 | this._data = null; 34 | } 35 | 36 | /** 37 | * 设置点击回调 38 | * @param cb 回调函数 39 | * @param cbT 回调作用域 40 | */ 41 | public setTouchCallback(cb?: Function, cbT?: any): void { 42 | this.callback = cb; 43 | this.cbThis = cbT; 44 | if (this.node) { 45 | if (this.node.hasEventListener(cc.Node.EventType.TOUCH_END)) { 46 | this.node.off(cc.Node.EventType.TOUCH_END, this.onClickCallback, this); 47 | } 48 | this.node.on(cc.Node.EventType.TOUCH_END, this.onClickCallback, this); 49 | } 50 | } 51 | 52 | /** 53 | * 预制体点击回调 会携带data 54 | * @param e 55 | */ 56 | protected onClickCallback(e: cc.Event): void { 57 | this.callback && this.callback.call(this.cbThis, this.data); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /example/3.2.0/virtualList/settings/v2/packages/engine.json: -------------------------------------------------------------------------------- 1 | { 2 | "__version__": "1.0.6", 3 | "modules": { 4 | "cache": { 5 | "base": { 6 | "_value": true 7 | }, 8 | "graphcis": { 9 | "_value": true 10 | }, 11 | "gfx-webgl": { 12 | "_value": true 13 | }, 14 | "gfx-webgl2": { 15 | "_value": true 16 | }, 17 | "animation": { 18 | "_value": false 19 | }, 20 | "skeletal-animation": { 21 | "_value": false 22 | }, 23 | "3d": { 24 | "_value": false 25 | }, 26 | "2d": { 27 | "_value": true 28 | }, 29 | "ui": { 30 | "_value": true 31 | }, 32 | "particle": { 33 | "_value": false 34 | }, 35 | "physics": { 36 | "_value": false, 37 | "_option": "physics-ammo" 38 | }, 39 | "physics-ammo": { 40 | "_value": false 41 | }, 42 | "physics-cannon": { 43 | "_value": false 44 | }, 45 | "physics-physx": { 46 | "_value": false 47 | }, 48 | "physics-builtin": { 49 | "_value": false 50 | }, 51 | "physics-2d": { 52 | "_value": false, 53 | "_option": "physics-2d-box2d" 54 | }, 55 | "physics-2d-box2d": { 56 | "_value": false 57 | }, 58 | "physics-2d-builtin": { 59 | "_value": false 60 | }, 61 | "intersection-2d": { 62 | "_value": false 63 | }, 64 | "primitive": { 65 | "_value": false 66 | }, 67 | "profiler": { 68 | "_value": true 69 | }, 70 | "particle-2d": { 71 | "_value": false 72 | }, 73 | "audio": { 74 | "_value": false 75 | }, 76 | "video": { 77 | "_value": false 78 | }, 79 | "webview": { 80 | "_value": false 81 | }, 82 | "tween": { 83 | "_value": true 84 | }, 85 | "terrain": { 86 | "_value": false 87 | }, 88 | "tiled-map": { 89 | "_value": false 90 | }, 91 | "spine": { 92 | "_value": false 93 | }, 94 | "dragon-bones": { 95 | "_value": false 96 | }, 97 | "marionette": { 98 | "_value": false 99 | } 100 | }, 101 | "flags": {}, 102 | "includeModules": [ 103 | "2d", 104 | "base", 105 | "gfx-webgl", 106 | "gfx-webgl2", 107 | "profiler", 108 | "tween", 109 | "ui" 110 | ], 111 | "noDeprecatedFeatures": { 112 | "value": false, 113 | "version": "" 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/ItemGrid.prefab: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "__type__": "cc.Prefab", 4 | "_name": "", 5 | "_objFlags": 0, 6 | "_native": "", 7 | "data": { 8 | "__id__": 1 9 | }, 10 | "optimizationPolicy": 0, 11 | "persistent": false, 12 | "asyncLoadAssets": false 13 | }, 14 | { 15 | "__type__": "cc.Node", 16 | "_name": "ItemGrid", 17 | "_objFlags": 0, 18 | "__editorExtras__": {}, 19 | "_parent": null, 20 | "_children": [ 21 | { 22 | "__id__": 2 23 | } 24 | ], 25 | "_active": true, 26 | "_components": [ 27 | { 28 | "__id__": 8 29 | }, 30 | { 31 | "__id__": 10 32 | }, 33 | { 34 | "__id__": 12 35 | } 36 | ], 37 | "_prefab": { 38 | "__id__": 14 39 | }, 40 | "_lpos": { 41 | "__type__": "cc.Vec3", 42 | "x": 0, 43 | "y": 0, 44 | "z": 0 45 | }, 46 | "_lrot": { 47 | "__type__": "cc.Quat", 48 | "x": 0, 49 | "y": 0, 50 | "z": 0, 51 | "w": 1 52 | }, 53 | "_lscale": { 54 | "__type__": "cc.Vec3", 55 | "x": 1, 56 | "y": 1, 57 | "z": 1 58 | }, 59 | "_layer": 33554432, 60 | "_euler": { 61 | "__type__": "cc.Vec3", 62 | "x": 0, 63 | "y": 0, 64 | "z": 0 65 | }, 66 | "_id": "" 67 | }, 68 | { 69 | "__type__": "cc.Node", 70 | "_name": "lblName", 71 | "_objFlags": 0, 72 | "_parent": { 73 | "__id__": 1 74 | }, 75 | "_children": [], 76 | "_active": true, 77 | "_components": [ 78 | { 79 | "__id__": 3 80 | }, 81 | { 82 | "__id__": 5 83 | } 84 | ], 85 | "_prefab": { 86 | "__id__": 7 87 | }, 88 | "_lpos": { 89 | "__type__": "cc.Vec3", 90 | "x": 0, 91 | "y": 0, 92 | "z": 0 93 | }, 94 | "_lrot": { 95 | "__type__": "cc.Quat", 96 | "x": 0, 97 | "y": 0, 98 | "z": 0, 99 | "w": 1 100 | }, 101 | "_lscale": { 102 | "__type__": "cc.Vec3", 103 | "x": 1, 104 | "y": 1, 105 | "z": 1 106 | }, 107 | "_layer": 33554432, 108 | "_euler": { 109 | "__type__": "cc.Vec3", 110 | "x": 0, 111 | "y": 0, 112 | "z": 0 113 | }, 114 | "_id": "" 115 | }, 116 | { 117 | "__type__": "cc.UITransform", 118 | "_name": "", 119 | "_objFlags": 0, 120 | "node": { 121 | "__id__": 2 122 | }, 123 | "_enabled": true, 124 | "__prefab": { 125 | "__id__": 4 126 | }, 127 | "_contentSize": { 128 | "__type__": "cc.Size", 129 | "width": 11.12, 130 | "height": 50.4 131 | }, 132 | "_anchorPoint": { 133 | "__type__": "cc.Vec2", 134 | "x": 0.5, 135 | "y": 0.5 136 | }, 137 | "_id": "" 138 | }, 139 | { 140 | "__type__": "cc.CompPrefabInfo", 141 | "fileId": "37f0LU8ulLEqd17XNI0n//" 142 | }, 143 | { 144 | "__type__": "cc.Label", 145 | "_name": "", 146 | "_objFlags": 0, 147 | "node": { 148 | "__id__": 2 149 | }, 150 | "_enabled": true, 151 | "__prefab": { 152 | "__id__": 6 153 | }, 154 | "_visFlags": 0, 155 | "_customMaterial": null, 156 | "_srcBlendFactor": 2, 157 | "_dstBlendFactor": 4, 158 | "_color": { 159 | "__type__": "cc.Color", 160 | "r": 252, 161 | "g": 0, 162 | "b": 0, 163 | "a": 255 164 | }, 165 | "_string": "1", 166 | "_horizontalAlign": 1, 167 | "_verticalAlign": 1, 168 | "_actualFontSize": 20, 169 | "_fontSize": 20, 170 | "_fontFamily": "Arial", 171 | "_lineHeight": 40, 172 | "_overflow": 0, 173 | "_enableWrapText": true, 174 | "_font": null, 175 | "_isSystemFontUsed": true, 176 | "_spacingX": 0, 177 | "_isItalic": false, 178 | "_isBold": false, 179 | "_isUnderline": false, 180 | "_underlineHeight": 2, 181 | "_cacheMode": 0, 182 | "_id": "" 183 | }, 184 | { 185 | "__type__": "cc.CompPrefabInfo", 186 | "fileId": "8fXvxKWW1IMIZDXofa5Ayk" 187 | }, 188 | { 189 | "__type__": "cc.PrefabInfo", 190 | "root": { 191 | "__id__": 1 192 | }, 193 | "asset": { 194 | "__id__": 0 195 | }, 196 | "fileId": "87pLYCIK9FyI7XtRQUCt/O" 197 | }, 198 | { 199 | "__type__": "cc.UITransform", 200 | "_name": "", 201 | "_objFlags": 0, 202 | "node": { 203 | "__id__": 1 204 | }, 205 | "_enabled": true, 206 | "__prefab": { 207 | "__id__": 9 208 | }, 209 | "_contentSize": { 210 | "__type__": "cc.Size", 211 | "width": 80, 212 | "height": 80 213 | }, 214 | "_anchorPoint": { 215 | "__type__": "cc.Vec2", 216 | "x": 0.5, 217 | "y": 0.5 218 | }, 219 | "_id": "" 220 | }, 221 | { 222 | "__type__": "cc.CompPrefabInfo", 223 | "fileId": "7a+k13z/dE3bLzgCTNLDOj" 224 | }, 225 | { 226 | "__type__": "cc.Sprite", 227 | "_name": "", 228 | "_objFlags": 0, 229 | "node": { 230 | "__id__": 1 231 | }, 232 | "_enabled": true, 233 | "__prefab": { 234 | "__id__": 11 235 | }, 236 | "_visFlags": 0, 237 | "_customMaterial": null, 238 | "_srcBlendFactor": 2, 239 | "_dstBlendFactor": 4, 240 | "_color": { 241 | "__type__": "cc.Color", 242 | "r": 255, 243 | "g": 255, 244 | "b": 255, 245 | "a": 255 246 | }, 247 | "_spriteFrame": { 248 | "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", 249 | "__expectedType__": "cc.SpriteFrame" 250 | }, 251 | "_type": 0, 252 | "_fillType": 0, 253 | "_sizeMode": 0, 254 | "_fillCenter": { 255 | "__type__": "cc.Vec2", 256 | "x": 0, 257 | "y": 0 258 | }, 259 | "_fillStart": 0, 260 | "_fillRange": 0, 261 | "_isTrimmedMode": true, 262 | "_useGrayscale": false, 263 | "_atlas": null, 264 | "_id": "" 265 | }, 266 | { 267 | "__type__": "cc.CompPrefabInfo", 268 | "fileId": "1fUGaP5iFB4KSA96HpbO4K" 269 | }, 270 | { 271 | "__type__": "ea76cnArY1OnK4dcoZ5o6xQ", 272 | "_name": "", 273 | "_objFlags": 0, 274 | "node": { 275 | "__id__": 1 276 | }, 277 | "_enabled": true, 278 | "__prefab": { 279 | "__id__": 13 280 | }, 281 | "_id": "" 282 | }, 283 | { 284 | "__type__": "cc.CompPrefabInfo", 285 | "fileId": "8el9gw0d1C8qd34UPeJqfD" 286 | }, 287 | { 288 | "__type__": "cc.PrefabInfo", 289 | "root": { 290 | "__id__": 1 291 | }, 292 | "asset": { 293 | "__id__": 0 294 | }, 295 | "fileId": "5c7PMOhYtBgJu/tbhc7i46" 296 | } 297 | ] -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/ItemAutoSizeGrid.prefab: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "__type__": "cc.Prefab", 4 | "_name": "", 5 | "_objFlags": 0, 6 | "_native": "", 7 | "data": { 8 | "__id__": 1 9 | }, 10 | "optimizationPolicy": 0, 11 | "persistent": false, 12 | "asyncLoadAssets": false 13 | }, 14 | { 15 | "__type__": "cc.Node", 16 | "_name": "ItemAutoSizeGrid", 17 | "_objFlags": 0, 18 | "__editorExtras__": {}, 19 | "_parent": null, 20 | "_children": [ 21 | { 22 | "__id__": 2 23 | } 24 | ], 25 | "_active": true, 26 | "_components": [ 27 | { 28 | "__id__": 8 29 | }, 30 | { 31 | "__id__": 10 32 | }, 33 | { 34 | "__id__": 12 35 | } 36 | ], 37 | "_prefab": { 38 | "__id__": 14 39 | }, 40 | "_lpos": { 41 | "__type__": "cc.Vec3", 42 | "x": 0, 43 | "y": 0, 44 | "z": 0 45 | }, 46 | "_lrot": { 47 | "__type__": "cc.Quat", 48 | "x": 0, 49 | "y": 0, 50 | "z": 0, 51 | "w": 1 52 | }, 53 | "_lscale": { 54 | "__type__": "cc.Vec3", 55 | "x": 1, 56 | "y": 1, 57 | "z": 1 58 | }, 59 | "_layer": 33554432, 60 | "_euler": { 61 | "__type__": "cc.Vec3", 62 | "x": 0, 63 | "y": 0, 64 | "z": 0 65 | }, 66 | "_id": "" 67 | }, 68 | { 69 | "__type__": "cc.Node", 70 | "_name": "lblName", 71 | "_objFlags": 0, 72 | "_parent": { 73 | "__id__": 1 74 | }, 75 | "_children": [], 76 | "_active": true, 77 | "_components": [ 78 | { 79 | "__id__": 3 80 | }, 81 | { 82 | "__id__": 5 83 | } 84 | ], 85 | "_prefab": { 86 | "__id__": 7 87 | }, 88 | "_lpos": { 89 | "__type__": "cc.Vec3", 90 | "x": 0, 91 | "y": 0, 92 | "z": 0 93 | }, 94 | "_lrot": { 95 | "__type__": "cc.Quat", 96 | "x": 0, 97 | "y": 0, 98 | "z": 0, 99 | "w": 1 100 | }, 101 | "_lscale": { 102 | "__type__": "cc.Vec3", 103 | "x": 1, 104 | "y": 1, 105 | "z": 1 106 | }, 107 | "_layer": 33554432, 108 | "_euler": { 109 | "__type__": "cc.Vec3", 110 | "x": 0, 111 | "y": 0, 112 | "z": 0 113 | }, 114 | "_id": "" 115 | }, 116 | { 117 | "__type__": "cc.UITransform", 118 | "_name": "", 119 | "_objFlags": 0, 120 | "node": { 121 | "__id__": 2 122 | }, 123 | "_enabled": true, 124 | "__prefab": { 125 | "__id__": 4 126 | }, 127 | "_contentSize": { 128 | "__type__": "cc.Size", 129 | "width": 11.12, 130 | "height": 50.4 131 | }, 132 | "_anchorPoint": { 133 | "__type__": "cc.Vec2", 134 | "x": 0.5, 135 | "y": 0.5 136 | }, 137 | "_id": "" 138 | }, 139 | { 140 | "__type__": "cc.CompPrefabInfo", 141 | "fileId": "37f0LU8ulLEqd17XNI0n//" 142 | }, 143 | { 144 | "__type__": "cc.Label", 145 | "_name": "", 146 | "_objFlags": 0, 147 | "node": { 148 | "__id__": 2 149 | }, 150 | "_enabled": true, 151 | "__prefab": { 152 | "__id__": 6 153 | }, 154 | "_visFlags": 0, 155 | "_customMaterial": null, 156 | "_srcBlendFactor": 2, 157 | "_dstBlendFactor": 4, 158 | "_color": { 159 | "__type__": "cc.Color", 160 | "r": 252, 161 | "g": 0, 162 | "b": 0, 163 | "a": 255 164 | }, 165 | "_string": "1", 166 | "_horizontalAlign": 1, 167 | "_verticalAlign": 1, 168 | "_actualFontSize": 20, 169 | "_fontSize": 20, 170 | "_fontFamily": "Arial", 171 | "_lineHeight": 40, 172 | "_overflow": 0, 173 | "_enableWrapText": true, 174 | "_font": null, 175 | "_isSystemFontUsed": true, 176 | "_spacingX": 0, 177 | "_isItalic": false, 178 | "_isBold": false, 179 | "_isUnderline": false, 180 | "_underlineHeight": 2, 181 | "_cacheMode": 0, 182 | "_id": "" 183 | }, 184 | { 185 | "__type__": "cc.CompPrefabInfo", 186 | "fileId": "8fXvxKWW1IMIZDXofa5Ayk" 187 | }, 188 | { 189 | "__type__": "cc.PrefabInfo", 190 | "root": { 191 | "__id__": 1 192 | }, 193 | "asset": { 194 | "__id__": 0 195 | }, 196 | "fileId": "87pLYCIK9FyI7XtRQUCt/O" 197 | }, 198 | { 199 | "__type__": "cc.UITransform", 200 | "_name": "", 201 | "_objFlags": 0, 202 | "node": { 203 | "__id__": 1 204 | }, 205 | "_enabled": true, 206 | "__prefab": { 207 | "__id__": 9 208 | }, 209 | "_contentSize": { 210 | "__type__": "cc.Size", 211 | "width": 80, 212 | "height": 80 213 | }, 214 | "_anchorPoint": { 215 | "__type__": "cc.Vec2", 216 | "x": 0.5, 217 | "y": 0.5 218 | }, 219 | "_id": "" 220 | }, 221 | { 222 | "__type__": "cc.CompPrefabInfo", 223 | "fileId": "7a+k13z/dE3bLzgCTNLDOj" 224 | }, 225 | { 226 | "__type__": "cc.Sprite", 227 | "_name": "", 228 | "_objFlags": 0, 229 | "node": { 230 | "__id__": 1 231 | }, 232 | "_enabled": true, 233 | "__prefab": { 234 | "__id__": 11 235 | }, 236 | "_visFlags": 0, 237 | "_customMaterial": null, 238 | "_srcBlendFactor": 2, 239 | "_dstBlendFactor": 4, 240 | "_color": { 241 | "__type__": "cc.Color", 242 | "r": 255, 243 | "g": 255, 244 | "b": 255, 245 | "a": 255 246 | }, 247 | "_spriteFrame": { 248 | "__uuid__": "7d8f9b89-4fd1-4c9f-a3ab-38ec7cded7ca@f9941", 249 | "__expectedType__": "cc.SpriteFrame" 250 | }, 251 | "_type": 0, 252 | "_fillType": 0, 253 | "_sizeMode": 0, 254 | "_fillCenter": { 255 | "__type__": "cc.Vec2", 256 | "x": 0, 257 | "y": 0 258 | }, 259 | "_fillStart": 0, 260 | "_fillRange": 0, 261 | "_isTrimmedMode": true, 262 | "_useGrayscale": false, 263 | "_atlas": null, 264 | "_id": "" 265 | }, 266 | { 267 | "__type__": "cc.CompPrefabInfo", 268 | "fileId": "1fUGaP5iFB4KSA96HpbO4K" 269 | }, 270 | { 271 | "__type__": "b5f2cANfkREoqk7++VyFsqR", 272 | "_name": "", 273 | "_objFlags": 0, 274 | "node": { 275 | "__id__": 1 276 | }, 277 | "_enabled": true, 278 | "__prefab": { 279 | "__id__": 13 280 | }, 281 | "_id": "" 282 | }, 283 | { 284 | "__type__": "cc.CompPrefabInfo", 285 | "fileId": "caOQ7646xAMrWsG2nq8I+q" 286 | }, 287 | { 288 | "__type__": "cc.PrefabInfo", 289 | "root": { 290 | "__id__": 1 291 | }, 292 | "asset": { 293 | "__id__": 0 294 | }, 295 | "fileId": "5c7PMOhYtBgJu/tbhc7i46" 296 | } 297 | ] -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Prefab/Item.prefab: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "__type__": "cc.Prefab", 4 | "_name": "", 5 | "_objFlags": 0, 6 | "_native": "", 7 | "data": { 8 | "__id__": 1 9 | }, 10 | "optimizationPolicy": 0, 11 | "asyncLoadAssets": false, 12 | "readonly": false 13 | }, 14 | { 15 | "__type__": "cc.Node", 16 | "_name": "Item", 17 | "_objFlags": 0, 18 | "_parent": null, 19 | "_children": [ 20 | { 21 | "__id__": 2 22 | }, 23 | { 24 | "__id__": 5 25 | } 26 | ], 27 | "_active": true, 28 | "_components": [ 29 | { 30 | "__id__": 8 31 | } 32 | ], 33 | "_prefab": { 34 | "__id__": 9 35 | }, 36 | "_opacity": 255, 37 | "_color": { 38 | "__type__": "cc.Color", 39 | "r": 255, 40 | "g": 255, 41 | "b": 255, 42 | "a": 255 43 | }, 44 | "_contentSize": { 45 | "__type__": "cc.Size", 46 | "width": 100, 47 | "height": 100 48 | }, 49 | "_anchorPoint": { 50 | "__type__": "cc.Vec2", 51 | "x": 0.5, 52 | "y": 0.5 53 | }, 54 | "_trs": { 55 | "__type__": "TypedArray", 56 | "ctor": "Float64Array", 57 | "array": [ 58 | 0, 59 | 0, 60 | 0, 61 | 0, 62 | 0, 63 | 0, 64 | 1, 65 | 1, 66 | 1, 67 | 1 68 | ] 69 | }, 70 | "_eulerAngles": { 71 | "__type__": "cc.Vec3", 72 | "x": 0, 73 | "y": 0, 74 | "z": 0 75 | }, 76 | "_skewX": 0, 77 | "_skewY": 0, 78 | "_is3DNode": false, 79 | "_groupIndex": 0, 80 | "groupIndex": 0, 81 | "_id": "" 82 | }, 83 | { 84 | "__type__": "cc.Node", 85 | "_name": "bg", 86 | "_objFlags": 0, 87 | "_parent": { 88 | "__id__": 1 89 | }, 90 | "_children": [], 91 | "_active": true, 92 | "_components": [ 93 | { 94 | "__id__": 3 95 | } 96 | ], 97 | "_prefab": { 98 | "__id__": 4 99 | }, 100 | "_opacity": 255, 101 | "_color": { 102 | "__type__": "cc.Color", 103 | "r": 255, 104 | "g": 255, 105 | "b": 255, 106 | "a": 255 107 | }, 108 | "_contentSize": { 109 | "__type__": "cc.Size", 110 | "width": 100, 111 | "height": 100 112 | }, 113 | "_anchorPoint": { 114 | "__type__": "cc.Vec2", 115 | "x": 0.5, 116 | "y": 0.5 117 | }, 118 | "_trs": { 119 | "__type__": "TypedArray", 120 | "ctor": "Float64Array", 121 | "array": [ 122 | 0, 123 | 0, 124 | 0, 125 | 0, 126 | 0, 127 | 0, 128 | 1, 129 | 1, 130 | 1, 131 | 1 132 | ] 133 | }, 134 | "_eulerAngles": { 135 | "__type__": "cc.Vec3", 136 | "x": 0, 137 | "y": 0, 138 | "z": 0 139 | }, 140 | "_skewX": 0, 141 | "_skewY": 0, 142 | "_is3DNode": false, 143 | "_groupIndex": 0, 144 | "groupIndex": 0, 145 | "_id": "" 146 | }, 147 | { 148 | "__type__": "cc.Sprite", 149 | "_name": "", 150 | "_objFlags": 0, 151 | "node": { 152 | "__id__": 2 153 | }, 154 | "_enabled": true, 155 | "_materials": [ 156 | { 157 | "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" 158 | } 159 | ], 160 | "_srcBlendFactor": 770, 161 | "_dstBlendFactor": 771, 162 | "_spriteFrame": { 163 | "__uuid__": "410fb916-8721-4663-bab8-34397391ace7" 164 | }, 165 | "_type": 0, 166 | "_sizeMode": 0, 167 | "_fillType": 0, 168 | "_fillCenter": { 169 | "__type__": "cc.Vec2", 170 | "x": 0, 171 | "y": 0 172 | }, 173 | "_fillStart": 0, 174 | "_fillRange": 0, 175 | "_isTrimmedMode": true, 176 | "_atlas": null, 177 | "_id": "" 178 | }, 179 | { 180 | "__type__": "cc.PrefabInfo", 181 | "root": { 182 | "__id__": 1 183 | }, 184 | "asset": { 185 | "__uuid__": "4d3d40d7-266e-49b6-9c68-51c55ca32e2e" 186 | }, 187 | "fileId": "f0RcAFuTNFEbvkjzHJN3qt", 188 | "sync": false 189 | }, 190 | { 191 | "__type__": "cc.Node", 192 | "_name": "lblName", 193 | "_objFlags": 0, 194 | "_parent": { 195 | "__id__": 1 196 | }, 197 | "_children": [], 198 | "_active": true, 199 | "_components": [ 200 | { 201 | "__id__": 6 202 | } 203 | ], 204 | "_prefab": { 205 | "__id__": 7 206 | }, 207 | "_opacity": 255, 208 | "_color": { 209 | "__type__": "cc.Color", 210 | "r": 34, 211 | "g": 0, 212 | "b": 244, 213 | "a": 255 214 | }, 215 | "_contentSize": { 216 | "__type__": "cc.Size", 217 | "width": 97.87, 218 | "height": 50.4 219 | }, 220 | "_anchorPoint": { 221 | "__type__": "cc.Vec2", 222 | "x": 0.5, 223 | "y": 0.5 224 | }, 225 | "_trs": { 226 | "__type__": "TypedArray", 227 | "ctor": "Float64Array", 228 | "array": [ 229 | 0, 230 | 0, 231 | 0, 232 | 0, 233 | 0, 234 | 0, 235 | 1, 236 | 1, 237 | 1, 238 | 1 239 | ] 240 | }, 241 | "_eulerAngles": { 242 | "__type__": "cc.Vec3", 243 | "x": 0, 244 | "y": 0, 245 | "z": 0 246 | }, 247 | "_skewX": 0, 248 | "_skewY": 0, 249 | "_is3DNode": false, 250 | "_groupIndex": 0, 251 | "groupIndex": 0, 252 | "_id": "" 253 | }, 254 | { 255 | "__type__": "cc.Label", 256 | "_name": "", 257 | "_objFlags": 0, 258 | "node": { 259 | "__id__": 5 260 | }, 261 | "_enabled": true, 262 | "_materials": [ 263 | { 264 | "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" 265 | } 266 | ], 267 | "_srcBlendFactor": 770, 268 | "_dstBlendFactor": 771, 269 | "_string": "Label", 270 | "_N$string": "Label", 271 | "_fontSize": 40, 272 | "_lineHeight": 40, 273 | "_enableWrapText": true, 274 | "_N$file": null, 275 | "_isSystemFontUsed": true, 276 | "_spacingX": 0, 277 | "_batchAsBitmap": false, 278 | "_styleFlags": 0, 279 | "_underlineHeight": 0, 280 | "_N$horizontalAlign": 1, 281 | "_N$verticalAlign": 1, 282 | "_N$fontFamily": "Arial", 283 | "_N$overflow": 0, 284 | "_N$cacheMode": 0, 285 | "_id": "" 286 | }, 287 | { 288 | "__type__": "cc.PrefabInfo", 289 | "root": { 290 | "__id__": 1 291 | }, 292 | "asset": { 293 | "__uuid__": "4d3d40d7-266e-49b6-9c68-51c55ca32e2e" 294 | }, 295 | "fileId": "84Ad/2OUFKwI1qJzHKFsnb", 296 | "sync": false 297 | }, 298 | { 299 | "__type__": "e7675VmLmFAFpQJyJadfJ3G", 300 | "_name": "", 301 | "_objFlags": 0, 302 | "node": { 303 | "__id__": 1 304 | }, 305 | "_enabled": true, 306 | "isClick": true, 307 | "_id": "" 308 | }, 309 | { 310 | "__type__": "cc.PrefabInfo", 311 | "root": { 312 | "__id__": 1 313 | }, 314 | "asset": { 315 | "__uuid__": "4d3d40d7-266e-49b6-9c68-51c55ca32e2e" 316 | }, 317 | "fileId": "", 318 | "sync": false 319 | } 320 | ] -------------------------------------------------------------------------------- /core/3.2.0/AVirtualScrollView.ts: -------------------------------------------------------------------------------- 1 | import { _decorator, Component, Node, Prefab, Vec2, Layout, ScrollView, Rect, UITransform, instantiate, Vec3 } from 'cc'; 2 | import { Widget } from 'cc'; 3 | import { view } from 'cc'; 4 | import { NodeEventType } from 'cc'; 5 | import { Size } from 'cc'; 6 | import { CCBoolean } from 'cc'; 7 | import { isValid } from 'cc'; 8 | import { TransformBit } from 'cc'; 9 | import { AItemRenderer } from './AItemRenderer'; 10 | 11 | const { ccclass, property } = _decorator; 12 | 13 | /** 14 | * 虚拟滚动视图 扩展ScrollView 15 | * 渲染预制体必需挂载 AItemRenderer子类 16 | * @author slf 17 | */ 18 | @ccclass('AVirtualScrollView') 19 | export default class AVirtualScrollView extends ScrollView { 20 | /**渲染预制体必需挂载 ItemRenderer子类 */ 21 | @property({ type: Prefab, serializable: true, displayName: "渲染预制体" }) 22 | itemRenderer: Prefab = null; 23 | @property({ displayName: "启动虚拟列表" }) 24 | virtualList: boolean = true; 25 | /**开启滑动到底部 发送回调 */ 26 | @property({ tooltip: "无限滑动,到底后发送回调事件", visible() { return this.virtualList } }) 27 | infiniteScroll = false; 28 | @property({ tooltip: "子项自适应大小", visible() { return this.virtualList } }) 29 | autoChildrenSize = false; 30 | 31 | 32 | private infiniteScrollCb: Function; 33 | private infiniteScrollThis: any; 34 | 35 | /**子项 回调函数 回调作用域*/ 36 | protected callback: Function; 37 | protected cbThis: any; 38 | 39 | /**最大渲染预制体 垂直数量 */ 40 | private verticalCount: number; 41 | /**最大渲染预制体 水平数量 */ 42 | private horizontalCount: number; 43 | /**预制体默认宽 加上间隔 */ 44 | private itemW: number; 45 | /**预制体默认高 加上间隔*/ 46 | private itemH: number; 47 | /**定时器 */ 48 | private interval: any; 49 | /**预制体池 */ 50 | private itemPool: any[]; 51 | /**预制体列表 */ 52 | private itemList: Node[]; 53 | /**预制体渲染类列表 */ 54 | private itemRendererList: any[]; 55 | /**数据列表 */ 56 | private dataList: any[]; 57 | /**方向布局 */ 58 | private direction: Vec2 = new Vec2(); 59 | /**方向间隙 */ 60 | private padding: Vec2 = new Vec2(); 61 | /**开始坐标 */ 62 | private startPos: Vec2 = new Vec2(); 63 | /**布局*/ 64 | private contentLayout: Layout; 65 | /**强制刷新标志 定时更新数据*/ 66 | private forcedRefreshMark: boolean; 67 | /**刷新标志 定时更新数据 */ 68 | private refreshMark: boolean; 69 | /**是否移动到底部 无限滚动回调*/ 70 | private moveBottom: boolean; 71 | 72 | private _uiTransform: UITransform; 73 | 74 | private isInit: boolean; 75 | 76 | /**节点锚点 */ 77 | private anchorPoint: Vec2; 78 | /**位置对应节点大小 */ 79 | private posToSize: { [key: number]: Size } = {}; 80 | 81 | onLoad() { 82 | this.isInit = true; 83 | this.itemList = []; 84 | this.itemPool = []; 85 | this.itemRendererList = []; 86 | 87 | if (this.virtualList) { 88 | this.contentLayout = this.content.getComponent(Layout); 89 | this.contentLayout.enabled = false; 90 | this._uiTransform = this.node.getComponent(UITransform); 91 | this.resetSize(); 92 | this.node.on(NodeEventType.SIZE_CHANGED, this.onSelfSizeChange, this); 93 | 94 | if (this.autoChildrenSize && this.contentLayout.type == Layout.Type.GRID) { 95 | this.autoChildrenSize = false; 96 | console.error("子项自适应大小 暂不支持网格布局"); 97 | } 98 | } 99 | if (this.dataList) { 100 | this.refreshData(this.dataList); 101 | } 102 | } 103 | 104 | private onSelfSizeChange() { 105 | this.unschedule(this.delayRefresh); 106 | this.scheduleOnce(this.delayRefresh, 0.5); 107 | } 108 | 109 | private delayRefresh(): void { 110 | this.resetSize(); 111 | if (this.dataList != null) { 112 | this.refreshData(this.dataList); 113 | } 114 | } 115 | 116 | /**重置大小 */ 117 | public resetSize(): void { 118 | let widget = this.content.getComponent(Widget); 119 | if (widget) { 120 | widget.updateAlignment(); 121 | } else { 122 | widget = this.getComponent(Widget); 123 | widget && widget.updateAlignment(); 124 | } 125 | 126 | 127 | let nodeUITransform: UITransform = this.itemRenderer.data._uiProps.uiTransformComp; 128 | this.anchorPoint = nodeUITransform.anchorPoint.clone(); 129 | let nodeWidth = nodeUITransform.width; 130 | let nodeHeight = nodeUITransform.height; 131 | 132 | //自适应节点大小 133 | if (this.autoChildrenSize) { 134 | nodeWidth = this.posToSize[0]?.width ?? nodeUITransform.width; 135 | nodeHeight = this.posToSize[0]?.height ?? nodeUITransform.height; 136 | } 137 | //方向布局 138 | this.direction.x = this.contentLayout.horizontalDirection == Layout.HorizontalDirection.LEFT_TO_RIGHT ? 1 : -1; 139 | this.direction.y = this.contentLayout.verticalDirection == Layout.VerticalDirection.TOP_TO_BOTTOM ? -1 : 1; 140 | 141 | //上下左右间隙 142 | this.padding.x = (this.contentLayout.horizontalDirection == Layout.HorizontalDirection.LEFT_TO_RIGHT ? this.contentLayout.paddingLeft : this.contentLayout.paddingRight); 143 | this.padding.y = (this.contentLayout.verticalDirection == Layout.VerticalDirection.TOP_TO_BOTTOM ? this.contentLayout.paddingTop : this.contentLayout.paddingBottom); 144 | 145 | //第一个节点大小 计算起始位置 146 | this.startPos.x = (nodeWidth - nodeWidth * this.anchorPoint.x + this.padding.x) * this.direction.x; 147 | this.startPos.y = (nodeHeight - nodeHeight * this.anchorPoint.y + this.padding.y) * this.direction.y; 148 | 149 | 150 | //预制体宽高 151 | this.itemW = nodeUITransform.width + this.contentLayout.spacingX; 152 | this.itemH = nodeUITransform.height + this.contentLayout.spacingY; 153 | 154 | let hCount = (this._uiTransform.width + this.contentLayout.spacingX - this.contentLayout.paddingLeft) / this.itemW; 155 | let vCount = (this._uiTransform.height + this.contentLayout.spacingY - this.contentLayout.paddingTop) / this.itemH; 156 | 157 | //垂直、水平最大预制体数量 如果自适应子项大小 用默认节点大小计算最大渲染数量 158 | this.horizontalCount = Math.ceil(hCount) + 1; 159 | this.verticalCount = Math.ceil(vCount) + 1; 160 | 161 | if (this.contentLayout.type == Layout.Type.GRID) { 162 | if (this.contentLayout.startAxis == Layout.AxisDirection.HORIZONTAL) { 163 | this.horizontalCount = Math.floor(hCount); 164 | } else { 165 | this.verticalCount = Math.floor(vCount); 166 | } 167 | } 168 | } 169 | 170 | /**利用ScrollView本身方法 来标记滑动中 */ 171 | _setContentPosition(position: Vec3) { 172 | super['_setContentPosition'](position); 173 | this.refreshMark = true; 174 | } 175 | 176 | /** 177 | * 设置列表 子项回调 178 | * 回调会携带当前子项的 data 和 透传参数 179 | * @param cb 回调 180 | * @param cbT 作用域 181 | */ 182 | public setItemCallback(cb: (data: any, thisarg?: any) => void, cbT: any): void { 183 | this.callback = cb; 184 | this.cbThis = cbT; 185 | } 186 | 187 | /**子项回调 */ 188 | private onItemCallback(data: any): void { 189 | this.callback && this.callback.call(this.cbThis, data); 190 | } 191 | 192 | /** 193 | * 设置列表 无限滚动到底部后 回调 194 | * @param cb 回调 195 | * @param cbT 作用域 196 | */ 197 | public setInfiniteScrollCallback(cb: () => void, cbT: any): void { 198 | this.infiniteScrollCb = cb; 199 | this.infiniteScrollThis = cbT; 200 | } 201 | 202 | /**无限滚动到底部后 回调 */ 203 | private onInfiniteScrollCallback(): void { 204 | this.moveBottom = false; 205 | if (this.infiniteScrollCb) { 206 | console.log("发送回调"); 207 | this.infiniteScrollCb.call(this.infiniteScrollThis); 208 | } 209 | } 210 | 211 | /** 212 | * 刷新数据 213 | * @param data 数据源 单项|队列 214 | */ 215 | public refreshData(data: any | any[]): void { 216 | if (Array.isArray(data)) { 217 | this.dataList = data; 218 | } else { 219 | this.dataList = [data]; 220 | } 221 | 222 | if (!this.isInit) { 223 | return; 224 | } 225 | 226 | if (this.interval) { 227 | clearInterval(this.interval); 228 | this.interval = null; 229 | } 230 | this.addItem(); 231 | 232 | if (this.virtualList) { 233 | // this.refreshContentSize(); 234 | this.forcedRefreshMark = true; 235 | this.refreshMark = true; 236 | this.interval = setInterval(this.refreshItem.bind(this), 1000 / 10); 237 | this.refreshItem(); 238 | } 239 | } 240 | 241 | /**添加预制体 */ 242 | private addItem(): void { 243 | let len: number = 0; 244 | if (this.virtualList) { 245 | switch (this.contentLayout.type) { 246 | case Layout.Type.HORIZONTAL: 247 | len = this.horizontalCount; 248 | break; 249 | case Layout.Type.VERTICAL: 250 | len = this.verticalCount; 251 | break; 252 | case Layout.Type.GRID: 253 | len = this.horizontalCount * this.verticalCount; 254 | break; 255 | } 256 | len = Math.min(len, this.dataList.length); 257 | } else { 258 | len = this.dataList.length; 259 | } 260 | 261 | let itemListLen = this.itemList.length; 262 | if (itemListLen < len) { 263 | let itemRenderer: AItemRenderer = null; 264 | for (var i = itemListLen; i < len; i++) { 265 | let child = this.itemPool.length > 0 ? this.itemPool.shift() : instantiate(this.itemRenderer); 266 | this.content.addChild(child); 267 | this.itemList.push(child); 268 | itemRenderer = child.getComponent(AItemRenderer); 269 | this.itemRendererList.push(itemRenderer); 270 | itemRenderer.registerCallback(this.onItemCallback, this); 271 | if (this.autoChildrenSize) { 272 | child.on(NodeEventType.SIZE_CHANGED, this.delayChangeRefreshMark.bind(this, child), this); 273 | } 274 | } 275 | } else { 276 | let cL: number = this.content.children.length; 277 | let item; 278 | while (cL > len) { 279 | item = this.itemList[cL - 1]; 280 | this.content.removeChild(item); 281 | this.itemList.splice(cL - 1, 1); 282 | this.itemRendererList.splice(cL - 1, 1); 283 | this.itemPool.push(item); 284 | cL = this.content.children.length; 285 | } 286 | } 287 | 288 | if (!this.virtualList) { 289 | this.dataList.forEach((v, idx) => { 290 | this.itemRendererList[idx].data = v; 291 | }); 292 | } 293 | } 294 | 295 | /**延迟一帧标记刷新列表数据 */ 296 | private delayChangeRefreshMark(node: Node) { 297 | let pos = this.posToSize[node['nowDataIdx']]; 298 | //记录数据源索引Node改变后的大小 299 | if (pos) { 300 | if (pos.x == node._uiProps.uiTransformComp.contentSize.x && pos.y == node._uiProps.uiTransformComp.contentSize.y) { 301 | return; 302 | } 303 | pos.x = node._uiProps.uiTransformComp.contentSize.x; 304 | pos.y = node._uiProps.uiTransformComp.contentSize.y; 305 | } else { 306 | pos = node._uiProps.uiTransformComp.contentSize.clone(); 307 | } 308 | console.log("预刷新子项状态===" + node['nowDataIdx']); 309 | this.posToSize[node['nowDataIdx']] = pos; 310 | //延迟一帧刷新 311 | // this.unschedule(this.changeRefreshMark); 312 | // this.scheduleOnce(this.changeRefreshMark, 0); 313 | 314 | //立即刷新 315 | this.refreshMark = true; 316 | this.refreshItem(); 317 | } 318 | 319 | /**标记刷新列表等定时刷新数据 */ 320 | private changeRefreshMark(): void { 321 | // console.log("刷新子项状态"); 322 | this.refreshMark = true; 323 | } 324 | 325 | /**根据数据数量 改变content宽高 */ 326 | private refreshContentSize(): void { 327 | let layout: Layout = this.contentLayout; 328 | let dataListLen: number = this.dataList.length; 329 | let value: number; 330 | switch (this.contentLayout.type) { 331 | case Layout.Type.HORIZONTAL: 332 | value = layout.paddingLeft + layout.paddingRight; 333 | if (this.autoChildrenSize) { 334 | for (let i: number = 0; i < dataListLen; i++) { 335 | value += (this.posToSize[i]?.width ?? this.itemW - this.contentLayout.spacingX) + this.contentLayout.spacingX; 336 | } 337 | } else { 338 | value += dataListLen * this.itemW; 339 | } 340 | 341 | //排列方向从右到左排序的话,scrollview底层会计算content的位置,导致位置不对,content的宽度最小值改为父容器的大小 342 | if (this.contentLayout.horizontalDirection == Layout.HorizontalDirection.RIGHT_TO_LEFT) { 343 | value = Math.max(value, this.content.parent?._uiProps.uiTransformComp.width ?? 0) 344 | } 345 | 346 | this.content._uiProps.uiTransformComp.width = value; 347 | break; 348 | case Layout.Type.VERTICAL: 349 | value = layout.paddingTop + layout.paddingBottom; 350 | if (this.autoChildrenSize) { 351 | for (let i: number = 0; i < dataListLen; i++) { 352 | value += (this.posToSize[i]?.height ?? this.itemH - this.contentLayout.spacingY) + this.contentLayout.spacingY; 353 | } 354 | } else { 355 | value += dataListLen * this.itemH; 356 | } 357 | 358 | //排列方向从下到上排序的话,scrollview底层会计算content的位置,导致位置不对,content的高度最小值改为父容器的大小 359 | if (this.contentLayout.verticalDirection == Layout.VerticalDirection.BOTTOM_TO_TOP) { 360 | value = Math.max(value, this.content.parent?._uiProps.uiTransformComp.height ?? 0) 361 | } 362 | 363 | this.content._uiProps.uiTransformComp.height = value; 364 | break; 365 | case Layout.Type.GRID: 366 | if (this.contentLayout.startAxis == Layout.AxisDirection.HORIZONTAL) { 367 | this.content.getComponent(UITransform).height = layout.paddingTop + Math.ceil(dataListLen / this.horizontalCount) * this.itemH + layout.paddingBottom; 368 | } else if (this.contentLayout.startAxis == Layout.AxisDirection.VERTICAL) { 369 | this.content.getComponent(UITransform).width = layout.paddingLeft + Math.ceil(dataListLen / this.verticalCount) * this.itemW + layout.paddingRight; 370 | } 371 | break; 372 | } 373 | } 374 | 375 | /**刷新预制体位置 和 数据填充 */ 376 | private refreshItem(): void { 377 | this.moveBottom && this.onInfiniteScrollCallback(); 378 | if (!this.refreshMark) { 379 | return; 380 | } 381 | switch (this.contentLayout.type) { 382 | case Layout.Type.HORIZONTAL: 383 | this.refreshHorizontal(); 384 | break; 385 | case Layout.Type.VERTICAL: 386 | this.refreshVertical(); 387 | break; 388 | case Layout.Type.GRID: 389 | this.refreshGrid(); 390 | break; 391 | } 392 | this.refreshContentSize(); 393 | this.refreshMark = false; 394 | this.forcedRefreshMark = false; 395 | } 396 | 397 | /**刷新水平 */ 398 | private refreshHorizontal() { 399 | let start = this.getStart(); 400 | let end = start + this.horizontalCount; 401 | if (end > this.dataList.length) {//超出边界处理 402 | end = this.dataList.length; 403 | start = Math.max(end - this.horizontalCount, 0); 404 | } 405 | 406 | let tempV = 0; 407 | let itemListLen = this.itemList.length; 408 | let item: Node, pos: Vec3, idx; 409 | for (var i = 0; i < itemListLen; i++) { 410 | idx = (start + i) % itemListLen; 411 | item = this.itemList[idx]; 412 | 413 | 414 | pos = item.getPosition(); 415 | tempV = this.getPos(start + i);; 416 | if (pos.x != tempV || this.forcedRefreshMark) { 417 | console.log("修改的数据=" + (start + i)) 418 | pos.x = tempV; 419 | item.position = pos; 420 | this.itemRendererList[idx].node.nowDataIdx = start + i; 421 | this.itemRendererList[idx].data = this.dataList[start + i]; 422 | 423 | //记录位置和node大小 424 | const trans = item._uiProps.uiTransformComp; 425 | this.posToSize[start + i] = trans.contentSize.clone(); 426 | 427 | if (this.infiniteScroll && start > 0 && start + i == this.dataList.length - 1) { 428 | this.moveBottom = true; 429 | } 430 | } 431 | } 432 | } 433 | 434 | /**刷新垂直 */ 435 | private refreshVertical(): void { 436 | let start = this.getStart(); 437 | 438 | let end = start + this.verticalCount; 439 | if (end > this.dataList.length) { 440 | end = this.dataList.length; 441 | start = Math.max(end - this.verticalCount, 0); 442 | } 443 | 444 | let tempV = 0; 445 | let itemListLen = this.itemList.length; 446 | let item: Node, pos: Vec3, idx; 447 | for (var i = 0; i < itemListLen; i++) { 448 | idx = (start + i) % itemListLen; 449 | item = this.itemList[idx]; 450 | pos = item.getPosition(); 451 | tempV = this.getPos(start + i); 452 | if (pos.y != tempV || this.forcedRefreshMark) { 453 | console.log("修改的数据=" + (start + i)); 454 | pos.y = tempV; 455 | item.position = pos; 456 | this.itemRendererList[idx].node.nowDataIdx = start + i; 457 | this.itemRendererList[idx].data = this.dataList[start + i]; 458 | 459 | //记录位置和node大小 460 | const trans = item._uiProps.uiTransformComp; 461 | this.posToSize[start + i] = trans.contentSize.clone(); 462 | if (this.infiniteScroll && start > 0 && start + i == this.dataList.length - 1) { 463 | this.moveBottom = true; 464 | } 465 | } 466 | } 467 | } 468 | 469 | /**刷新网格 */ 470 | private refreshGrid(): void { 471 | //是否垂直方向 添加网格 472 | let isVDirection = this.contentLayout.startAxis == Layout.AxisDirection.VERTICAL; 473 | let start = Math.floor(Math.abs(this.content.position.y) / this.itemH) * this.horizontalCount; 474 | if (isVDirection) { 475 | start = Math.floor(Math.abs(this.content.position.x) / this.itemW) * this.verticalCount; 476 | if (this.content.position.x > 0) { 477 | start = 0; 478 | } 479 | } else if (this.content.position.y < 0) { 480 | start = 0; 481 | } 482 | 483 | if (start < 0) { 484 | start = 0; 485 | } 486 | 487 | let end = start + this.horizontalCount * this.verticalCount; 488 | if (end > this.dataList.length) { 489 | end = this.dataList.length; 490 | start = Math.max(end - this.horizontalCount * this.verticalCount, 0); 491 | } 492 | 493 | let tempX = 0; 494 | let tempY = 0; 495 | let itemListLen = this.itemList.length; 496 | let item: Node, pos: Vec3, idx; 497 | for (var i = 0; i < itemListLen; i++) { 498 | idx = (start + i) % itemListLen; 499 | item = this.itemList[idx]; 500 | pos = item.getPosition(); 501 | if (isVDirection) { 502 | tempX = this.startPos.x + this.direction.x * (Math.floor((start + i) / this.verticalCount)) * this.itemW; 503 | tempY = this.startPos.y + this.direction.y * ((start + i) % this.verticalCount) * this.itemH; 504 | } else { 505 | tempX = this.startPos.x + this.direction.x * ((start + i) % this.horizontalCount) * this.itemW; 506 | tempY = this.startPos.y + this.direction.y * (Math.floor((start + i) / this.horizontalCount)) * this.itemH; 507 | } 508 | 509 | if (pos.y != tempY || pos.x != tempX || this.forcedRefreshMark) { 510 | console.log("修改的数据=" + (start + i)) 511 | pos.x = tempX; 512 | pos.y = tempY; 513 | item.position = pos; 514 | this.itemRendererList[idx].data = this.dataList[start + i]; 515 | if (this.infiniteScroll && start > 0 && start + i == this.dataList.length - 1) { 516 | this.moveBottom = true; 517 | } 518 | } 519 | } 520 | } 521 | 522 | 523 | /**获取开始索引 */ 524 | private getStart(): number { 525 | let start: number = 0; 526 | /**节点高度 */ 527 | let value: number = 0; 528 | switch (this.contentLayout.type) { 529 | case Layout.Type.HORIZONTAL: 530 | if (this.autoChildrenSize) { 531 | value = Math.abs(this.content.position.x); 532 | for (let item in this.posToSize) { 533 | value -= (this.posToSize[item]?.width ?? (this.itemW - this.contentLayout.spacingX)) + this.contentLayout.spacingX; 534 | if (value <= 0) { 535 | break 536 | } else { 537 | start++; 538 | } 539 | } 540 | } else { 541 | start = Math.floor(Math.abs(this.content.position.x) / this.itemW); 542 | } 543 | 544 | //超出边界处理 545 | if (this.contentLayout.horizontalDirection == Layout.HorizontalDirection.LEFT_TO_RIGHT && this.content.position.x > 0 || this.contentLayout.horizontalDirection == Layout.HorizontalDirection.RIGHT_TO_LEFT && this.content.position.x < 0) { 546 | start = 0; 547 | } 548 | break; 549 | case Layout.Type.VERTICAL: 550 | if (this.autoChildrenSize) { 551 | value = Math.abs(this.content.position.y); 552 | for (let item in this.posToSize) { 553 | value -= (this.posToSize[item]?.height ?? (this.itemH - this.contentLayout.spacingY)) + this.contentLayout.spacingY; 554 | if (value <= 0) { 555 | break 556 | } else { 557 | start++; 558 | } 559 | } 560 | } else { 561 | start = Math.floor(Math.abs(this.content.position.y) / this.itemH); 562 | } 563 | 564 | //超出边界处理 565 | if (this.contentLayout.verticalDirection == Layout.VerticalDirection.TOP_TO_BOTTOM && this.content.position.y < 0 || this.contentLayout.verticalDirection == Layout.VerticalDirection.BOTTOM_TO_TOP && this.content.position.y > 0) { 566 | start = 0; 567 | } 568 | break; 569 | case Layout.Type.GRID: 570 | console.error("暂不支持网格布局"); 571 | break; 572 | } 573 | if (start < 0) { 574 | start = 0; 575 | } 576 | return start; 577 | } 578 | 579 | /**根据索引获取位置 */ 580 | private getPos(idx: number): number { 581 | let position: number = 0; 582 | let h: number, w: number; 583 | switch (this.contentLayout.type) { 584 | case Layout.Type.HORIZONTAL: 585 | if (this.autoChildrenSize) { 586 | //重置开始位置 587 | if (idx == 0) { 588 | w = (this.posToSize[0]?.width ?? (this.itemH - this.contentLayout.spacingX)); 589 | this.startPos.x = w * this.anchorPoint.x * this.direction.x; 590 | } 591 | position += this.startPos.x + this.padding.x * this.direction.x; 592 | for (let i: number = 1; i <= idx; i++) { 593 | w = (this.posToSize[i - 1]?.width ?? (this.itemW - this.contentLayout.spacingX)); 594 | position += (w - w * this.anchorPoint.x + this.contentLayout.spacingX) * this.direction.x; 595 | w = (this.posToSize[i]?.width ?? (this.itemW - this.contentLayout.spacingX)) 596 | position += (w * this.anchorPoint.x) * this.direction.x; 597 | } 598 | } else { 599 | position = this.startPos.x + idx * this.itemW * this.direction.x; 600 | } 601 | break; 602 | case Layout.Type.VERTICAL: 603 | if (this.autoChildrenSize) { 604 | //重置开始位置 605 | if (idx == 0) { 606 | h = (this.posToSize[0]?.height ?? (this.itemH - this.contentLayout.spacingY)) 607 | this.startPos.y = (h - h * this.anchorPoint.y) * this.direction.y; 608 | } 609 | position += this.startPos.y + this.padding.y * this.direction.y; 610 | for (let i: number = 1; i <= idx; i++) { 611 | h = (this.posToSize[i - 1]?.height ?? (this.itemH - this.contentLayout.spacingY)) * this.anchorPoint.y + this.contentLayout.spacingY; 612 | position += h * this.direction.y; 613 | h = (this.posToSize[i]?.height ?? (this.itemH - this.contentLayout.spacingY)) 614 | position += (h - h * this.anchorPoint.y) * this.direction.y; 615 | } 616 | } else { 617 | position = this.startPos.y + idx * this.itemH * this.direction.y; 618 | } 619 | break; 620 | case Layout.Type.GRID: 621 | console.error("暂不支持网格布局"); 622 | break; 623 | } 624 | return position; 625 | } 626 | 627 | protected onDestroy(): void { 628 | this.dataList = null; 629 | this.itemList = null; 630 | this.itemRendererList = null; 631 | this.posToSize = null; 632 | this.itemPool.forEach(item => { 633 | item.destroy(); 634 | }) 635 | this.itemPool = null; 636 | if (this.interval) { 637 | clearInterval(this.interval); 638 | } 639 | this.node.targetOff(this); 640 | } 641 | } 642 | -------------------------------------------------------------------------------- /core/2.4.6/AVirtualScrollView.ts: -------------------------------------------------------------------------------- 1 | import AItemRenderer from "./AItemRenerer"; 2 | 3 | const { ccclass, property } = cc._decorator; 4 | 5 | /** 6 | * 虚拟滚动视图 扩展cc.ScrollView 7 | * 渲染预制体必需挂载 AItemRenderer子类 8 | * @author slf 9 | */ 10 | @ccclass 11 | export default class AVirtualScrollView extends cc.ScrollView { 12 | /**渲染预制体必需挂载 AItemRenderer子类 */ 13 | @property({ type: cc.Prefab, serializable: true, displayName: "渲染预制体" }) 14 | itemRenderer: cc.Prefab = null; 15 | @property({ displayName: "启动虚拟列表" }) 16 | virtualList: boolean = true; 17 | /**开启滑动到底部 发送回调 */ 18 | @property({ tooltip: "无限滑动,到底后发送回调事件", visible() { return this.virtualList } }) 19 | infiniteScroll = false; 20 | @property({ tooltip: "子项自适应大小", visible() { return this.virtualList } }) 21 | autoChildrenSize = false; 22 | 23 | private infiniteScrollCb: Function; 24 | private infiniteScrollThis: any; 25 | 26 | /**子项点击 回调函数 回调作用域*/ 27 | protected callback: Function; 28 | protected cbThis: any; 29 | 30 | /**最大渲染预制体 垂直数量 */ 31 | private verticalCount: number; 32 | /**最大渲染预制体 水平数量 */ 33 | private horizontalCount: number; 34 | /**预制体宽高 */ 35 | private itemW: number; 36 | private itemH: number; 37 | /**定时器 */ 38 | private interval: number; 39 | /**预制体池 */ 40 | private itemPool: any[]; 41 | /**预制体列表 */ 42 | private itemList: cc.Node[]; 43 | /**预制体渲染类列表 */ 44 | private itemRendererList: any[]; 45 | /**数据列表 */ 46 | private dataList: any[]; 47 | /**方向布局 */ 48 | private direction: cc.Vec2 = new cc.Vec2(); 49 | /**方向间隙 */ 50 | private padding: cc.Vec2 = new cc.Vec2(); 51 | /**开始坐标 */ 52 | private startPos: cc.Vec2 = new cc.Vec2(); 53 | /**布局*/ 54 | private contentLayout: cc.Layout; 55 | /**是否移动到底部 无限滚动回调*/ 56 | private moveBottom: boolean; 57 | 58 | private _uiTransform: cc.Node; 59 | 60 | private isInit: boolean; 61 | 62 | /**强制刷新 */ 63 | private forcedRefresh: boolean; 64 | /**刷新 */ 65 | private refresh: boolean; 66 | 67 | /**节点锚点 */ 68 | private anchorPoint: cc.Vec2; 69 | /**位置对应节点大小 */ 70 | private posToSize: { [key: number]: cc.Size } = {}; 71 | 72 | protected onLoad(): void { 73 | this.isInit = true; 74 | this.itemList = []; 75 | this.itemPool = []; 76 | this.itemRendererList = []; 77 | 78 | if (this.virtualList) { 79 | this.contentLayout = this.content.getComponent(cc.Layout); 80 | this.contentLayout.enabled = false; 81 | this._uiTransform = this.node; 82 | this.resetSize(); 83 | this.node.on(cc.Node.EventType.SIZE_CHANGED, this.onSelfSizeChange, this); 84 | 85 | if (this.autoChildrenSize && this.contentLayout.type == cc.Layout.Type.GRID) { 86 | this.autoChildrenSize = false; 87 | console.error("子项自适应大小 暂不支持网格布局"); 88 | } 89 | 90 | // //起始位置 91 | // let itemNode: cc.Node = this.itemRenderer.data; 92 | // this.startPos = new cc.Vec2(itemNode.width * itemNode.anchorX + this.contentLayout.paddingLeft, -(itemNode.height * itemNode.anchorY + this.contentLayout.paddingTop)); 93 | // //预制体宽高 94 | // this.itemW = itemNode.width + this.contentLayout.spacingX; 95 | // this.itemH = itemNode.height + this.contentLayout.spacingY; 96 | // //垂直、水平最大预制体数量 97 | // this.horizontalCount = Math.ceil(this.node.width / this.itemW) + 1; 98 | // this.verticalCount = Math.ceil(this.node.height / this.itemH) + 1; 99 | 100 | // if (this.contentLayout.type == cc.Layout.Type.GRID) { 101 | // if (this.contentLayout.startAxis == cc.Layout.AxisDirection.HORIZONTAL) { 102 | // this.horizontalCount = Math.floor(this.node.width / this.itemW); 103 | // } else { 104 | // this.verticalCount = Math.floor(this.node.height / this.itemH); 105 | // } 106 | // } 107 | } 108 | 109 | if (this.dataList) { 110 | this.refreshData(this.dataList); 111 | } 112 | } 113 | 114 | private onSelfSizeChange() { 115 | this.unschedule(this.delayRefresh); 116 | this.scheduleOnce(this.delayRefresh, 0.5); 117 | } 118 | 119 | private delayRefresh(): void { 120 | this.resetSize(); 121 | if (this.dataList != null) { 122 | this.refreshData(this.dataList); 123 | } 124 | } 125 | 126 | /**重置大小 */ 127 | public resetSize(): void { 128 | let widget = this.content.getComponent(cc.Widget); 129 | if (widget) { 130 | widget.updateAlignment(); 131 | } else { 132 | widget = this.getComponent(cc.Widget); 133 | widget && widget.updateAlignment(); 134 | } 135 | 136 | 137 | let nodeUITransform: cc.Node = this.itemRenderer.data; 138 | this.anchorPoint = nodeUITransform.getAnchorPoint().clone(); 139 | let nodeWidth = nodeUITransform.width; 140 | let nodeHeight = nodeUITransform.height; 141 | 142 | //自适应节点大小 143 | if (this.autoChildrenSize) { 144 | nodeWidth = this.posToSize[0]?.width ?? nodeUITransform.width; 145 | nodeHeight = this.posToSize[0]?.height ?? nodeUITransform.height; 146 | } 147 | //方向布局 148 | this.direction.x = this.contentLayout.horizontalDirection == cc.Layout.HorizontalDirection.LEFT_TO_RIGHT ? 1 : -1; 149 | this.direction.y = this.contentLayout.verticalDirection == cc.Layout.VerticalDirection.TOP_TO_BOTTOM ? -1 : 1; 150 | 151 | //上下左右间隙 152 | this.padding.x = (this.contentLayout.horizontalDirection == cc.Layout.HorizontalDirection.LEFT_TO_RIGHT ? this.contentLayout.paddingLeft : this.contentLayout.paddingRight); 153 | this.padding.y = (this.contentLayout.verticalDirection == cc.Layout.VerticalDirection.TOP_TO_BOTTOM ? this.contentLayout.paddingTop : this.contentLayout.paddingBottom); 154 | 155 | //第一个节点大小 计算起始位置 156 | this.startPos.x = (nodeWidth - nodeWidth * this.anchorPoint.x + this.padding.x) * this.direction.x; 157 | this.startPos.y = (nodeHeight - nodeHeight * this.anchorPoint.y + this.padding.y) * this.direction.y; 158 | 159 | 160 | //预制体宽高 161 | this.itemW = nodeUITransform.width + this.contentLayout.spacingX; 162 | this.itemH = nodeUITransform.height + this.contentLayout.spacingY; 163 | 164 | let hCount = (this._uiTransform.width + this.contentLayout.spacingX - this.contentLayout.paddingLeft) / this.itemW; 165 | let vCount = (this._uiTransform.height + this.contentLayout.spacingY - this.contentLayout.paddingTop) / this.itemH; 166 | 167 | //垂直、水平最大预制体数量 如果自适应子项大小 用默认节点大小计算最大渲染数量 168 | this.horizontalCount = Math.ceil(hCount) + 1; 169 | this.verticalCount = Math.ceil(vCount) + 1; 170 | 171 | if (this.contentLayout.type == cc.Layout.Type.GRID) { 172 | if (this.contentLayout.startAxis == cc.Layout.AxisDirection.HORIZONTAL) { 173 | this.horizontalCount = Math.floor(hCount); 174 | } else { 175 | this.verticalCount = Math.floor(vCount); 176 | } 177 | } 178 | } 179 | 180 | protected onDestroy(): void { 181 | this.dataList = null; 182 | this.itemList = null; 183 | this.itemRendererList = null; 184 | this.posToSize = null; 185 | this.itemPool.forEach(item => { 186 | item.destroy(); 187 | }) 188 | this.itemPool = null; 189 | if (this.interval) { 190 | clearInterval(this.interval); 191 | } 192 | this.node.targetOff(this); 193 | } 194 | 195 | /**利用cc.ScrollView本身方法 来标记滑动中 */ 196 | setContentPosition(position: cc.Vec2) { 197 | super.setContentPosition(position); 198 | this.refresh = true; 199 | } 200 | 201 | /** 202 | * 设置列表 子项点击回调 203 | * 回调会携带当前子项的 data 204 | * @param cb 回调 205 | * @param cbT 作用域 206 | */ 207 | public setTouchItemCallback(cb: Function, cbT: any): void { 208 | this.callback = cb; 209 | this.cbThis = cbT; 210 | } 211 | 212 | /**选中数据 */ 213 | private onItemTap(data: any): void { 214 | this.callback && this.callback.call(this.cbThis, data); 215 | } 216 | 217 | /** 218 | * 设置列表 无限滚动到底部后 回调 219 | * @param cb 回调 220 | * @param cbT 作用域 221 | */ 222 | public setInfiniteScrollCallback(cb: () => void, cbT: any): void { 223 | this.infiniteScrollCb = cb; 224 | this.infiniteScrollThis = cbT; 225 | } 226 | 227 | /**无限滚动到底部后 回调 */ 228 | private onInfiniteScrollCallback(): void { 229 | this.moveBottom = false; 230 | if (this.infiniteScrollCb) { 231 | console.log("发送回调"); 232 | this.infiniteScrollCb.call(this.infiniteScrollThis); 233 | } 234 | } 235 | 236 | /** 237 | * 刷新数据 238 | * @param data 数据源 单项|队列 239 | */ 240 | public refreshData(data: any | any[]): void { 241 | if (Array.isArray(data)) { 242 | this.dataList = data; 243 | } else { 244 | this.dataList = [data]; 245 | } 246 | 247 | if (!this.isInit) { 248 | return; 249 | } 250 | 251 | if (this.interval) { 252 | clearInterval(this.interval); 253 | } 254 | this.addItem(); 255 | 256 | if (this.virtualList) { 257 | this.refreshContentSize(); 258 | this.forcedRefresh = true; 259 | this.refresh = true; 260 | this.interval = setInterval(this.refreshItem.bind(this), 1000 / 10); 261 | this.refreshItem(); 262 | } 263 | 264 | } 265 | 266 | 267 | /**添加预制体 */ 268 | private addItem(): void { 269 | let len: number = 0; 270 | if (this.virtualList) { 271 | switch (this.contentLayout.type) { 272 | case cc.Layout.Type.HORIZONTAL: 273 | len = this.horizontalCount; 274 | break; 275 | case cc.Layout.Type.VERTICAL: 276 | len = this.verticalCount; 277 | break; 278 | case cc.Layout.Type.GRID: 279 | len = this.horizontalCount * this.verticalCount; 280 | break; 281 | } 282 | len = Math.min(len, this.dataList.length); 283 | } else { 284 | len = this.dataList.length; 285 | } 286 | 287 | let itemListLen = this.itemList.length; 288 | if (itemListLen < len) { 289 | let itemRenderer: AItemRenderer = null; 290 | for (var i = itemListLen; i < len; i++) { 291 | let child = this.itemPool.length > 0 ? this.itemPool.shift() : cc.instantiate(this.itemRenderer); 292 | this.content.addChild(child); 293 | this.itemList.push(child); 294 | itemRenderer = child.getComponent(AItemRenderer); 295 | this.itemRendererList.push(itemRenderer); 296 | 297 | if (itemRenderer.isClick) { 298 | itemRenderer.setTouchCallback(this.onItemTap, this); 299 | } 300 | if (this.autoChildrenSize) { 301 | child.on(cc.Node.EventType.SIZE_CHANGED, this.delayChangeRefreshMark.bind(this, child), this); 302 | } 303 | } 304 | } else { 305 | let cL: number = this.content.childrenCount; 306 | let item; 307 | while (cL > len) { 308 | item = this.itemList[cL - 1]; 309 | this.content.removeChild(item); 310 | this.itemList.splice(cL - 1, 1); 311 | this.itemRendererList.splice(cL - 1, 1); 312 | this.itemPool.push(item); 313 | cL = this.content.childrenCount; 314 | } 315 | } 316 | 317 | if (!this.virtualList) { 318 | this.dataList.forEach((v, idx) => { 319 | this.itemRendererList[idx].data = v; 320 | }); 321 | } 322 | } 323 | 324 | /**延迟一帧标记刷新列表数据 */ 325 | private delayChangeRefreshMark(node: cc.Node) { 326 | let pos = this.posToSize[node['nowDataIdx']]; 327 | //记录数据源索引Node改变后的大小 328 | if (pos) { 329 | if (pos.width == node.width && pos.height == node.height) { 330 | return; 331 | } 332 | pos.width = node.width; 333 | pos.height = node.height; 334 | } else { 335 | pos = node.getContentSize().clone(); 336 | } 337 | console.log("预刷新子项状态===" + node['nowDataIdx']); 338 | this.posToSize[node['nowDataIdx']] = pos; 339 | //延迟一帧刷新 340 | // this.unschedule(this.changeRefreshMark); 341 | // this.scheduleOnce(this.changeRefreshMark, 0); 342 | 343 | //立即刷新 344 | this.refresh = true; 345 | this.refreshItem(); 346 | } 347 | 348 | /**标记刷新列表等定时刷新数据 */ 349 | private changeRefreshMark(): void { 350 | // console.log("刷新子项状态"); 351 | this.refresh = true; 352 | } 353 | 354 | /**根据数据数量 改变content宽高 */ 355 | private refreshContentSize(): void { 356 | let layout: cc.Layout = this.contentLayout; 357 | let dataListLen: number = this.dataList.length; 358 | let value: number; 359 | switch (this.contentLayout.type) { 360 | case cc.Layout.Type.VERTICAL: 361 | value = layout.paddingTop + layout.paddingBottom; 362 | if (this.autoChildrenSize) { 363 | for (let i: number = 0; i < dataListLen; i++) { 364 | value += (this.posToSize[i]?.height ?? this.itemH - this.contentLayout.spacingY) + this.contentLayout.spacingY; 365 | } 366 | } else { 367 | value += dataListLen * this.itemH; 368 | } 369 | //排列方向从下到上排序的话,scrollview底层会计算content的位置,导致位置不对,content的高度最小值改为父容器的大小 370 | if (this.contentLayout.verticalDirection == cc.Layout.VerticalDirection.BOTTOM_TO_TOP) { 371 | value = Math.max(value, this.content.parent?.height ?? 0) 372 | } 373 | 374 | this.content.height = value; 375 | break; 376 | case cc.Layout.Type.HORIZONTAL: 377 | value = layout.paddingLeft + layout.paddingRight; 378 | if (this.autoChildrenSize) { 379 | for (let i: number = 0; i < dataListLen; i++) { 380 | value += (this.posToSize[i]?.width ?? this.itemW - this.contentLayout.spacingX) + this.contentLayout.spacingX; 381 | } 382 | } else { 383 | value += dataListLen * this.itemW; 384 | } 385 | 386 | //排列方向从右到左排序的话,scrollview底层会计算content的位置,导致位置不对,content的宽度最小值改为父容器的大小 387 | if (this.contentLayout.horizontalDirection == cc.Layout.HorizontalDirection.RIGHT_TO_LEFT) { 388 | value = Math.max(value, this.content.parent?.width ?? 0) 389 | } 390 | 391 | this.content.width = value; 392 | break; 393 | case cc.Layout.Type.GRID: 394 | if (this.contentLayout.startAxis == cc.Layout.AxisDirection.HORIZONTAL) { 395 | this.content.height = layout.paddingTop + Math.ceil(dataListLen / this.horizontalCount) * this.itemH + layout.paddingBottom; 396 | } else if (this.contentLayout.startAxis == cc.Layout.AxisDirection.VERTICAL) { 397 | this.content.width = layout.paddingLeft + Math.ceil(dataListLen / this.verticalCount) * this.itemW + layout.paddingRight; 398 | } 399 | break; 400 | } 401 | } 402 | 403 | /**刷新预制体位置 和 数据填充 */ 404 | private refreshItem(): void { 405 | this.moveBottom && this.onInfiniteScrollCallback(); 406 | if (!this.refresh) { 407 | return; 408 | } 409 | switch (this.contentLayout.type) { 410 | case cc.Layout.Type.HORIZONTAL: 411 | this.refreshHorizontal(); 412 | break; 413 | case cc.Layout.Type.VERTICAL: 414 | this.refreshVertical(); 415 | break; 416 | case cc.Layout.Type.GRID: 417 | this.refreshGrid(); 418 | break; 419 | } 420 | this.refreshContentSize(); 421 | this.refresh = false; 422 | this.forcedRefresh = false; 423 | } 424 | 425 | /**刷新水平 */ 426 | private refreshHorizontal() { 427 | let start = this.getStart(); // Math.floor(Math.abs(this.getContentPosition().x) / this.itemW); 428 | // if (start < 0 || this.getContentPosition().x > 0) { //超出边界处理 429 | // start = 0; 430 | // } 431 | let end = start + this.horizontalCount; 432 | if (end > this.dataList.length) {//超出边界处理 433 | end = this.dataList.length; 434 | start = Math.max(end - this.horizontalCount, 0); 435 | } 436 | let tempV = 0; 437 | let itemListLen = this.itemList.length; 438 | let item:cc.Node, idx; 439 | for (var i = 0; i < itemListLen; i++) { 440 | idx = (start + i) % itemListLen; 441 | item = this.itemList[idx]; 442 | tempV = this.getPos(start + i); // this.startPos.x + ((start + i) * this.itemW); 443 | if (item.x != tempV || this.forcedRefresh) { 444 | console.log("修改的数据=" + (start + i)) 445 | item.x = tempV; 446 | this.itemRendererList[idx].node.nowDataIdx = start + i; 447 | this.itemRendererList[idx].data = this.dataList[start + i]; 448 | 449 | //记录位置和node大小 450 | this.posToSize[start + i] = item.getContentSize().clone(); 451 | 452 | if (this.infiniteScroll && start > 0 && start + i == this.dataList.length - 1) { 453 | this.moveBottom = true; 454 | } 455 | } 456 | } 457 | } 458 | 459 | /**刷新垂直 */ 460 | private refreshVertical(): void { 461 | let start = this.getStart(); //Math.floor(Math.abs(this.getContentPosition().y) / this.itemH); 462 | // if (start < 0 || this.getContentPosition().y < 0) { 463 | // start = 0; 464 | // } 465 | 466 | let end = start + this.verticalCount; 467 | if (end > this.dataList.length) { 468 | end = this.dataList.length; 469 | start = Math.max(end - this.verticalCount, 0); 470 | } 471 | 472 | let tempV = 0; 473 | let itemListLen = this.itemList.length; 474 | let item:cc.Node, idx; 475 | for (var i = 0; i < itemListLen; i++) { 476 | idx = (start + i) % itemListLen; 477 | item = this.itemList[idx]; 478 | tempV = this.getPos(start + i); // this.startPos.y + (-(start + i) * this.itemH); 479 | if (item.y != tempV || this.forcedRefresh) { 480 | console.log("修改的数据=" + (start + i)) 481 | item.y = tempV; 482 | this.itemRendererList[idx].node.nowDataIdx = start + i; 483 | this.itemRendererList[idx].data = this.dataList[start + i]; 484 | 485 | //记录位置和node大小 486 | this.posToSize[start + i] = item.getContentSize().clone(); 487 | if (this.infiniteScroll && start > 0 && start + i == this.dataList.length - 1) { 488 | this.moveBottom = true; 489 | } 490 | } 491 | } 492 | } 493 | 494 | /**刷新网格 */ 495 | private refreshGrid(): void { 496 | //是否垂直方向 添加网格 497 | let isVDirection = this.contentLayout.startAxis == cc.Layout.AxisDirection.VERTICAL; 498 | let start = Math.floor(Math.abs(this.getContentPosition().y) / this.itemH) * this.horizontalCount; 499 | if (isVDirection) { 500 | start = Math.floor(Math.abs(this.getContentPosition().x) / this.itemW) * this.verticalCount; 501 | if (this.getContentPosition().x > 0) { 502 | start = 0; 503 | } 504 | } else if (this.getContentPosition().y < 0) { 505 | start = 0; 506 | } 507 | 508 | if (start < 0) { 509 | start = 0; 510 | } 511 | 512 | let end = start + this.horizontalCount * this.verticalCount; 513 | if (end > this.dataList.length) { 514 | end = this.dataList.length; 515 | start = Math.max(end - this.horizontalCount * this.verticalCount, 0); 516 | } 517 | 518 | let tempX = 0; 519 | let tempY = 0; 520 | let itemListLen = this.itemList.length; 521 | let item, idx; 522 | for (var i = 0; i < itemListLen; i++) { 523 | idx = (start + i) % itemListLen; 524 | item = this.itemList[idx]; 525 | if (isVDirection) { 526 | tempX = this.startPos.x + (Math.floor((start + i) / this.verticalCount)) * this.itemW; 527 | tempY = this.startPos.y + -((start + i) % this.verticalCount) * this.itemH; 528 | } else { 529 | tempX = this.startPos.x + ((start + i) % this.horizontalCount) * this.itemW; 530 | tempY = this.startPos.y + -(Math.floor((start + i) / this.horizontalCount)) * this.itemH; 531 | } 532 | 533 | if (item.y != tempY || item.x != tempX || this.forcedRefresh) { 534 | console.log("修改的数据=" + (start + i)) 535 | item.x = tempX; 536 | item.y = tempY; 537 | this.itemRendererList[idx].data = this.dataList[start + i]; 538 | if (this.infiniteScroll && start > 0 && start + i == this.dataList.length - 1) { 539 | this.moveBottom = true; 540 | } 541 | } 542 | } 543 | } 544 | 545 | /**获取开始索引 */ 546 | private getStart(): number { 547 | let start: number = 0; 548 | /**节点高度 */ 549 | let value: number = 0; 550 | switch (this.contentLayout.type) { 551 | case cc.Layout.Type.HORIZONTAL: 552 | if (this.autoChildrenSize) { 553 | value = Math.abs(this.content.position.x); 554 | for (let item in this.posToSize) { 555 | value -= (this.posToSize[item]?.width ?? (this.itemW - this.contentLayout.spacingX)) + this.contentLayout.spacingX; 556 | if (value <= 0) { 557 | break 558 | } else { 559 | start++; 560 | } 561 | } 562 | } else { 563 | start = Math.floor(Math.abs(this.content.position.x) / this.itemW); 564 | } 565 | 566 | //超出边界处理 567 | if (this.contentLayout.horizontalDirection == cc.Layout.HorizontalDirection.LEFT_TO_RIGHT && this.content.position.x > 0 || this.contentLayout.horizontalDirection == cc.Layout.HorizontalDirection.RIGHT_TO_LEFT && this.content.position.x < 0) { 568 | start = 0; 569 | } 570 | break; 571 | case cc.Layout.Type.VERTICAL: 572 | if (this.autoChildrenSize) { 573 | value = Math.abs(this.content.position.y); 574 | for (let item in this.posToSize) { 575 | value -= (this.posToSize[item]?.height ?? (this.itemH - this.contentLayout.spacingY)) + this.contentLayout.spacingY; 576 | if (value <= 0) { 577 | break 578 | } else { 579 | start++; 580 | } 581 | } 582 | } else { 583 | start = Math.floor(Math.abs(this.content.position.y) / this.itemH); 584 | } 585 | 586 | //超出边界处理 587 | if (this.contentLayout.verticalDirection == cc.Layout.VerticalDirection.TOP_TO_BOTTOM && this.content.position.y < 0 || this.contentLayout.verticalDirection == cc.Layout.VerticalDirection.BOTTOM_TO_TOP && this.content.position.y > 0) { 588 | start = 0; 589 | } 590 | break; 591 | case cc.Layout.Type.GRID: 592 | console.error("暂不支持网格布局"); 593 | break; 594 | } 595 | if (start < 0) { 596 | start = 0; 597 | } 598 | return start; 599 | } 600 | 601 | /**根据索引获取位置 */ 602 | private getPos(idx: number): number { 603 | let position: number = 0; 604 | let h: number, w: number; 605 | switch (this.contentLayout.type) { 606 | case cc.Layout.Type.HORIZONTAL: 607 | if (this.autoChildrenSize) { 608 | //重置开始位置 609 | if (idx == 0) { 610 | w = (this.posToSize[0]?.width ?? (this.itemH - this.contentLayout.spacingX)); 611 | this.startPos.x = w * this.anchorPoint.x * this.direction.x; 612 | } 613 | position += this.startPos.x + this.padding.x * this.direction.x; 614 | for (let i: number = 1; i <= idx; i++) { 615 | w = (this.posToSize[i - 1]?.width ?? (this.itemW - this.contentLayout.spacingX)); 616 | position += (w - w * this.anchorPoint.x + this.contentLayout.spacingX) * this.direction.x; 617 | w = (this.posToSize[i]?.width ?? (this.itemW - this.contentLayout.spacingX)) 618 | position += (w * this.anchorPoint.x) * this.direction.x; 619 | } 620 | } else { 621 | position = this.startPos.x + idx * this.itemW * this.direction.x; 622 | } 623 | break; 624 | case cc.Layout.Type.VERTICAL: 625 | if (this.autoChildrenSize) { 626 | //重置开始位置 627 | if (idx == 0) { 628 | h = (this.posToSize[0]?.height ?? (this.itemH - this.contentLayout.spacingY)) 629 | this.startPos.y = (h - h * this.anchorPoint.y) * this.direction.y; 630 | } 631 | position += this.startPos.y + this.padding.y * this.direction.y; 632 | for (let i: number = 1; i <= idx; i++) { 633 | h = (this.posToSize[i - 1]?.height ?? (this.itemH - this.contentLayout.spacingY)) * this.anchorPoint.y + this.contentLayout.spacingY; 634 | position += h * this.direction.y; 635 | h = (this.posToSize[i]?.height ?? (this.itemH - this.contentLayout.spacingY)) 636 | position += (h - h * this.anchorPoint.y) * this.direction.y; 637 | } 638 | } else { 639 | position = this.startPos.y + idx * this.itemH * this.direction.y; 640 | } 641 | break; 642 | case cc.Layout.Type.GRID: 643 | console.error("暂不支持网格布局"); 644 | break; 645 | } 646 | return position; 647 | } 648 | 649 | } 650 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Script/core/AVirtualScrollView.ts: -------------------------------------------------------------------------------- 1 | import AItemRenderer from "./AItemRenerer"; 2 | 3 | const { ccclass, property } = cc._decorator; 4 | 5 | /** 6 | * 虚拟滚动视图 扩展cc.ScrollView 7 | * 渲染预制体必需挂载 AItemRenderer子类 8 | * @author slf 9 | */ 10 | @ccclass 11 | export default class AVirtualScrollView extends cc.ScrollView { 12 | /**渲染预制体必需挂载 AItemRenderer子类 */ 13 | @property({ type: cc.Prefab, serializable: true, displayName: "渲染预制体" }) 14 | itemRenderer: cc.Prefab = null; 15 | @property({ displayName: "启动虚拟列表" }) 16 | virtualList: boolean = true; 17 | /**开启滑动到底部 发送回调 */ 18 | @property({ tooltip: "无限滑动,到底后发送回调事件", visible() { return this.virtualList } }) 19 | infiniteScroll = false; 20 | @property({ tooltip: "子项自适应大小", visible() { return this.virtualList } }) 21 | autoChildrenSize = false; 22 | 23 | private infiniteScrollCb: Function; 24 | private infiniteScrollThis: any; 25 | 26 | /**子项点击 回调函数 回调作用域*/ 27 | protected callback: Function; 28 | protected cbThis: any; 29 | 30 | /**最大渲染预制体 垂直数量 */ 31 | private verticalCount: number; 32 | /**最大渲染预制体 水平数量 */ 33 | private horizontalCount: number; 34 | /**预制体宽高 */ 35 | private itemW: number; 36 | private itemH: number; 37 | /**定时器 */ 38 | private interval: number; 39 | /**预制体池 */ 40 | private itemPool: any[]; 41 | /**预制体列表 */ 42 | private itemList: cc.Node[]; 43 | /**预制体渲染类列表 */ 44 | private itemRendererList: any[]; 45 | /**数据列表 */ 46 | private dataList: any[]; 47 | /**方向布局 */ 48 | private direction: cc.Vec2 = new cc.Vec2(); 49 | /**方向间隙 */ 50 | private padding: cc.Vec2 = new cc.Vec2(); 51 | /**开始坐标 */ 52 | private startPos: cc.Vec2 = new cc.Vec2(); 53 | /**布局*/ 54 | private contentLayout: cc.Layout; 55 | /**是否移动到底部 无限滚动回调*/ 56 | private moveBottom: boolean; 57 | 58 | private _uiTransform: cc.Node; 59 | 60 | private isInit: boolean; 61 | 62 | /**强制刷新 */ 63 | private forcedRefresh: boolean; 64 | /**刷新 */ 65 | private refresh: boolean; 66 | 67 | /**节点锚点 */ 68 | private anchorPoint: cc.Vec2; 69 | /**位置对应节点大小 */ 70 | private posToSize: { [key: number]: cc.Size } = {}; 71 | 72 | protected onLoad(): void { 73 | this.isInit = true; 74 | this.itemList = []; 75 | this.itemPool = []; 76 | this.itemRendererList = []; 77 | 78 | if (this.virtualList) { 79 | this.contentLayout = this.content.getComponent(cc.Layout); 80 | this.contentLayout.enabled = false; 81 | this._uiTransform = this.node; 82 | this.resetSize(); 83 | this.node.on(cc.Node.EventType.SIZE_CHANGED, this.onSelfSizeChange, this); 84 | 85 | if (this.autoChildrenSize && this.contentLayout.type == cc.Layout.Type.GRID) { 86 | this.autoChildrenSize = false; 87 | console.error("子项自适应大小 暂不支持网格布局"); 88 | } 89 | 90 | // //起始位置 91 | // let itemNode: cc.Node = this.itemRenderer.data; 92 | // this.startPos = new cc.Vec2(itemNode.width * itemNode.anchorX + this.contentLayout.paddingLeft, -(itemNode.height * itemNode.anchorY + this.contentLayout.paddingTop)); 93 | // //预制体宽高 94 | // this.itemW = itemNode.width + this.contentLayout.spacingX; 95 | // this.itemH = itemNode.height + this.contentLayout.spacingY; 96 | // //垂直、水平最大预制体数量 97 | // this.horizontalCount = Math.ceil(this.node.width / this.itemW) + 1; 98 | // this.verticalCount = Math.ceil(this.node.height / this.itemH) + 1; 99 | 100 | // if (this.contentLayout.type == cc.Layout.Type.GRID) { 101 | // if (this.contentLayout.startAxis == cc.Layout.AxisDirection.HORIZONTAL) { 102 | // this.horizontalCount = Math.floor(this.node.width / this.itemW); 103 | // } else { 104 | // this.verticalCount = Math.floor(this.node.height / this.itemH); 105 | // } 106 | // } 107 | } 108 | 109 | if (this.dataList) { 110 | this.refreshData(this.dataList); 111 | } 112 | } 113 | 114 | private onSelfSizeChange() { 115 | this.unschedule(this.delayRefresh); 116 | this.scheduleOnce(this.delayRefresh, 0.5); 117 | } 118 | 119 | private delayRefresh(): void { 120 | this.resetSize(); 121 | if (this.dataList != null) { 122 | this.refreshData(this.dataList); 123 | } 124 | } 125 | 126 | /**重置大小 */ 127 | public resetSize(): void { 128 | let widget = this.content.getComponent(cc.Widget); 129 | if (widget) { 130 | widget.updateAlignment(); 131 | } else { 132 | widget = this.getComponent(cc.Widget); 133 | widget && widget.updateAlignment(); 134 | } 135 | 136 | 137 | let nodeUITransform: cc.Node = this.itemRenderer.data; 138 | this.anchorPoint = nodeUITransform.getAnchorPoint().clone(); 139 | let nodeWidth = nodeUITransform.width; 140 | let nodeHeight = nodeUITransform.height; 141 | 142 | //自适应节点大小 143 | if (this.autoChildrenSize) { 144 | nodeWidth = this.posToSize[0]?.width ?? nodeUITransform.width; 145 | nodeHeight = this.posToSize[0]?.height ?? nodeUITransform.height; 146 | } 147 | //方向布局 148 | this.direction.x = this.contentLayout.horizontalDirection == cc.Layout.HorizontalDirection.LEFT_TO_RIGHT ? 1 : -1; 149 | this.direction.y = this.contentLayout.verticalDirection == cc.Layout.VerticalDirection.TOP_TO_BOTTOM ? -1 : 1; 150 | 151 | //上下左右间隙 152 | this.padding.x = (this.contentLayout.horizontalDirection == cc.Layout.HorizontalDirection.LEFT_TO_RIGHT ? this.contentLayout.paddingLeft : this.contentLayout.paddingRight); 153 | this.padding.y = (this.contentLayout.verticalDirection == cc.Layout.VerticalDirection.TOP_TO_BOTTOM ? this.contentLayout.paddingTop : this.contentLayout.paddingBottom); 154 | 155 | //第一个节点大小 计算起始位置 156 | this.startPos.x = (nodeWidth - nodeWidth * this.anchorPoint.x + this.padding.x) * this.direction.x; 157 | this.startPos.y = (nodeHeight - nodeHeight * this.anchorPoint.y + this.padding.y) * this.direction.y; 158 | 159 | 160 | //预制体宽高 161 | this.itemW = nodeUITransform.width + this.contentLayout.spacingX; 162 | this.itemH = nodeUITransform.height + this.contentLayout.spacingY; 163 | 164 | let hCount = (this._uiTransform.width + this.contentLayout.spacingX - this.contentLayout.paddingLeft) / this.itemW; 165 | let vCount = (this._uiTransform.height + this.contentLayout.spacingY - this.contentLayout.paddingTop) / this.itemH; 166 | 167 | //垂直、水平最大预制体数量 如果自适应子项大小 用默认节点大小计算最大渲染数量 168 | this.horizontalCount = Math.ceil(hCount) + 1; 169 | this.verticalCount = Math.ceil(vCount) + 1; 170 | 171 | if (this.contentLayout.type == cc.Layout.Type.GRID) { 172 | if (this.contentLayout.startAxis == cc.Layout.AxisDirection.HORIZONTAL) { 173 | this.horizontalCount = Math.floor(hCount); 174 | } else { 175 | this.verticalCount = Math.floor(vCount); 176 | } 177 | } 178 | } 179 | 180 | protected onDestroy(): void { 181 | this.dataList = null; 182 | this.itemList = null; 183 | this.itemRendererList = null; 184 | this.posToSize = null; 185 | this.itemPool.forEach(item => { 186 | item.destroy(); 187 | }) 188 | this.itemPool = null; 189 | if (this.interval) { 190 | clearInterval(this.interval); 191 | } 192 | this.node.targetOff(this); 193 | } 194 | 195 | /**利用cc.ScrollView本身方法 来标记滑动中 */ 196 | setContentPosition(position: cc.Vec2) { 197 | super.setContentPosition(position); 198 | this.refresh = true; 199 | } 200 | 201 | /** 202 | * 设置列表 子项点击回调 203 | * 回调会携带当前子项的 data 204 | * @param cb 回调 205 | * @param cbT 作用域 206 | */ 207 | public setTouchItemCallback(cb: Function, cbT: any): void { 208 | this.callback = cb; 209 | this.cbThis = cbT; 210 | } 211 | 212 | /**选中数据 */ 213 | private onItemTap(data: any): void { 214 | this.callback && this.callback.call(this.cbThis, data); 215 | } 216 | 217 | /** 218 | * 设置列表 无限滚动到底部后 回调 219 | * @param cb 回调 220 | * @param cbT 作用域 221 | */ 222 | public setInfiniteScrollCallback(cb: () => void, cbT: any): void { 223 | this.infiniteScrollCb = cb; 224 | this.infiniteScrollThis = cbT; 225 | } 226 | 227 | /**无限滚动到底部后 回调 */ 228 | private onInfiniteScrollCallback(): void { 229 | this.moveBottom = false; 230 | if (this.infiniteScrollCb) { 231 | console.log("发送回调"); 232 | this.infiniteScrollCb.call(this.infiniteScrollThis); 233 | } 234 | } 235 | 236 | /** 237 | * 刷新数据 238 | * @param data 数据源 单项|队列 239 | */ 240 | public refreshData(data: any | any[]): void { 241 | if (Array.isArray(data)) { 242 | this.dataList = data; 243 | } else { 244 | this.dataList = [data]; 245 | } 246 | 247 | if (!this.isInit) { 248 | return; 249 | } 250 | 251 | if (this.interval) { 252 | clearInterval(this.interval); 253 | } 254 | this.addItem(); 255 | 256 | if (this.virtualList) { 257 | this.refreshContentSize(); 258 | this.forcedRefresh = true; 259 | this.refresh = true; 260 | this.interval = setInterval(this.refreshItem.bind(this), 1000 / 10); 261 | this.refreshItem(); 262 | } 263 | 264 | } 265 | 266 | 267 | /**添加预制体 */ 268 | private addItem(): void { 269 | let len: number = 0; 270 | if (this.virtualList) { 271 | switch (this.contentLayout.type) { 272 | case cc.Layout.Type.HORIZONTAL: 273 | len = this.horizontalCount; 274 | break; 275 | case cc.Layout.Type.VERTICAL: 276 | len = this.verticalCount; 277 | break; 278 | case cc.Layout.Type.GRID: 279 | len = this.horizontalCount * this.verticalCount; 280 | break; 281 | } 282 | len = Math.min(len, this.dataList.length); 283 | } else { 284 | len = this.dataList.length; 285 | } 286 | 287 | let itemListLen = this.itemList.length; 288 | if (itemListLen < len) { 289 | let itemRenderer: AItemRenderer = null; 290 | for (var i = itemListLen; i < len; i++) { 291 | let child = this.itemPool.length > 0 ? this.itemPool.shift() : cc.instantiate(this.itemRenderer); 292 | this.content.addChild(child); 293 | this.itemList.push(child); 294 | itemRenderer = child.getComponent(AItemRenderer); 295 | this.itemRendererList.push(itemRenderer); 296 | 297 | if (itemRenderer.isClick) { 298 | itemRenderer.setTouchCallback(this.onItemTap, this); 299 | } 300 | if (this.autoChildrenSize) { 301 | child.on(cc.Node.EventType.SIZE_CHANGED, this.delayChangeRefreshMark.bind(this, child), this); 302 | } 303 | } 304 | } else { 305 | let cL: number = this.content.childrenCount; 306 | let item; 307 | while (cL > len) { 308 | item = this.itemList[cL - 1]; 309 | this.content.removeChild(item); 310 | this.itemList.splice(cL - 1, 1); 311 | this.itemRendererList.splice(cL - 1, 1); 312 | this.itemPool.push(item); 313 | cL = this.content.childrenCount; 314 | } 315 | } 316 | 317 | if (!this.virtualList) { 318 | this.dataList.forEach((v, idx) => { 319 | this.itemRendererList[idx].data = v; 320 | }); 321 | } 322 | } 323 | 324 | /**延迟一帧标记刷新列表数据 */ 325 | private delayChangeRefreshMark(node: cc.Node) { 326 | let pos = this.posToSize[node['nowDataIdx']]; 327 | //记录数据源索引Node改变后的大小 328 | if (pos) { 329 | if (pos.width == node.width && pos.height == node.height) { 330 | return; 331 | } 332 | pos.width = node.width; 333 | pos.height = node.height; 334 | } else { 335 | pos = node.getContentSize().clone(); 336 | } 337 | console.log("预刷新子项状态===" + node['nowDataIdx']); 338 | this.posToSize[node['nowDataIdx']] = pos; 339 | //延迟一帧刷新 340 | // this.unschedule(this.changeRefreshMark); 341 | // this.scheduleOnce(this.changeRefreshMark, 0); 342 | 343 | //立即刷新 344 | this.refresh = true; 345 | this.refreshItem(); 346 | } 347 | 348 | /**标记刷新列表等定时刷新数据 */ 349 | private changeRefreshMark(): void { 350 | // console.log("刷新子项状态"); 351 | this.refresh = true; 352 | } 353 | 354 | /**根据数据数量 改变content宽高 */ 355 | private refreshContentSize(): void { 356 | let layout: cc.Layout = this.contentLayout; 357 | let dataListLen: number = this.dataList.length; 358 | let value: number; 359 | switch (this.contentLayout.type) { 360 | case cc.Layout.Type.VERTICAL: 361 | value = layout.paddingTop + layout.paddingBottom; 362 | if (this.autoChildrenSize) { 363 | for (let i: number = 0; i < dataListLen; i++) { 364 | value += (this.posToSize[i]?.height ?? this.itemH - this.contentLayout.spacingY) + this.contentLayout.spacingY; 365 | } 366 | } else { 367 | value += dataListLen * this.itemH; 368 | } 369 | //排列方向从下到上排序的话,scrollview底层会计算content的位置,导致位置不对,content的高度最小值改为父容器的大小 370 | if (this.contentLayout.verticalDirection == cc.Layout.VerticalDirection.BOTTOM_TO_TOP) { 371 | value = Math.max(value, this.content.parent?.height ?? 0) 372 | } 373 | 374 | this.content.height = value; 375 | break; 376 | case cc.Layout.Type.HORIZONTAL: 377 | value = layout.paddingLeft + layout.paddingRight; 378 | if (this.autoChildrenSize) { 379 | for (let i: number = 0; i < dataListLen; i++) { 380 | value += (this.posToSize[i]?.width ?? this.itemW - this.contentLayout.spacingX) + this.contentLayout.spacingX; 381 | } 382 | } else { 383 | value += dataListLen * this.itemW; 384 | } 385 | 386 | //排列方向从右到左排序的话,scrollview底层会计算content的位置,导致位置不对,content的宽度最小值改为父容器的大小 387 | if (this.contentLayout.horizontalDirection == cc.Layout.HorizontalDirection.RIGHT_TO_LEFT) { 388 | value = Math.max(value, this.content.parent?.width ?? 0) 389 | } 390 | 391 | this.content.width = value; 392 | break; 393 | case cc.Layout.Type.GRID: 394 | if (this.contentLayout.startAxis == cc.Layout.AxisDirection.HORIZONTAL) { 395 | this.content.height = layout.paddingTop + Math.ceil(dataListLen / this.horizontalCount) * this.itemH + layout.paddingBottom; 396 | } else if (this.contentLayout.startAxis == cc.Layout.AxisDirection.VERTICAL) { 397 | this.content.width = layout.paddingLeft + Math.ceil(dataListLen / this.verticalCount) * this.itemW + layout.paddingRight; 398 | } 399 | break; 400 | } 401 | } 402 | 403 | /**刷新预制体位置 和 数据填充 */ 404 | private refreshItem(): void { 405 | this.moveBottom && this.onInfiniteScrollCallback(); 406 | if (!this.refresh) { 407 | return; 408 | } 409 | switch (this.contentLayout.type) { 410 | case cc.Layout.Type.HORIZONTAL: 411 | this.refreshHorizontal(); 412 | break; 413 | case cc.Layout.Type.VERTICAL: 414 | this.refreshVertical(); 415 | break; 416 | case cc.Layout.Type.GRID: 417 | this.refreshGrid(); 418 | break; 419 | } 420 | this.refreshContentSize(); 421 | this.refresh = false; 422 | this.forcedRefresh = false; 423 | } 424 | 425 | /**刷新水平 */ 426 | private refreshHorizontal() { 427 | let start = this.getStart(); // Math.floor(Math.abs(this.getContentPosition().x) / this.itemW); 428 | // if (start < 0 || this.getContentPosition().x > 0) { //超出边界处理 429 | // start = 0; 430 | // } 431 | let end = start + this.horizontalCount; 432 | if (end > this.dataList.length) {//超出边界处理 433 | end = this.dataList.length; 434 | start = Math.max(end - this.horizontalCount, 0); 435 | } 436 | let tempV = 0; 437 | let itemListLen = this.itemList.length; 438 | let item:cc.Node, idx; 439 | for (var i = 0; i < itemListLen; i++) { 440 | idx = (start + i) % itemListLen; 441 | item = this.itemList[idx]; 442 | tempV = this.getPos(start + i); // this.startPos.x + ((start + i) * this.itemW); 443 | if (item.x != tempV || this.forcedRefresh) { 444 | console.log("修改的数据=" + (start + i)) 445 | item.x = tempV; 446 | this.itemRendererList[idx].node.nowDataIdx = start + i; 447 | this.itemRendererList[idx].data = this.dataList[start + i]; 448 | 449 | //记录位置和node大小 450 | this.posToSize[start + i] = item.getContentSize().clone(); 451 | 452 | if (this.infiniteScroll && start > 0 && start + i == this.dataList.length - 1) { 453 | this.moveBottom = true; 454 | } 455 | } 456 | } 457 | } 458 | 459 | /**刷新垂直 */ 460 | private refreshVertical(): void { 461 | let start = this.getStart(); //Math.floor(Math.abs(this.getContentPosition().y) / this.itemH); 462 | // if (start < 0 || this.getContentPosition().y < 0) { 463 | // start = 0; 464 | // } 465 | 466 | let end = start + this.verticalCount; 467 | if (end > this.dataList.length) { 468 | end = this.dataList.length; 469 | start = Math.max(end - this.verticalCount, 0); 470 | } 471 | 472 | let tempV = 0; 473 | let itemListLen = this.itemList.length; 474 | let item:cc.Node, idx; 475 | for (var i = 0; i < itemListLen; i++) { 476 | idx = (start + i) % itemListLen; 477 | item = this.itemList[idx]; 478 | tempV = this.getPos(start + i); // this.startPos.y + (-(start + i) * this.itemH); 479 | if (item.y != tempV || this.forcedRefresh) { 480 | console.log("修改的数据=" + (start + i)) 481 | item.y = tempV; 482 | this.itemRendererList[idx].node.nowDataIdx = start + i; 483 | this.itemRendererList[idx].data = this.dataList[start + i]; 484 | 485 | //记录位置和node大小 486 | this.posToSize[start + i] = item.getContentSize().clone(); 487 | if (this.infiniteScroll && start > 0 && start + i == this.dataList.length - 1) { 488 | this.moveBottom = true; 489 | } 490 | } 491 | } 492 | } 493 | 494 | /**刷新网格 */ 495 | private refreshGrid(): void { 496 | //是否垂直方向 添加网格 497 | let isVDirection = this.contentLayout.startAxis == cc.Layout.AxisDirection.VERTICAL; 498 | let start = Math.floor(Math.abs(this.getContentPosition().y) / this.itemH) * this.horizontalCount; 499 | if (isVDirection) { 500 | start = Math.floor(Math.abs(this.getContentPosition().x) / this.itemW) * this.verticalCount; 501 | if (this.getContentPosition().x > 0) { 502 | start = 0; 503 | } 504 | } else if (this.getContentPosition().y < 0) { 505 | start = 0; 506 | } 507 | 508 | if (start < 0) { 509 | start = 0; 510 | } 511 | 512 | let end = start + this.horizontalCount * this.verticalCount; 513 | if (end > this.dataList.length) { 514 | end = this.dataList.length; 515 | start = Math.max(end - this.horizontalCount * this.verticalCount, 0); 516 | } 517 | 518 | let tempX = 0; 519 | let tempY = 0; 520 | let itemListLen = this.itemList.length; 521 | let item, idx; 522 | for (var i = 0; i < itemListLen; i++) { 523 | idx = (start + i) % itemListLen; 524 | item = this.itemList[idx]; 525 | if (isVDirection) { 526 | tempX = this.startPos.x + (Math.floor((start + i) / this.verticalCount)) * this.itemW; 527 | tempY = this.startPos.y + -((start + i) % this.verticalCount) * this.itemH; 528 | } else { 529 | tempX = this.startPos.x + ((start + i) % this.horizontalCount) * this.itemW; 530 | tempY = this.startPos.y + -(Math.floor((start + i) / this.horizontalCount)) * this.itemH; 531 | } 532 | 533 | if (item.y != tempY || item.x != tempX || this.forcedRefresh) { 534 | console.log("修改的数据=" + (start + i)) 535 | item.x = tempX; 536 | item.y = tempY; 537 | this.itemRendererList[idx].data = this.dataList[start + i]; 538 | if (this.infiniteScroll && start > 0 && start + i == this.dataList.length - 1) { 539 | this.moveBottom = true; 540 | } 541 | } 542 | } 543 | } 544 | 545 | /**获取开始索引 */ 546 | private getStart(): number { 547 | let start: number = 0; 548 | /**节点高度 */ 549 | let value: number = 0; 550 | switch (this.contentLayout.type) { 551 | case cc.Layout.Type.HORIZONTAL: 552 | if (this.autoChildrenSize) { 553 | value = Math.abs(this.content.position.x); 554 | for (let item in this.posToSize) { 555 | value -= (this.posToSize[item]?.width ?? (this.itemW - this.contentLayout.spacingX)) + this.contentLayout.spacingX; 556 | if (value <= 0) { 557 | break 558 | } else { 559 | start++; 560 | } 561 | } 562 | } else { 563 | start = Math.floor(Math.abs(this.content.position.x) / this.itemW); 564 | } 565 | 566 | //超出边界处理 567 | if (this.contentLayout.horizontalDirection == cc.Layout.HorizontalDirection.LEFT_TO_RIGHT && this.content.position.x > 0 || this.contentLayout.horizontalDirection == cc.Layout.HorizontalDirection.RIGHT_TO_LEFT && this.content.position.x < 0) { 568 | start = 0; 569 | } 570 | break; 571 | case cc.Layout.Type.VERTICAL: 572 | if (this.autoChildrenSize) { 573 | value = Math.abs(this.content.position.y); 574 | for (let item in this.posToSize) { 575 | value -= (this.posToSize[item]?.height ?? (this.itemH - this.contentLayout.spacingY)) + this.contentLayout.spacingY; 576 | if (value <= 0) { 577 | break 578 | } else { 579 | start++; 580 | } 581 | } 582 | } else { 583 | start = Math.floor(Math.abs(this.content.position.y) / this.itemH); 584 | } 585 | 586 | //超出边界处理 587 | if (this.contentLayout.verticalDirection == cc.Layout.VerticalDirection.TOP_TO_BOTTOM && this.content.position.y < 0 || this.contentLayout.verticalDirection == cc.Layout.VerticalDirection.BOTTOM_TO_TOP && this.content.position.y > 0) { 588 | start = 0; 589 | } 590 | break; 591 | case cc.Layout.Type.GRID: 592 | console.error("暂不支持网格布局"); 593 | break; 594 | } 595 | if (start < 0) { 596 | start = 0; 597 | } 598 | return start; 599 | } 600 | 601 | /**根据索引获取位置 */ 602 | private getPos(idx: number): number { 603 | let position: number = 0; 604 | let h: number, w: number; 605 | switch (this.contentLayout.type) { 606 | case cc.Layout.Type.HORIZONTAL: 607 | if (this.autoChildrenSize) { 608 | //重置开始位置 609 | if (idx == 0) { 610 | w = (this.posToSize[0]?.width ?? (this.itemH - this.contentLayout.spacingX)); 611 | this.startPos.x = w * this.anchorPoint.x * this.direction.x; 612 | } 613 | position += this.startPos.x + this.padding.x * this.direction.x; 614 | for (let i: number = 1; i <= idx; i++) { 615 | w = (this.posToSize[i - 1]?.width ?? (this.itemW - this.contentLayout.spacingX)); 616 | position += (w - w * this.anchorPoint.x + this.contentLayout.spacingX) * this.direction.x; 617 | w = (this.posToSize[i]?.width ?? (this.itemW - this.contentLayout.spacingX)) 618 | position += (w * this.anchorPoint.x) * this.direction.x; 619 | } 620 | } else { 621 | position = this.startPos.x + idx * this.itemW * this.direction.x; 622 | } 623 | break; 624 | case cc.Layout.Type.VERTICAL: 625 | if (this.autoChildrenSize) { 626 | //重置开始位置 627 | if (idx == 0) { 628 | h = (this.posToSize[0]?.height ?? (this.itemH - this.contentLayout.spacingY)) 629 | this.startPos.y = (h - h * this.anchorPoint.y) * this.direction.y; 630 | } 631 | position += this.startPos.y + this.padding.y * this.direction.y; 632 | for (let i: number = 1; i <= idx; i++) { 633 | h = (this.posToSize[i - 1]?.height ?? (this.itemH - this.contentLayout.spacingY)) * this.anchorPoint.y + this.contentLayout.spacingY; 634 | position += h * this.direction.y; 635 | h = (this.posToSize[i]?.height ?? (this.itemH - this.contentLayout.spacingY)) 636 | position += (h - h * this.anchorPoint.y) * this.direction.y; 637 | } 638 | } else { 639 | position = this.startPos.y + idx * this.itemH * this.direction.y; 640 | } 641 | break; 642 | case cc.Layout.Type.GRID: 643 | console.error("暂不支持网格布局"); 644 | break; 645 | } 646 | return position; 647 | } 648 | 649 | } 650 | -------------------------------------------------------------------------------- /example/3.2.0/virtualList/assets/Scripts/core/AVirtualScrollView.ts: -------------------------------------------------------------------------------- 1 | import { _decorator, Component, Node, Prefab, Vec2, Layout, ScrollView, Rect, UITransform, instantiate, Vec3 } from 'cc'; 2 | import { Widget } from 'cc'; 3 | import { view } from 'cc'; 4 | import { NodeEventType } from 'cc'; 5 | import { Size } from 'cc'; 6 | import { CCBoolean } from 'cc'; 7 | import { isValid } from 'cc'; 8 | import { TransformBit } from 'cc'; 9 | import { AItemRenderer } from './AItemRenderer'; 10 | 11 | const { ccclass, property } = _decorator; 12 | 13 | /** 14 | * 虚拟滚动视图 扩展ScrollView 15 | * 渲染预制体必需挂载 AItemRenderer子类 16 | * @author slf 17 | */ 18 | @ccclass('AVirtualScrollView') 19 | export default class AVirtualScrollView extends ScrollView { 20 | /**渲染预制体必需挂载 ItemRenderer子类 */ 21 | @property({ type: Prefab, serializable: true, displayName: "渲染预制体" }) 22 | itemRenderer: Prefab = null; 23 | @property({ displayName: "启动虚拟列表" }) 24 | virtualList: boolean = true; 25 | /**开启滑动到底部 发送回调 */ 26 | @property({ tooltip: "无限滑动,到底后发送回调事件", visible() { return this.virtualList } }) 27 | infiniteScroll = false; 28 | @property({ tooltip: "子项自适应大小", visible() { return this.virtualList } }) 29 | autoChildrenSize = false; 30 | 31 | 32 | private infiniteScrollCb: Function; 33 | private infiniteScrollThis: any; 34 | 35 | /**子项 回调函数 回调作用域*/ 36 | protected callback: Function; 37 | protected cbThis: any; 38 | 39 | /**最大渲染预制体 垂直数量 */ 40 | private verticalCount: number; 41 | /**最大渲染预制体 水平数量 */ 42 | private horizontalCount: number; 43 | /**预制体默认宽 加上间隔 */ 44 | private itemW: number; 45 | /**预制体默认高 加上间隔*/ 46 | private itemH: number; 47 | /**定时器 */ 48 | private interval: any; 49 | /**预制体池 */ 50 | private itemPool: any[]; 51 | /**预制体列表 */ 52 | private itemList: Node[]; 53 | /**预制体渲染类列表 */ 54 | private itemRendererList: any[]; 55 | /**数据列表 */ 56 | private dataList: any[]; 57 | /**方向布局 */ 58 | private direction: Vec2 = new Vec2(); 59 | /**方向间隙 */ 60 | private padding: Vec2 = new Vec2(); 61 | /**开始坐标 */ 62 | private startPos: Vec2 = new Vec2(); 63 | /**布局*/ 64 | private contentLayout: Layout; 65 | /**强制刷新标志 定时更新数据*/ 66 | private forcedRefreshMark: boolean; 67 | /**刷新标志 定时更新数据 */ 68 | private refreshMark: boolean; 69 | /**是否移动到底部 无限滚动回调*/ 70 | private moveBottom: boolean; 71 | 72 | private _uiTransform: UITransform; 73 | 74 | private isInit: boolean; 75 | 76 | /**节点锚点 */ 77 | private anchorPoint: Vec2; 78 | /**位置对应节点大小 */ 79 | private posToSize: { [key: number]: Size } = {}; 80 | 81 | onLoad() { 82 | this.isInit = true; 83 | this.itemList = []; 84 | this.itemPool = []; 85 | this.itemRendererList = []; 86 | 87 | if (this.virtualList) { 88 | this.contentLayout = this.content.getComponent(Layout); 89 | this.contentLayout.enabled = false; 90 | this._uiTransform = this.node.getComponent(UITransform); 91 | this.resetSize(); 92 | this.node.on(NodeEventType.SIZE_CHANGED, this.onSelfSizeChange, this); 93 | 94 | if (this.autoChildrenSize && this.contentLayout.type == Layout.Type.GRID) { 95 | this.autoChildrenSize = false; 96 | console.error("子项自适应大小 暂不支持网格布局"); 97 | } 98 | } 99 | if (this.dataList) { 100 | this.refreshData(this.dataList); 101 | } 102 | } 103 | 104 | private onSelfSizeChange() { 105 | this.unschedule(this.delayRefresh); 106 | this.scheduleOnce(this.delayRefresh, 0.5); 107 | } 108 | 109 | private delayRefresh(): void { 110 | this.resetSize(); 111 | if (this.dataList != null) { 112 | this.refreshData(this.dataList); 113 | } 114 | } 115 | 116 | /**重置大小 */ 117 | public resetSize(): void { 118 | let widget = this.content.getComponent(Widget); 119 | if (widget) { 120 | widget.updateAlignment(); 121 | } else { 122 | widget = this.getComponent(Widget); 123 | widget && widget.updateAlignment(); 124 | } 125 | 126 | 127 | let nodeUITransform: UITransform = this.itemRenderer.data._uiProps.uiTransformComp; 128 | this.anchorPoint = nodeUITransform.anchorPoint.clone(); 129 | let nodeWidth = nodeUITransform.width; 130 | let nodeHeight = nodeUITransform.height; 131 | 132 | //自适应节点大小 133 | if (this.autoChildrenSize) { 134 | nodeWidth = this.posToSize[0]?.width ?? nodeUITransform.width; 135 | nodeHeight = this.posToSize[0]?.height ?? nodeUITransform.height; 136 | } 137 | //方向布局 138 | this.direction.x = this.contentLayout.horizontalDirection == Layout.HorizontalDirection.LEFT_TO_RIGHT ? 1 : -1; 139 | this.direction.y = this.contentLayout.verticalDirection == Layout.VerticalDirection.TOP_TO_BOTTOM ? -1 : 1; 140 | 141 | //上下左右间隙 142 | this.padding.x = (this.contentLayout.horizontalDirection == Layout.HorizontalDirection.LEFT_TO_RIGHT ? this.contentLayout.paddingLeft : this.contentLayout.paddingRight); 143 | this.padding.y = (this.contentLayout.verticalDirection == Layout.VerticalDirection.TOP_TO_BOTTOM ? this.contentLayout.paddingTop : this.contentLayout.paddingBottom); 144 | 145 | //第一个节点大小 计算起始位置 146 | this.startPos.x = (nodeWidth - nodeWidth * this.anchorPoint.x + this.padding.x) * this.direction.x; 147 | this.startPos.y = (nodeHeight - nodeHeight * this.anchorPoint.y + this.padding.y) * this.direction.y; 148 | 149 | 150 | //预制体宽高 151 | this.itemW = nodeUITransform.width + this.contentLayout.spacingX; 152 | this.itemH = nodeUITransform.height + this.contentLayout.spacingY; 153 | 154 | let hCount = (this._uiTransform.width + this.contentLayout.spacingX - this.contentLayout.paddingLeft) / this.itemW; 155 | let vCount = (this._uiTransform.height + this.contentLayout.spacingY - this.contentLayout.paddingTop) / this.itemH; 156 | 157 | //垂直、水平最大预制体数量 如果自适应子项大小 用默认节点大小计算最大渲染数量 158 | this.horizontalCount = Math.ceil(hCount) + 1; 159 | this.verticalCount = Math.ceil(vCount) + 1; 160 | 161 | if (this.contentLayout.type == Layout.Type.GRID) { 162 | if (this.contentLayout.startAxis == Layout.AxisDirection.HORIZONTAL) { 163 | this.horizontalCount = Math.floor(hCount); 164 | } else { 165 | this.verticalCount = Math.floor(vCount); 166 | } 167 | } 168 | } 169 | 170 | /**利用ScrollView本身方法 来标记滑动中 */ 171 | _setContentPosition(position: Vec3) { 172 | super['_setContentPosition'](position); 173 | this.refreshMark = true; 174 | } 175 | 176 | /** 177 | * 设置列表 子项回调 178 | * 回调会携带当前子项的 data 和 透传参数 179 | * @param cb 回调 180 | * @param cbT 作用域 181 | */ 182 | public setItemCallback(cb: (data: any, thisarg?: any) => void, cbT: any): void { 183 | this.callback = cb; 184 | this.cbThis = cbT; 185 | } 186 | 187 | /**子项回调 */ 188 | private onItemCallback(data: any): void { 189 | this.callback && this.callback.call(this.cbThis, data); 190 | } 191 | 192 | /** 193 | * 设置列表 无限滚动到底部后 回调 194 | * @param cb 回调 195 | * @param cbT 作用域 196 | */ 197 | public setInfiniteScrollCallback(cb: () => void, cbT: any): void { 198 | this.infiniteScrollCb = cb; 199 | this.infiniteScrollThis = cbT; 200 | } 201 | 202 | /**无限滚动到底部后 回调 */ 203 | private onInfiniteScrollCallback(): void { 204 | this.moveBottom = false; 205 | if (this.infiniteScrollCb) { 206 | console.log("发送回调"); 207 | this.infiniteScrollCb.call(this.infiniteScrollThis); 208 | } 209 | } 210 | 211 | /** 212 | * 刷新数据 213 | * @param data 数据源 单项|队列 214 | */ 215 | public refreshData(data: any | any[]): void { 216 | if (Array.isArray(data)) { 217 | this.dataList = data; 218 | } else { 219 | this.dataList = [data]; 220 | } 221 | 222 | if (!this.isInit) { 223 | return; 224 | } 225 | 226 | if (this.interval) { 227 | clearInterval(this.interval); 228 | this.interval = null; 229 | } 230 | this.addItem(); 231 | 232 | if (this.virtualList) { 233 | // this.refreshContentSize(); 234 | this.forcedRefreshMark = true; 235 | this.refreshMark = true; 236 | this.interval = setInterval(this.refreshItem.bind(this), 1000 / 10); 237 | this.refreshItem(); 238 | } 239 | } 240 | 241 | /**添加预制体 */ 242 | private addItem(): void { 243 | let len: number = 0; 244 | if (this.virtualList) { 245 | switch (this.contentLayout.type) { 246 | case Layout.Type.HORIZONTAL: 247 | len = this.horizontalCount; 248 | break; 249 | case Layout.Type.VERTICAL: 250 | len = this.verticalCount; 251 | break; 252 | case Layout.Type.GRID: 253 | len = this.horizontalCount * this.verticalCount; 254 | break; 255 | } 256 | len = Math.min(len, this.dataList.length); 257 | } else { 258 | len = this.dataList.length; 259 | } 260 | 261 | let itemListLen = this.itemList.length; 262 | if (itemListLen < len) { 263 | let itemRenderer: AItemRenderer = null; 264 | for (var i = itemListLen; i < len; i++) { 265 | let child = this.itemPool.length > 0 ? this.itemPool.shift() : instantiate(this.itemRenderer); 266 | this.content.addChild(child); 267 | this.itemList.push(child); 268 | itemRenderer = child.getComponent(AItemRenderer); 269 | this.itemRendererList.push(itemRenderer); 270 | itemRenderer.registerCallback(this.onItemCallback, this); 271 | if (this.autoChildrenSize) { 272 | child.on(NodeEventType.SIZE_CHANGED, this.delayChangeRefreshMark.bind(this, child), this); 273 | } 274 | } 275 | } else { 276 | let cL: number = this.content.children.length; 277 | let item; 278 | while (cL > len) { 279 | item = this.itemList[cL - 1]; 280 | this.content.removeChild(item); 281 | this.itemList.splice(cL - 1, 1); 282 | this.itemRendererList.splice(cL - 1, 1); 283 | this.itemPool.push(item); 284 | cL = this.content.children.length; 285 | } 286 | } 287 | 288 | if (!this.virtualList) { 289 | this.dataList.forEach((v, idx) => { 290 | this.itemRendererList[idx].data = v; 291 | }); 292 | } 293 | } 294 | 295 | /**延迟一帧标记刷新列表数据 */ 296 | private delayChangeRefreshMark(node: Node) { 297 | let pos = this.posToSize[node['nowDataIdx']]; 298 | //记录数据源索引Node改变后的大小 299 | if (pos) { 300 | if (pos.x == node._uiProps.uiTransformComp.contentSize.x && pos.y == node._uiProps.uiTransformComp.contentSize.y) { 301 | return; 302 | } 303 | pos.x = node._uiProps.uiTransformComp.contentSize.x; 304 | pos.y = node._uiProps.uiTransformComp.contentSize.y; 305 | } else { 306 | pos = node._uiProps.uiTransformComp.contentSize.clone(); 307 | } 308 | console.log("预刷新子项状态===" + node['nowDataIdx']); 309 | this.posToSize[node['nowDataIdx']] = pos; 310 | //延迟一帧刷新 311 | // this.unschedule(this.changeRefreshMark); 312 | // this.scheduleOnce(this.changeRefreshMark, 0); 313 | 314 | //立即刷新 315 | this.refreshMark = true; 316 | this.refreshItem(); 317 | } 318 | 319 | /**标记刷新列表等定时刷新数据 */ 320 | private changeRefreshMark(): void { 321 | // console.log("刷新子项状态"); 322 | this.refreshMark = true; 323 | } 324 | 325 | /**根据数据数量 改变content宽高 */ 326 | private refreshContentSize(): void { 327 | let layout: Layout = this.contentLayout; 328 | let dataListLen: number = this.dataList.length; 329 | let value: number; 330 | switch (this.contentLayout.type) { 331 | case Layout.Type.HORIZONTAL: 332 | value = layout.paddingLeft + layout.paddingRight; 333 | if (this.autoChildrenSize) { 334 | for (let i: number = 0; i < dataListLen; i++) { 335 | value += (this.posToSize[i]?.width ?? this.itemW - this.contentLayout.spacingX) + this.contentLayout.spacingX; 336 | } 337 | } else { 338 | value += dataListLen * this.itemW; 339 | } 340 | 341 | //排列方向从右到左排序的话,scrollview底层会计算content的位置,导致位置不对,content的宽度最小值改为父容器的大小 342 | if (this.contentLayout.horizontalDirection == Layout.HorizontalDirection.RIGHT_TO_LEFT) { 343 | value = Math.max(value, this.content.parent?._uiProps.uiTransformComp.width ?? 0) 344 | } 345 | 346 | this.content._uiProps.uiTransformComp.width = value; 347 | break; 348 | case Layout.Type.VERTICAL: 349 | value = layout.paddingTop + layout.paddingBottom; 350 | if (this.autoChildrenSize) { 351 | for (let i: number = 0; i < dataListLen; i++) { 352 | value += (this.posToSize[i]?.height ?? this.itemH - this.contentLayout.spacingY) + this.contentLayout.spacingY; 353 | } 354 | } else { 355 | value += dataListLen * this.itemH; 356 | } 357 | 358 | //排列方向从下到上排序的话,scrollview底层会计算content的位置,导致位置不对,content的高度最小值改为父容器的大小 359 | if (this.contentLayout.verticalDirection == Layout.VerticalDirection.BOTTOM_TO_TOP) { 360 | value = Math.max(value, this.content.parent?._uiProps.uiTransformComp.height ?? 0) 361 | } 362 | 363 | this.content._uiProps.uiTransformComp.height = value; 364 | break; 365 | case Layout.Type.GRID: 366 | if (this.contentLayout.startAxis == Layout.AxisDirection.HORIZONTAL) { 367 | this.content.getComponent(UITransform).height = layout.paddingTop + Math.ceil(dataListLen / this.horizontalCount) * this.itemH + layout.paddingBottom; 368 | } else if (this.contentLayout.startAxis == Layout.AxisDirection.VERTICAL) { 369 | this.content.getComponent(UITransform).width = layout.paddingLeft + Math.ceil(dataListLen / this.verticalCount) * this.itemW + layout.paddingRight; 370 | } 371 | break; 372 | } 373 | } 374 | 375 | /**刷新预制体位置 和 数据填充 */ 376 | private refreshItem(): void { 377 | this.moveBottom && this.onInfiniteScrollCallback(); 378 | if (!this.refreshMark) { 379 | return; 380 | } 381 | switch (this.contentLayout.type) { 382 | case Layout.Type.HORIZONTAL: 383 | this.refreshHorizontal(); 384 | break; 385 | case Layout.Type.VERTICAL: 386 | this.refreshVertical(); 387 | break; 388 | case Layout.Type.GRID: 389 | this.refreshGrid(); 390 | break; 391 | } 392 | this.refreshContentSize(); 393 | this.refreshMark = false; 394 | this.forcedRefreshMark = false; 395 | } 396 | 397 | /**刷新水平 */ 398 | private refreshHorizontal() { 399 | let start = this.getStart(); 400 | let end = start + this.horizontalCount; 401 | if (end > this.dataList.length) {//超出边界处理 402 | end = this.dataList.length; 403 | start = Math.max(end - this.horizontalCount, 0); 404 | } 405 | 406 | let tempV = 0; 407 | let itemListLen = this.itemList.length; 408 | let item: Node, pos: Vec3, idx; 409 | for (var i = 0; i < itemListLen; i++) { 410 | idx = (start + i) % itemListLen; 411 | item = this.itemList[idx]; 412 | 413 | 414 | pos = item.getPosition(); 415 | tempV = this.getPos(start + i);; 416 | if (pos.x != tempV || this.forcedRefreshMark) { 417 | console.log("修改的数据=" + (start + i)) 418 | pos.x = tempV; 419 | item.position = pos; 420 | this.itemRendererList[idx].node.nowDataIdx = start + i; 421 | this.itemRendererList[idx].data = this.dataList[start + i]; 422 | 423 | //记录位置和node大小 424 | const trans = item._uiProps.uiTransformComp; 425 | this.posToSize[start + i] = trans.contentSize.clone(); 426 | 427 | if (this.infiniteScroll && start > 0 && start + i == this.dataList.length - 1) { 428 | this.moveBottom = true; 429 | } 430 | } 431 | } 432 | } 433 | 434 | /**刷新垂直 */ 435 | private refreshVertical(): void { 436 | let start = this.getStart(); 437 | 438 | let end = start + this.verticalCount; 439 | if (end > this.dataList.length) { 440 | end = this.dataList.length; 441 | start = Math.max(end - this.verticalCount, 0); 442 | } 443 | 444 | let tempV = 0; 445 | let itemListLen = this.itemList.length; 446 | let item: Node, pos: Vec3, idx; 447 | for (var i = 0; i < itemListLen; i++) { 448 | idx = (start + i) % itemListLen; 449 | item = this.itemList[idx]; 450 | pos = item.getPosition(); 451 | tempV = this.getPos(start + i); 452 | if (pos.y != tempV || this.forcedRefreshMark) { 453 | console.log("修改的数据=" + (start + i)); 454 | pos.y = tempV; 455 | item.position = pos; 456 | this.itemRendererList[idx].node.nowDataIdx = start + i; 457 | this.itemRendererList[idx].data = this.dataList[start + i]; 458 | 459 | //记录位置和node大小 460 | const trans = item._uiProps.uiTransformComp; 461 | this.posToSize[start + i] = trans.contentSize.clone(); 462 | if (this.infiniteScroll && start > 0 && start + i == this.dataList.length - 1) { 463 | this.moveBottom = true; 464 | } 465 | } 466 | } 467 | } 468 | 469 | /**刷新网格 */ 470 | private refreshGrid(): void { 471 | //是否垂直方向 添加网格 472 | let isVDirection = this.contentLayout.startAxis == Layout.AxisDirection.VERTICAL; 473 | let start = Math.floor(Math.abs(this.content.position.y) / this.itemH) * this.horizontalCount; 474 | if (isVDirection) { 475 | start = Math.floor(Math.abs(this.content.position.x) / this.itemW) * this.verticalCount; 476 | if (this.content.position.x > 0) { 477 | start = 0; 478 | } 479 | } else if (this.content.position.y < 0) { 480 | start = 0; 481 | } 482 | 483 | if (start < 0) { 484 | start = 0; 485 | } 486 | 487 | let end = start + this.horizontalCount * this.verticalCount; 488 | if (end > this.dataList.length) { 489 | end = this.dataList.length; 490 | start = Math.max(end - this.horizontalCount * this.verticalCount, 0); 491 | } 492 | 493 | let tempX = 0; 494 | let tempY = 0; 495 | let itemListLen = this.itemList.length; 496 | let item: Node, pos: Vec3, idx; 497 | for (var i = 0; i < itemListLen; i++) { 498 | idx = (start + i) % itemListLen; 499 | item = this.itemList[idx]; 500 | pos = item.getPosition(); 501 | if (isVDirection) { 502 | tempX = this.startPos.x + this.direction.x * (Math.floor((start + i) / this.verticalCount)) * this.itemW; 503 | tempY = this.startPos.y + this.direction.y * ((start + i) % this.verticalCount) * this.itemH; 504 | } else { 505 | tempX = this.startPos.x + this.direction.x * ((start + i) % this.horizontalCount) * this.itemW; 506 | tempY = this.startPos.y + this.direction.y * (Math.floor((start + i) / this.horizontalCount)) * this.itemH; 507 | } 508 | 509 | if (pos.y != tempY || pos.x != tempX || this.forcedRefreshMark) { 510 | console.log("修改的数据=" + (start + i)) 511 | pos.x = tempX; 512 | pos.y = tempY; 513 | item.position = pos; 514 | this.itemRendererList[idx].data = this.dataList[start + i]; 515 | if (this.infiniteScroll && start > 0 && start + i == this.dataList.length - 1) { 516 | this.moveBottom = true; 517 | } 518 | } 519 | } 520 | } 521 | 522 | 523 | /**获取开始索引 */ 524 | private getStart(): number { 525 | let start: number = 0; 526 | /**节点高度 */ 527 | let value: number = 0; 528 | switch (this.contentLayout.type) { 529 | case Layout.Type.HORIZONTAL: 530 | if (this.autoChildrenSize) { 531 | value = Math.abs(this.content.position.x); 532 | for (let item in this.posToSize) { 533 | value -= (this.posToSize[item]?.width ?? (this.itemW - this.contentLayout.spacingX)) + this.contentLayout.spacingX; 534 | if (value <= 0) { 535 | break 536 | } else { 537 | start++; 538 | } 539 | } 540 | } else { 541 | start = Math.floor(Math.abs(this.content.position.x) / this.itemW); 542 | } 543 | 544 | //超出边界处理 545 | if (this.contentLayout.horizontalDirection == Layout.HorizontalDirection.LEFT_TO_RIGHT && this.content.position.x > 0 || this.contentLayout.horizontalDirection == Layout.HorizontalDirection.RIGHT_TO_LEFT && this.content.position.x < 0) { 546 | start = 0; 547 | } 548 | break; 549 | case Layout.Type.VERTICAL: 550 | if (this.autoChildrenSize) { 551 | value = Math.abs(this.content.position.y); 552 | for (let item in this.posToSize) { 553 | value -= (this.posToSize[item]?.height ?? (this.itemH - this.contentLayout.spacingY)) + this.contentLayout.spacingY; 554 | if (value <= 0) { 555 | break 556 | } else { 557 | start++; 558 | } 559 | } 560 | } else { 561 | start = Math.floor(Math.abs(this.content.position.y) / this.itemH); 562 | } 563 | 564 | //超出边界处理 565 | if (this.contentLayout.verticalDirection == Layout.VerticalDirection.TOP_TO_BOTTOM && this.content.position.y < 0 || this.contentLayout.verticalDirection == Layout.VerticalDirection.BOTTOM_TO_TOP && this.content.position.y > 0) { 566 | start = 0; 567 | } 568 | break; 569 | case Layout.Type.GRID: 570 | console.error("暂不支持网格布局"); 571 | break; 572 | } 573 | if (start < 0) { 574 | start = 0; 575 | } 576 | return start; 577 | } 578 | 579 | /**根据索引获取位置 */ 580 | private getPos(idx: number): number { 581 | let position: number = 0; 582 | let h: number, w: number; 583 | switch (this.contentLayout.type) { 584 | case Layout.Type.HORIZONTAL: 585 | if (this.autoChildrenSize) { 586 | //重置开始位置 587 | if (idx == 0) { 588 | w = (this.posToSize[0]?.width ?? (this.itemH - this.contentLayout.spacingX)); 589 | this.startPos.x = w * this.anchorPoint.x * this.direction.x; 590 | } 591 | position += this.startPos.x + this.padding.x * this.direction.x; 592 | for (let i: number = 1; i <= idx; i++) { 593 | w = (this.posToSize[i - 1]?.width ?? (this.itemW - this.contentLayout.spacingX)); 594 | position += (w - w * this.anchorPoint.x + this.contentLayout.spacingX) * this.direction.x; 595 | w = (this.posToSize[i]?.width ?? (this.itemW - this.contentLayout.spacingX)) 596 | position += (w * this.anchorPoint.x) * this.direction.x; 597 | } 598 | } else { 599 | position = this.startPos.x + idx * this.itemW * this.direction.x; 600 | } 601 | break; 602 | case Layout.Type.VERTICAL: 603 | if (this.autoChildrenSize) { 604 | //重置开始位置 605 | if (idx == 0) { 606 | h = (this.posToSize[0]?.height ?? (this.itemH - this.contentLayout.spacingY)) 607 | this.startPos.y = (h - h * this.anchorPoint.y) * this.direction.y; 608 | } 609 | position += this.startPos.y + this.padding.y * this.direction.y; 610 | for (let i: number = 1; i <= idx; i++) { 611 | h = (this.posToSize[i - 1]?.height ?? (this.itemH - this.contentLayout.spacingY)) * this.anchorPoint.y + this.contentLayout.spacingY; 612 | position += h * this.direction.y; 613 | h = (this.posToSize[i]?.height ?? (this.itemH - this.contentLayout.spacingY)) 614 | position += (h - h * this.anchorPoint.y) * this.direction.y; 615 | } 616 | } else { 617 | position = this.startPos.y + idx * this.itemH * this.direction.y; 618 | } 619 | break; 620 | case Layout.Type.GRID: 621 | console.error("暂不支持网格布局"); 622 | break; 623 | } 624 | return position; 625 | } 626 | 627 | private posToData: Vec2 = new Vec2(); 628 | /** 629 | * 视图内容将在规定时间内滚动到data数据在视图的位置 630 | * 暂时只支持 HORIZONTAL和VERTICAL 631 | * @param data 数据 632 | * @param timeInSecond 滚动时间(s)。 如果超时,内容将立即跳到数据位置 633 | */ 634 | public scrollToData(data: any, timeInSecond?: number): void { 635 | let idx = this.dataList.indexOf(data); 636 | if (idx != -1) { 637 | this.posToData.x = 0; 638 | this.posToData.y = 1; 639 | switch (this.contentLayout.type) { 640 | case Layout.Type.HORIZONTAL: 641 | this.posToData.x = this.itemW * idx / (this.content._uiProps.uiTransformComp.width - this.node._uiProps.uiTransformComp.width - this.contentLayout.paddingLeft); 642 | break; 643 | case Layout.Type.VERTICAL: 644 | this.posToData.y = 1 - this.itemH * idx / (this.content._uiProps.uiTransformComp.height - this.node._uiProps.uiTransformComp.height - this.contentLayout.paddingTop); 645 | break; 646 | } 647 | this.scrollTo(this.posToData, timeInSecond); 648 | } 649 | } 650 | 651 | protected onDestroy(): void { 652 | this.dataList = null; 653 | this.itemList = null; 654 | this.itemRendererList = null; 655 | this.posToSize = null; 656 | this.itemPool.forEach(item => { 657 | item.destroy(); 658 | }) 659 | this.itemPool = null; 660 | if (this.interval) { 661 | clearInterval(this.interval); 662 | } 663 | this.node.targetOff(this); 664 | } 665 | } 666 | -------------------------------------------------------------------------------- /example/2.4.6/VirtualListProject/assets/Scene/helloworld.fire: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "__type__": "cc.SceneAsset", 4 | "_name": "", 5 | "_objFlags": 0, 6 | "_native": "", 7 | "scene": { 8 | "__id__": 1 9 | } 10 | }, 11 | { 12 | "__type__": "cc.Scene", 13 | "_objFlags": 0, 14 | "_parent": null, 15 | "_children": [ 16 | { 17 | "__id__": 2 18 | } 19 | ], 20 | "_active": false, 21 | "_components": [], 22 | "_prefab": null, 23 | "_opacity": 255, 24 | "_color": { 25 | "__type__": "cc.Color", 26 | "r": 255, 27 | "g": 255, 28 | "b": 255, 29 | "a": 255 30 | }, 31 | "_contentSize": { 32 | "__type__": "cc.Size", 33 | "width": 0, 34 | "height": 0 35 | }, 36 | "_anchorPoint": { 37 | "__type__": "cc.Vec2", 38 | "x": 0, 39 | "y": 0 40 | }, 41 | "_trs": { 42 | "__type__": "TypedArray", 43 | "ctor": "Float64Array", 44 | "array": [ 45 | 0, 46 | 0, 47 | 0, 48 | 0, 49 | 0, 50 | 0, 51 | 1, 52 | 1, 53 | 1, 54 | 1 55 | ] 56 | }, 57 | "_is3DNode": true, 58 | "_groupIndex": 0, 59 | "groupIndex": 0, 60 | "autoReleaseAssets": false, 61 | "_id": "2d2f792f-a40c-49bb-a189-ed176a246e49" 62 | }, 63 | { 64 | "__type__": "cc.Node", 65 | "_name": "Canvas", 66 | "_objFlags": 0, 67 | "_parent": { 68 | "__id__": 1 69 | }, 70 | "_children": [ 71 | { 72 | "__id__": 3 73 | }, 74 | { 75 | "__id__": 5 76 | }, 77 | { 78 | "__id__": 12 79 | }, 80 | { 81 | "__id__": 19 82 | }, 83 | { 84 | "__id__": 26 85 | } 86 | ], 87 | "_active": true, 88 | "_components": [ 89 | { 90 | "__id__": 33 91 | }, 92 | { 93 | "__id__": 34 94 | }, 95 | { 96 | "__id__": 35 97 | } 98 | ], 99 | "_prefab": null, 100 | "_opacity": 255, 101 | "_color": { 102 | "__type__": "cc.Color", 103 | "r": 252, 104 | "g": 252, 105 | "b": 252, 106 | "a": 255 107 | }, 108 | "_contentSize": { 109 | "__type__": "cc.Size", 110 | "width": 1920, 111 | "height": 1080 112 | }, 113 | "_anchorPoint": { 114 | "__type__": "cc.Vec2", 115 | "x": 0.5, 116 | "y": 0.5 117 | }, 118 | "_trs": { 119 | "__type__": "TypedArray", 120 | "ctor": "Float64Array", 121 | "array": [ 122 | 960, 123 | 540, 124 | 0, 125 | 0, 126 | 0, 127 | 0, 128 | 1, 129 | 1, 130 | 1, 131 | 1 132 | ] 133 | }, 134 | "_eulerAngles": { 135 | "__type__": "cc.Vec3", 136 | "x": 0, 137 | "y": 0, 138 | "z": 0 139 | }, 140 | "_skewX": 0, 141 | "_skewY": 0, 142 | "_is3DNode": false, 143 | "_groupIndex": 0, 144 | "groupIndex": 0, 145 | "_id": "a286bbGknJLZpRpxROV6M94" 146 | }, 147 | { 148 | "__type__": "cc.Node", 149 | "_name": "Main Camera", 150 | "_objFlags": 0, 151 | "_parent": { 152 | "__id__": 2 153 | }, 154 | "_children": [], 155 | "_active": true, 156 | "_components": [ 157 | { 158 | "__id__": 4 159 | } 160 | ], 161 | "_prefab": null, 162 | "_opacity": 255, 163 | "_color": { 164 | "__type__": "cc.Color", 165 | "r": 255, 166 | "g": 255, 167 | "b": 255, 168 | "a": 255 169 | }, 170 | "_contentSize": { 171 | "__type__": "cc.Size", 172 | "width": 0, 173 | "height": 0 174 | }, 175 | "_anchorPoint": { 176 | "__type__": "cc.Vec2", 177 | "x": 0.5, 178 | "y": 0.5 179 | }, 180 | "_trs": { 181 | "__type__": "TypedArray", 182 | "ctor": "Float64Array", 183 | "array": [ 184 | 0, 185 | 0, 186 | 0, 187 | 0, 188 | 0, 189 | 0, 190 | 1, 191 | 1, 192 | 1, 193 | 1 194 | ] 195 | }, 196 | "_eulerAngles": { 197 | "__type__": "cc.Vec3", 198 | "x": 0, 199 | "y": 0, 200 | "z": 0 201 | }, 202 | "_skewX": 0, 203 | "_skewY": 0, 204 | "_is3DNode": false, 205 | "_groupIndex": 0, 206 | "groupIndex": 0, 207 | "_id": "40nYx8JlpN57f/wTXHIVbu" 208 | }, 209 | { 210 | "__type__": "cc.Camera", 211 | "_name": "", 212 | "_objFlags": 0, 213 | "node": { 214 | "__id__": 3 215 | }, 216 | "_enabled": true, 217 | "_cullingMask": 4294967295, 218 | "_clearFlags": 7, 219 | "_backgroundColor": { 220 | "__type__": "cc.Color", 221 | "r": 0, 222 | "g": 0, 223 | "b": 0, 224 | "a": 255 225 | }, 226 | "_depth": -1, 227 | "_zoomRatio": 1, 228 | "_targetTexture": null, 229 | "_fov": 60, 230 | "_orthoSize": 10, 231 | "_nearClip": 1, 232 | "_farClip": 4096, 233 | "_ortho": true, 234 | "_rect": { 235 | "__type__": "cc.Rect", 236 | "x": 0, 237 | "y": 0, 238 | "width": 1, 239 | "height": 1 240 | }, 241 | "_renderStages": 1, 242 | "_alignWithScreen": true, 243 | "_id": "9dIRqEDm1Km5kUTGFSkpHG" 244 | }, 245 | { 246 | "__type__": "cc.Node", 247 | "_name": "test1", 248 | "_objFlags": 0, 249 | "_parent": { 250 | "__id__": 2 251 | }, 252 | "_children": [ 253 | { 254 | "__id__": 6 255 | } 256 | ], 257 | "_active": true, 258 | "_components": [ 259 | { 260 | "__id__": 10 261 | }, 262 | { 263 | "__id__": 11 264 | } 265 | ], 266 | "_prefab": null, 267 | "_opacity": 255, 268 | "_color": { 269 | "__type__": "cc.Color", 270 | "r": 150, 271 | "g": 150, 272 | "b": 150, 273 | "a": 255 274 | }, 275 | "_contentSize": { 276 | "__type__": "cc.Size", 277 | "width": 300, 278 | "height": 600 279 | }, 280 | "_anchorPoint": { 281 | "__type__": "cc.Vec2", 282 | "x": 0.5, 283 | "y": 0.5 284 | }, 285 | "_trs": { 286 | "__type__": "TypedArray", 287 | "ctor": "Float64Array", 288 | "array": [ 289 | -793.689, 290 | 105.026, 291 | 0, 292 | 0, 293 | 0, 294 | 0, 295 | 1, 296 | 1, 297 | 1, 298 | 1 299 | ] 300 | }, 301 | "_eulerAngles": { 302 | "__type__": "cc.Vec3", 303 | "x": 0, 304 | "y": 0, 305 | "z": 0 306 | }, 307 | "_skewX": 0, 308 | "_skewY": 0, 309 | "_is3DNode": false, 310 | "_groupIndex": 0, 311 | "groupIndex": 0, 312 | "_id": "c50IjyMbRDGY6FMeVbdlBO" 313 | }, 314 | { 315 | "__type__": "cc.Node", 316 | "_name": "view", 317 | "_objFlags": 0, 318 | "_parent": { 319 | "__id__": 5 320 | }, 321 | "_children": [ 322 | { 323 | "__id__": 7 324 | } 325 | ], 326 | "_active": true, 327 | "_components": [ 328 | { 329 | "__id__": 9 330 | } 331 | ], 332 | "_prefab": null, 333 | "_opacity": 255, 334 | "_color": { 335 | "__type__": "cc.Color", 336 | "r": 255, 337 | "g": 255, 338 | "b": 255, 339 | "a": 255 340 | }, 341 | "_contentSize": { 342 | "__type__": "cc.Size", 343 | "width": 300, 344 | "height": 600 345 | }, 346 | "_anchorPoint": { 347 | "__type__": "cc.Vec2", 348 | "x": 0.5, 349 | "y": 1 350 | }, 351 | "_trs": { 352 | "__type__": "TypedArray", 353 | "ctor": "Float64Array", 354 | "array": [ 355 | 0, 356 | 300, 357 | 0, 358 | 0, 359 | 0, 360 | 0, 361 | 1, 362 | 1, 363 | 1, 364 | 1 365 | ] 366 | }, 367 | "_eulerAngles": { 368 | "__type__": "cc.Vec3", 369 | "x": 0, 370 | "y": 0, 371 | "z": 0 372 | }, 373 | "_skewX": 0, 374 | "_skewY": 0, 375 | "_is3DNode": false, 376 | "_groupIndex": 0, 377 | "groupIndex": 0, 378 | "_id": "depihqKyNFkr3+5ffkBl9P" 379 | }, 380 | { 381 | "__type__": "cc.Node", 382 | "_name": "content", 383 | "_objFlags": 0, 384 | "_parent": { 385 | "__id__": 6 386 | }, 387 | "_children": [], 388 | "_active": true, 389 | "_components": [ 390 | { 391 | "__id__": 8 392 | } 393 | ], 394 | "_prefab": null, 395 | "_opacity": 255, 396 | "_color": { 397 | "__type__": "cc.Color", 398 | "r": 255, 399 | "g": 255, 400 | "b": 255, 401 | "a": 255 402 | }, 403 | "_contentSize": { 404 | "__type__": "cc.Size", 405 | "width": 300, 406 | "height": 130 407 | }, 408 | "_anchorPoint": { 409 | "__type__": "cc.Vec2", 410 | "x": 0.5, 411 | "y": 1 412 | }, 413 | "_trs": { 414 | "__type__": "TypedArray", 415 | "ctor": "Float64Array", 416 | "array": [ 417 | 0, 418 | 0, 419 | 0, 420 | 0, 421 | 0, 422 | 0, 423 | 1, 424 | 1, 425 | 1, 426 | 1 427 | ] 428 | }, 429 | "_eulerAngles": { 430 | "__type__": "cc.Vec3", 431 | "x": 0, 432 | "y": 0, 433 | "z": 0 434 | }, 435 | "_skewX": 0, 436 | "_skewY": 0, 437 | "_is3DNode": false, 438 | "_groupIndex": 0, 439 | "groupIndex": 0, 440 | "_id": "44kyYVS+lJE7P+WLwY1RQc" 441 | }, 442 | { 443 | "__type__": "cc.Layout", 444 | "_name": "", 445 | "_objFlags": 0, 446 | "node": { 447 | "__id__": 7 448 | }, 449 | "_enabled": true, 450 | "_layoutSize": { 451 | "__type__": "cc.Size", 452 | "width": 300, 453 | "height": 130 454 | }, 455 | "_resize": 1, 456 | "_N$layoutType": 2, 457 | "_N$cellSize": { 458 | "__type__": "cc.Size", 459 | "width": 40, 460 | "height": 40 461 | }, 462 | "_N$startAxis": 0, 463 | "_N$paddingLeft": 0, 464 | "_N$paddingRight": 0, 465 | "_N$paddingTop": 10, 466 | "_N$paddingBottom": 20, 467 | "_N$spacingX": 0, 468 | "_N$spacingY": 30, 469 | "_N$verticalDirection": 1, 470 | "_N$horizontalDirection": 0, 471 | "_N$affectedByScale": false, 472 | "_id": "f7ZPps+ZxJEoEAsKqtki08" 473 | }, 474 | { 475 | "__type__": "cc.Mask", 476 | "_name": "", 477 | "_objFlags": 0, 478 | "node": { 479 | "__id__": 6 480 | }, 481 | "_enabled": false, 482 | "_materials": [ 483 | { 484 | "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" 485 | } 486 | ], 487 | "_spriteFrame": null, 488 | "_type": 0, 489 | "_segments": 64, 490 | "_N$alphaThreshold": 0, 491 | "_N$inverted": false, 492 | "_id": "0aO4fnQ+pJvYjo/7XkvW3C" 493 | }, 494 | { 495 | "__type__": "cc.Sprite", 496 | "_name": "", 497 | "_objFlags": 0, 498 | "node": { 499 | "__id__": 5 500 | }, 501 | "_enabled": true, 502 | "_materials": [ 503 | { 504 | "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" 505 | } 506 | ], 507 | "_srcBlendFactor": 770, 508 | "_dstBlendFactor": 771, 509 | "_spriteFrame": { 510 | "__uuid__": "9bbda31e-ad49-43c9-aaf2-f7d9896bac69" 511 | }, 512 | "_type": 1, 513 | "_sizeMode": 0, 514 | "_fillType": 0, 515 | "_fillCenter": { 516 | "__type__": "cc.Vec2", 517 | "x": 0, 518 | "y": 0 519 | }, 520 | "_fillStart": 0, 521 | "_fillRange": 0, 522 | "_isTrimmedMode": true, 523 | "_atlas": null, 524 | "_id": "c0klg1gV5A8oeq8q4KI72d" 525 | }, 526 | { 527 | "__type__": "46ab61aNvNNP6F9EsXIgOa8", 528 | "_name": "", 529 | "_objFlags": 0, 530 | "node": { 531 | "__id__": 5 532 | }, 533 | "_enabled": true, 534 | "horizontal": false, 535 | "vertical": true, 536 | "inertia": true, 537 | "brake": 0.5, 538 | "elastic": true, 539 | "bounceDuration": 1, 540 | "scrollEvents": [], 541 | "cancelInnerEvents": true, 542 | "_N$content": { 543 | "__id__": 7 544 | }, 545 | "content": { 546 | "__id__": 7 547 | }, 548 | "_N$verticalScrollBar": null, 549 | "itemRenderer": { 550 | "__uuid__": "4d3d40d7-266e-49b6-9c68-51c55ca32e2e" 551 | }, 552 | "_id": "4cHPWnYVhEyKB5rPF4Goh8" 553 | }, 554 | { 555 | "__type__": "cc.Node", 556 | "_name": "test2", 557 | "_objFlags": 0, 558 | "_parent": { 559 | "__id__": 2 560 | }, 561 | "_children": [ 562 | { 563 | "__id__": 13 564 | } 565 | ], 566 | "_active": true, 567 | "_components": [ 568 | { 569 | "__id__": 17 570 | }, 571 | { 572 | "__id__": 18 573 | } 574 | ], 575 | "_prefab": null, 576 | "_opacity": 255, 577 | "_color": { 578 | "__type__": "cc.Color", 579 | "r": 150, 580 | "g": 150, 581 | "b": 150, 582 | "a": 255 583 | }, 584 | "_contentSize": { 585 | "__type__": "cc.Size", 586 | "width": 500, 587 | "height": 600 588 | }, 589 | "_anchorPoint": { 590 | "__type__": "cc.Vec2", 591 | "x": 0.5, 592 | "y": 0.5 593 | }, 594 | "_trs": { 595 | "__type__": "TypedArray", 596 | "ctor": "Float64Array", 597 | "array": [ 598 | -335.797, 599 | 105.026, 600 | 0, 601 | 0, 602 | 0, 603 | 0, 604 | 1, 605 | 1, 606 | 1, 607 | 1 608 | ] 609 | }, 610 | "_eulerAngles": { 611 | "__type__": "cc.Vec3", 612 | "x": 0, 613 | "y": 0, 614 | "z": 0 615 | }, 616 | "_skewX": 0, 617 | "_skewY": 0, 618 | "_is3DNode": false, 619 | "_groupIndex": 0, 620 | "groupIndex": 0, 621 | "_id": "5dzZL+EyVH24/z6+9K2g6U" 622 | }, 623 | { 624 | "__type__": "cc.Node", 625 | "_name": "view", 626 | "_objFlags": 0, 627 | "_parent": { 628 | "__id__": 12 629 | }, 630 | "_children": [ 631 | { 632 | "__id__": 14 633 | } 634 | ], 635 | "_active": true, 636 | "_components": [ 637 | { 638 | "__id__": 16 639 | } 640 | ], 641 | "_prefab": null, 642 | "_opacity": 255, 643 | "_color": { 644 | "__type__": "cc.Color", 645 | "r": 255, 646 | "g": 255, 647 | "b": 255, 648 | "a": 255 649 | }, 650 | "_contentSize": { 651 | "__type__": "cc.Size", 652 | "width": 500, 653 | "height": 600 654 | }, 655 | "_anchorPoint": { 656 | "__type__": "cc.Vec2", 657 | "x": 0, 658 | "y": 1 659 | }, 660 | "_trs": { 661 | "__type__": "TypedArray", 662 | "ctor": "Float64Array", 663 | "array": [ 664 | -250, 665 | 300, 666 | 0, 667 | 0, 668 | 0, 669 | 0, 670 | 1, 671 | 1, 672 | 1, 673 | 1 674 | ] 675 | }, 676 | "_eulerAngles": { 677 | "__type__": "cc.Vec3", 678 | "x": 0, 679 | "y": 0, 680 | "z": 0 681 | }, 682 | "_skewX": 0, 683 | "_skewY": 0, 684 | "_is3DNode": false, 685 | "_groupIndex": 0, 686 | "groupIndex": 0, 687 | "_id": "5a5DShp11PIZUlbamPswwR" 688 | }, 689 | { 690 | "__type__": "cc.Node", 691 | "_name": "content", 692 | "_objFlags": 0, 693 | "_parent": { 694 | "__id__": 13 695 | }, 696 | "_children": [], 697 | "_active": true, 698 | "_components": [ 699 | { 700 | "__id__": 15 701 | } 702 | ], 703 | "_prefab": null, 704 | "_opacity": 255, 705 | "_color": { 706 | "__type__": "cc.Color", 707 | "r": 255, 708 | "g": 255, 709 | "b": 255, 710 | "a": 255 711 | }, 712 | "_contentSize": { 713 | "__type__": "cc.Size", 714 | "width": 500, 715 | "height": 0 716 | }, 717 | "_anchorPoint": { 718 | "__type__": "cc.Vec2", 719 | "x": 0, 720 | "y": 1 721 | }, 722 | "_trs": { 723 | "__type__": "TypedArray", 724 | "ctor": "Float64Array", 725 | "array": [ 726 | 0, 727 | 0, 728 | 0, 729 | 0, 730 | 0, 731 | 0, 732 | 1, 733 | 1, 734 | 1, 735 | 1 736 | ] 737 | }, 738 | "_eulerAngles": { 739 | "__type__": "cc.Vec3", 740 | "x": 0, 741 | "y": 0, 742 | "z": 0 743 | }, 744 | "_skewX": 0, 745 | "_skewY": 0, 746 | "_is3DNode": false, 747 | "_groupIndex": 0, 748 | "groupIndex": 0, 749 | "_id": "e6o3Z93cVEKrpjC+L2SUpH" 750 | }, 751 | { 752 | "__type__": "cc.Layout", 753 | "_name": "", 754 | "_objFlags": 0, 755 | "node": { 756 | "__id__": 14 757 | }, 758 | "_enabled": true, 759 | "_layoutSize": { 760 | "__type__": "cc.Size", 761 | "width": 500, 762 | "height": 0 763 | }, 764 | "_resize": 1, 765 | "_N$layoutType": 3, 766 | "_N$cellSize": { 767 | "__type__": "cc.Size", 768 | "width": 40, 769 | "height": 40 770 | }, 771 | "_N$startAxis": 0, 772 | "_N$paddingLeft": 30, 773 | "_N$paddingRight": 10, 774 | "_N$paddingTop": 10, 775 | "_N$paddingBottom": 20, 776 | "_N$spacingX": 60, 777 | "_N$spacingY": 30, 778 | "_N$verticalDirection": 1, 779 | "_N$horizontalDirection": 0, 780 | "_N$affectedByScale": false, 781 | "_id": "f9upLpBRtBxo8s8I98kpvl" 782 | }, 783 | { 784 | "__type__": "cc.Mask", 785 | "_name": "", 786 | "_objFlags": 0, 787 | "node": { 788 | "__id__": 13 789 | }, 790 | "_enabled": false, 791 | "_materials": [ 792 | { 793 | "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" 794 | } 795 | ], 796 | "_spriteFrame": null, 797 | "_type": 0, 798 | "_segments": 64, 799 | "_N$alphaThreshold": 0, 800 | "_N$inverted": false, 801 | "_id": "03qCBqBnRB6LVsePbr1903" 802 | }, 803 | { 804 | "__type__": "cc.Sprite", 805 | "_name": "", 806 | "_objFlags": 0, 807 | "node": { 808 | "__id__": 12 809 | }, 810 | "_enabled": true, 811 | "_materials": [ 812 | { 813 | "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" 814 | } 815 | ], 816 | "_srcBlendFactor": 770, 817 | "_dstBlendFactor": 771, 818 | "_spriteFrame": { 819 | "__uuid__": "9bbda31e-ad49-43c9-aaf2-f7d9896bac69" 820 | }, 821 | "_type": 1, 822 | "_sizeMode": 0, 823 | "_fillType": 0, 824 | "_fillCenter": { 825 | "__type__": "cc.Vec2", 826 | "x": 0, 827 | "y": 0 828 | }, 829 | "_fillStart": 0, 830 | "_fillRange": 0, 831 | "_isTrimmedMode": true, 832 | "_atlas": null, 833 | "_id": "camCdz2LlH3YnVyFwe+A5e" 834 | }, 835 | { 836 | "__type__": "46ab61aNvNNP6F9EsXIgOa8", 837 | "_name": "", 838 | "_objFlags": 0, 839 | "node": { 840 | "__id__": 12 841 | }, 842 | "_enabled": true, 843 | "horizontal": false, 844 | "vertical": true, 845 | "inertia": true, 846 | "brake": 0.5, 847 | "elastic": true, 848 | "bounceDuration": 1, 849 | "scrollEvents": [], 850 | "cancelInnerEvents": true, 851 | "_N$content": { 852 | "__id__": 14 853 | }, 854 | "content": { 855 | "__id__": 14 856 | }, 857 | "_N$verticalScrollBar": null, 858 | "itemRenderer": { 859 | "__uuid__": "4d3d40d7-266e-49b6-9c68-51c55ca32e2e" 860 | }, 861 | "_id": "8f2yuLP5BMcr37YFpkB3N5" 862 | }, 863 | { 864 | "__type__": "cc.Node", 865 | "_name": "test3", 866 | "_objFlags": 0, 867 | "_parent": { 868 | "__id__": 2 869 | }, 870 | "_children": [ 871 | { 872 | "__id__": 20 873 | } 874 | ], 875 | "_active": true, 876 | "_components": [ 877 | { 878 | "__id__": 24 879 | }, 880 | { 881 | "__id__": 25 882 | } 883 | ], 884 | "_prefab": null, 885 | "_opacity": 255, 886 | "_color": { 887 | "__type__": "cc.Color", 888 | "r": 150, 889 | "g": 150, 890 | "b": 150, 891 | "a": 255 892 | }, 893 | "_contentSize": { 894 | "__type__": "cc.Size", 895 | "width": 800, 896 | "height": 300 897 | }, 898 | "_anchorPoint": { 899 | "__type__": "cc.Vec2", 900 | "x": 0.5, 901 | "y": 0.5 902 | }, 903 | "_trs": { 904 | "__type__": "TypedArray", 905 | "ctor": "Float64Array", 906 | "array": [ 907 | 475.406, 908 | 300, 909 | 0, 910 | 0, 911 | 0, 912 | 0, 913 | 1, 914 | 1, 915 | 1, 916 | 1 917 | ] 918 | }, 919 | "_eulerAngles": { 920 | "__type__": "cc.Vec3", 921 | "x": 0, 922 | "y": 0, 923 | "z": 0 924 | }, 925 | "_skewX": 0, 926 | "_skewY": 0, 927 | "_is3DNode": false, 928 | "_groupIndex": 0, 929 | "groupIndex": 0, 930 | "_id": "d2T75+E19Lc7rhbVh5Ga1r" 931 | }, 932 | { 933 | "__type__": "cc.Node", 934 | "_name": "view", 935 | "_objFlags": 0, 936 | "_parent": { 937 | "__id__": 19 938 | }, 939 | "_children": [ 940 | { 941 | "__id__": 21 942 | } 943 | ], 944 | "_active": true, 945 | "_components": [ 946 | { 947 | "__id__": 23 948 | } 949 | ], 950 | "_prefab": null, 951 | "_opacity": 255, 952 | "_color": { 953 | "__type__": "cc.Color", 954 | "r": 255, 955 | "g": 255, 956 | "b": 255, 957 | "a": 255 958 | }, 959 | "_contentSize": { 960 | "__type__": "cc.Size", 961 | "width": 800, 962 | "height": 300 963 | }, 964 | "_anchorPoint": { 965 | "__type__": "cc.Vec2", 966 | "x": 0, 967 | "y": 0.5 968 | }, 969 | "_trs": { 970 | "__type__": "TypedArray", 971 | "ctor": "Float64Array", 972 | "array": [ 973 | -400, 974 | 0, 975 | 0, 976 | 0, 977 | 0, 978 | 0, 979 | 1, 980 | 1, 981 | 1, 982 | 1 983 | ] 984 | }, 985 | "_eulerAngles": { 986 | "__type__": "cc.Vec3", 987 | "x": 0, 988 | "y": 0, 989 | "z": 0 990 | }, 991 | "_skewX": 0, 992 | "_skewY": 0, 993 | "_is3DNode": false, 994 | "_groupIndex": 0, 995 | "groupIndex": 0, 996 | "_id": "55iMxtapFNG7WgmUYUMlEY" 997 | }, 998 | { 999 | "__type__": "cc.Node", 1000 | "_name": "content", 1001 | "_objFlags": 0, 1002 | "_parent": { 1003 | "__id__": 20 1004 | }, 1005 | "_children": [], 1006 | "_active": true, 1007 | "_components": [ 1008 | { 1009 | "__id__": 22 1010 | } 1011 | ], 1012 | "_prefab": null, 1013 | "_opacity": 255, 1014 | "_color": { 1015 | "__type__": "cc.Color", 1016 | "r": 255, 1017 | "g": 255, 1018 | "b": 255, 1019 | "a": 255 1020 | }, 1021 | "_contentSize": { 1022 | "__type__": "cc.Size", 1023 | "width": 10, 1024 | "height": 130 1025 | }, 1026 | "_anchorPoint": { 1027 | "__type__": "cc.Vec2", 1028 | "x": 0, 1029 | "y": 0.5 1030 | }, 1031 | "_trs": { 1032 | "__type__": "TypedArray", 1033 | "ctor": "Float64Array", 1034 | "array": [ 1035 | 0, 1036 | 0, 1037 | 0, 1038 | 0, 1039 | 0, 1040 | 0, 1041 | 1, 1042 | 1, 1043 | 1, 1044 | 1 1045 | ] 1046 | }, 1047 | "_eulerAngles": { 1048 | "__type__": "cc.Vec3", 1049 | "x": 0, 1050 | "y": 0, 1051 | "z": 0 1052 | }, 1053 | "_skewX": 0, 1054 | "_skewY": 0, 1055 | "_is3DNode": false, 1056 | "_groupIndex": 0, 1057 | "groupIndex": 0, 1058 | "_id": "60UdnEHMBFJYhmeBRzClpW" 1059 | }, 1060 | { 1061 | "__type__": "cc.Layout", 1062 | "_name": "", 1063 | "_objFlags": 0, 1064 | "node": { 1065 | "__id__": 21 1066 | }, 1067 | "_enabled": true, 1068 | "_layoutSize": { 1069 | "__type__": "cc.Size", 1070 | "width": 10, 1071 | "height": 130 1072 | }, 1073 | "_resize": 1, 1074 | "_N$layoutType": 1, 1075 | "_N$cellSize": { 1076 | "__type__": "cc.Size", 1077 | "width": 40, 1078 | "height": 40 1079 | }, 1080 | "_N$startAxis": 0, 1081 | "_N$paddingLeft": 20, 1082 | "_N$paddingRight": 20, 1083 | "_N$paddingTop": 10, 1084 | "_N$paddingBottom": 20, 1085 | "_N$spacingX": 30, 1086 | "_N$spacingY": 30, 1087 | "_N$verticalDirection": 1, 1088 | "_N$horizontalDirection": 0, 1089 | "_N$affectedByScale": false, 1090 | "_id": "f7klWnOzZA37237EWfW/B+" 1091 | }, 1092 | { 1093 | "__type__": "cc.Mask", 1094 | "_name": "", 1095 | "_objFlags": 0, 1096 | "node": { 1097 | "__id__": 20 1098 | }, 1099 | "_enabled": false, 1100 | "_materials": [ 1101 | { 1102 | "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" 1103 | } 1104 | ], 1105 | "_spriteFrame": null, 1106 | "_type": 0, 1107 | "_segments": 64, 1108 | "_N$alphaThreshold": 0, 1109 | "_N$inverted": false, 1110 | "_id": "85lTDKkdhKPYwdm60bJ7Bl" 1111 | }, 1112 | { 1113 | "__type__": "cc.Sprite", 1114 | "_name": "", 1115 | "_objFlags": 0, 1116 | "node": { 1117 | "__id__": 19 1118 | }, 1119 | "_enabled": true, 1120 | "_materials": [ 1121 | { 1122 | "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" 1123 | } 1124 | ], 1125 | "_srcBlendFactor": 770, 1126 | "_dstBlendFactor": 771, 1127 | "_spriteFrame": { 1128 | "__uuid__": "9bbda31e-ad49-43c9-aaf2-f7d9896bac69" 1129 | }, 1130 | "_type": 1, 1131 | "_sizeMode": 0, 1132 | "_fillType": 0, 1133 | "_fillCenter": { 1134 | "__type__": "cc.Vec2", 1135 | "x": 0, 1136 | "y": 0 1137 | }, 1138 | "_fillStart": 0, 1139 | "_fillRange": 0, 1140 | "_isTrimmedMode": true, 1141 | "_atlas": null, 1142 | "_id": "68i8AKb/dL7KQHtRCFBkVS" 1143 | }, 1144 | { 1145 | "__type__": "46ab61aNvNNP6F9EsXIgOa8", 1146 | "_name": "", 1147 | "_objFlags": 0, 1148 | "node": { 1149 | "__id__": 19 1150 | }, 1151 | "_enabled": true, 1152 | "horizontal": true, 1153 | "vertical": false, 1154 | "inertia": true, 1155 | "brake": 0.5, 1156 | "elastic": true, 1157 | "bounceDuration": 1, 1158 | "scrollEvents": [], 1159 | "cancelInnerEvents": true, 1160 | "_N$content": { 1161 | "__id__": 21 1162 | }, 1163 | "content": { 1164 | "__id__": 21 1165 | }, 1166 | "_N$verticalScrollBar": null, 1167 | "itemRenderer": { 1168 | "__uuid__": "4d3d40d7-266e-49b6-9c68-51c55ca32e2e" 1169 | }, 1170 | "_id": "02qe2K2qNBqa4UbXC9qA0o" 1171 | }, 1172 | { 1173 | "__type__": "cc.Node", 1174 | "_name": "test4", 1175 | "_objFlags": 0, 1176 | "_parent": { 1177 | "__id__": 2 1178 | }, 1179 | "_children": [ 1180 | { 1181 | "__id__": 27 1182 | } 1183 | ], 1184 | "_active": true, 1185 | "_components": [ 1186 | { 1187 | "__id__": 31 1188 | }, 1189 | { 1190 | "__id__": 32 1191 | } 1192 | ], 1193 | "_prefab": null, 1194 | "_opacity": 255, 1195 | "_color": { 1196 | "__type__": "cc.Color", 1197 | "r": 150, 1198 | "g": 150, 1199 | "b": 150, 1200 | "a": 255 1201 | }, 1202 | "_contentSize": { 1203 | "__type__": "cc.Size", 1204 | "width": 800, 1205 | "height": 500 1206 | }, 1207 | "_anchorPoint": { 1208 | "__type__": "cc.Vec2", 1209 | "x": 0.5, 1210 | "y": 0.5 1211 | }, 1212 | "_trs": { 1213 | "__type__": "TypedArray", 1214 | "ctor": "Float64Array", 1215 | "array": [ 1216 | 475.406, 1217 | -169.582, 1218 | 0, 1219 | 0, 1220 | 0, 1221 | 0, 1222 | 1, 1223 | 1, 1224 | 1, 1225 | 1 1226 | ] 1227 | }, 1228 | "_eulerAngles": { 1229 | "__type__": "cc.Vec3", 1230 | "x": 0, 1231 | "y": 0, 1232 | "z": 0 1233 | }, 1234 | "_skewX": 0, 1235 | "_skewY": 0, 1236 | "_is3DNode": false, 1237 | "_groupIndex": 0, 1238 | "groupIndex": 0, 1239 | "_id": "0bx13dzrhPV5UcGAWpmSfo" 1240 | }, 1241 | { 1242 | "__type__": "cc.Node", 1243 | "_name": "view", 1244 | "_objFlags": 0, 1245 | "_parent": { 1246 | "__id__": 26 1247 | }, 1248 | "_children": [ 1249 | { 1250 | "__id__": 28 1251 | } 1252 | ], 1253 | "_active": true, 1254 | "_components": [ 1255 | { 1256 | "__id__": 30 1257 | } 1258 | ], 1259 | "_prefab": null, 1260 | "_opacity": 255, 1261 | "_color": { 1262 | "__type__": "cc.Color", 1263 | "r": 255, 1264 | "g": 255, 1265 | "b": 255, 1266 | "a": 255 1267 | }, 1268 | "_contentSize": { 1269 | "__type__": "cc.Size", 1270 | "width": 800, 1271 | "height": 500 1272 | }, 1273 | "_anchorPoint": { 1274 | "__type__": "cc.Vec2", 1275 | "x": 0, 1276 | "y": 1 1277 | }, 1278 | "_trs": { 1279 | "__type__": "TypedArray", 1280 | "ctor": "Float64Array", 1281 | "array": [ 1282 | -400, 1283 | 250, 1284 | 0, 1285 | 0, 1286 | 0, 1287 | 0, 1288 | 1, 1289 | 1, 1290 | 1, 1291 | 1 1292 | ] 1293 | }, 1294 | "_eulerAngles": { 1295 | "__type__": "cc.Vec3", 1296 | "x": 0, 1297 | "y": 0, 1298 | "z": 0 1299 | }, 1300 | "_skewX": 0, 1301 | "_skewY": 0, 1302 | "_is3DNode": false, 1303 | "_groupIndex": 0, 1304 | "groupIndex": 0, 1305 | "_id": "64Aq00SspEMbtKkdG9FgkI" 1306 | }, 1307 | { 1308 | "__type__": "cc.Node", 1309 | "_name": "content", 1310 | "_objFlags": 0, 1311 | "_parent": { 1312 | "__id__": 27 1313 | }, 1314 | "_children": [], 1315 | "_active": true, 1316 | "_components": [ 1317 | { 1318 | "__id__": 29 1319 | } 1320 | ], 1321 | "_prefab": null, 1322 | "_opacity": 255, 1323 | "_color": { 1324 | "__type__": "cc.Color", 1325 | "r": 255, 1326 | "g": 255, 1327 | "b": 255, 1328 | "a": 255 1329 | }, 1330 | "_contentSize": { 1331 | "__type__": "cc.Size", 1332 | "width": 150, 1333 | "height": 500 1334 | }, 1335 | "_anchorPoint": { 1336 | "__type__": "cc.Vec2", 1337 | "x": 0, 1338 | "y": 1 1339 | }, 1340 | "_trs": { 1341 | "__type__": "TypedArray", 1342 | "ctor": "Float64Array", 1343 | "array": [ 1344 | 0, 1345 | 0, 1346 | 0, 1347 | 0, 1348 | 0, 1349 | 0, 1350 | 1, 1351 | 1, 1352 | 1, 1353 | 1 1354 | ] 1355 | }, 1356 | "_eulerAngles": { 1357 | "__type__": "cc.Vec3", 1358 | "x": 0, 1359 | "y": 0, 1360 | "z": 0 1361 | }, 1362 | "_skewX": 0, 1363 | "_skewY": 0, 1364 | "_is3DNode": false, 1365 | "_groupIndex": 0, 1366 | "groupIndex": 0, 1367 | "_id": "5aZpk8IztHwKZL5knZMsW8" 1368 | }, 1369 | { 1370 | "__type__": "cc.Layout", 1371 | "_name": "", 1372 | "_objFlags": 0, 1373 | "node": { 1374 | "__id__": 28 1375 | }, 1376 | "_enabled": true, 1377 | "_layoutSize": { 1378 | "__type__": "cc.Size", 1379 | "width": 150, 1380 | "height": 500 1381 | }, 1382 | "_resize": 1, 1383 | "_N$layoutType": 3, 1384 | "_N$cellSize": { 1385 | "__type__": "cc.Size", 1386 | "width": 40, 1387 | "height": 40 1388 | }, 1389 | "_N$startAxis": 1, 1390 | "_N$paddingLeft": 30, 1391 | "_N$paddingRight": 20, 1392 | "_N$paddingTop": 30, 1393 | "_N$paddingBottom": 20, 1394 | "_N$spacingX": 30, 1395 | "_N$spacingY": 60, 1396 | "_N$verticalDirection": 1, 1397 | "_N$horizontalDirection": 0, 1398 | "_N$affectedByScale": false, 1399 | "_id": "3dsxJMz0BDr7obH/fRtcTd" 1400 | }, 1401 | { 1402 | "__type__": "cc.Mask", 1403 | "_name": "", 1404 | "_objFlags": 0, 1405 | "node": { 1406 | "__id__": 27 1407 | }, 1408 | "_enabled": false, 1409 | "_materials": [ 1410 | { 1411 | "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" 1412 | } 1413 | ], 1414 | "_spriteFrame": null, 1415 | "_type": 0, 1416 | "_segments": 64, 1417 | "_N$alphaThreshold": 0, 1418 | "_N$inverted": false, 1419 | "_id": "72JM/IUdFOy7fG9RzledgT" 1420 | }, 1421 | { 1422 | "__type__": "cc.Sprite", 1423 | "_name": "", 1424 | "_objFlags": 0, 1425 | "node": { 1426 | "__id__": 26 1427 | }, 1428 | "_enabled": true, 1429 | "_materials": [ 1430 | { 1431 | "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" 1432 | } 1433 | ], 1434 | "_srcBlendFactor": 770, 1435 | "_dstBlendFactor": 771, 1436 | "_spriteFrame": { 1437 | "__uuid__": "9bbda31e-ad49-43c9-aaf2-f7d9896bac69" 1438 | }, 1439 | "_type": 1, 1440 | "_sizeMode": 0, 1441 | "_fillType": 0, 1442 | "_fillCenter": { 1443 | "__type__": "cc.Vec2", 1444 | "x": 0, 1445 | "y": 0 1446 | }, 1447 | "_fillStart": 0, 1448 | "_fillRange": 0, 1449 | "_isTrimmedMode": true, 1450 | "_atlas": null, 1451 | "_id": "adJhwU96ZINYAsbsdvSniZ" 1452 | }, 1453 | { 1454 | "__type__": "46ab61aNvNNP6F9EsXIgOa8", 1455 | "_name": "", 1456 | "_objFlags": 0, 1457 | "node": { 1458 | "__id__": 26 1459 | }, 1460 | "_enabled": true, 1461 | "horizontal": true, 1462 | "vertical": false, 1463 | "inertia": true, 1464 | "brake": 0.5, 1465 | "elastic": true, 1466 | "bounceDuration": 1, 1467 | "scrollEvents": [], 1468 | "cancelInnerEvents": true, 1469 | "_N$content": { 1470 | "__id__": 28 1471 | }, 1472 | "content": { 1473 | "__id__": 28 1474 | }, 1475 | "_N$verticalScrollBar": null, 1476 | "itemRenderer": { 1477 | "__uuid__": "4d3d40d7-266e-49b6-9c68-51c55ca32e2e" 1478 | }, 1479 | "_id": "7dXk+qh1hIZZWsZpoD1tab" 1480 | }, 1481 | { 1482 | "__type__": "cc.Canvas", 1483 | "_name": "", 1484 | "_objFlags": 0, 1485 | "node": { 1486 | "__id__": 2 1487 | }, 1488 | "_enabled": true, 1489 | "_designResolution": { 1490 | "__type__": "cc.Size", 1491 | "width": 1920, 1492 | "height": 1080 1493 | }, 1494 | "_fitWidth": false, 1495 | "_fitHeight": true, 1496 | "_id": "75ubv+Ls1EM4z8xbyUbYre" 1497 | }, 1498 | { 1499 | "__type__": "cc.Widget", 1500 | "_name": "", 1501 | "_objFlags": 0, 1502 | "node": { 1503 | "__id__": 2 1504 | }, 1505 | "_enabled": true, 1506 | "alignMode": 1, 1507 | "_target": null, 1508 | "_alignFlags": 45, 1509 | "_left": 0, 1510 | "_right": 0, 1511 | "_top": 0, 1512 | "_bottom": 0, 1513 | "_verticalCenter": 0, 1514 | "_horizontalCenter": 0, 1515 | "_isAbsLeft": true, 1516 | "_isAbsRight": true, 1517 | "_isAbsTop": true, 1518 | "_isAbsBottom": true, 1519 | "_isAbsHorizontalCenter": true, 1520 | "_isAbsVerticalCenter": true, 1521 | "_originalWidth": 0, 1522 | "_originalHeight": 0, 1523 | "_id": "c6rFy0kJZBqqZL/B3atwcp" 1524 | }, 1525 | { 1526 | "__type__": "b3da81OXatGPqsJM3oI7A1A", 1527 | "_name": "", 1528 | "_objFlags": 0, 1529 | "node": { 1530 | "__id__": 2 1531 | }, 1532 | "_enabled": true, 1533 | "test1": { 1534 | "__id__": 11 1535 | }, 1536 | "test2": { 1537 | "__id__": 18 1538 | }, 1539 | "test3": { 1540 | "__id__": 25 1541 | }, 1542 | "test4": { 1543 | "__id__": 32 1544 | }, 1545 | "_id": "eegd9e+axMN6UPZ+f5VDt+" 1546 | } 1547 | ] --------------------------------------------------------------------------------