├── .gitignore ├── LICENSE ├── README.md ├── doc ├── declarations │ └── yx-collection-view.d.ts ├── imgs │ ├── editor-panel.png │ └── title.png └── md │ ├── layout.md │ ├── table-layout-1.md │ ├── table-layout-2.md │ ├── table-layout-3.md │ ├── table-layout-4.md │ ├── table-layout-5.md │ └── table-layout-6.md ├── list-2x ├── .gitignore ├── assets │ ├── common-cells.meta │ ├── common-cells │ │ ├── shape-label-cell.prefab │ │ └── shape-label-cell.prefab.meta │ ├── home.meta │ ├── home │ │ ├── home.fire │ │ ├── home.fire.meta │ │ ├── home.ts │ │ └── home.ts.meta │ ├── lib.meta │ └── lib │ │ ├── yx-collection-view.ts │ │ ├── yx-collection-view.ts.meta │ │ ├── yx-table-layout.ts │ │ └── yx-table-layout.ts.meta ├── creator.d.ts ├── jsconfig.json ├── project.json ├── settings │ ├── project.json │ └── services.json └── tsconfig.json └── list-3x ├── .creator ├── asset-template │ └── typescript │ │ └── Custom Script Template Help Documentation.url └── default-meta.json ├── .gitignore ├── assets ├── common-cells.meta ├── common-cells │ ├── shape-label-cell.prefab │ └── shape-label-cell.prefab.meta ├── home.meta ├── home │ ├── home.scene │ ├── home.scene.meta │ ├── home.ts │ └── home.ts.meta ├── lib.meta └── lib │ ├── grid-layout.ts │ ├── grid-layout.ts.meta │ ├── yx-collection-view.ts │ ├── yx-collection-view.ts.meta │ ├── yx-table-layout.ts │ └── yx-table-layout.ts.meta ├── package.json ├── settings └── v2 │ └── packages │ ├── builder.json │ ├── cocos-service.json │ ├── device.json │ ├── engine.json │ ├── information.json │ ├── program.json │ └── project.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | *.DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 568071718@qq.com 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![title.png](./doc/imgs/title.png) 2 | 3 | --- 4 | 5 | ## 开发环境 6 | * 2.x 7 | 引擎版本:Cocos Creator **2.4.13** 8 | 编程语言:TypeScript 9 | * 3.x 10 | 引擎版本:Cocos Creator **3.8.0** 11 | 编程语言:TypeScript 12 | 13 | ## 基本特性 14 | 15 | * 节点回收复用(虚拟列表模式) 16 | * 分帧预加载节点(非虚拟列表模式) 17 | * 多种 cell 节点类型 18 | * 列表嵌套 19 | * 分区概念 20 | * supplementary 补充视图概念 21 | * 多种 supplementary 节点类型 22 | * [布局解耦(组件核心)](./doc/md/layout.md) 23 | 24 | ## table-layout 25 | 26 | * 仿 TableView 样式,仅支持垂直方向排列 27 | * 支持设置不同的行高 28 | * 支持分区模式 29 | * 支持添加区头/区尾 30 | * 支持区头/区尾悬浮吸附效果 31 | * [在线演示](https://568071718.github.io/cocos-creator-build/collection-view/table-layout/) 32 | 33 | ## 使用 34 | 35 | ```ts 36 | listComp.numberOfItems = () => 10000 37 | listComp.cellForItemAt = (indexPath, collectionView) => { 38 | const cell = collectionView.dequeueReusableCell(`cell`) 39 | cell.getChildByName('label').getComponent(Label).string = `${indexPath}` 40 | return cell 41 | } 42 | 43 | let layout = new YXTableLayout() 44 | layout.spacing = 20 45 | layout.rowHeight = 100 46 | listComp.layout = layout 47 | 48 | listComp.reloadData() 49 | ``` 50 | 51 | ## 更多接口 52 | 53 | * 内部 ScrollView 组件 54 | ```ts 55 | let isScrolling = this.listComp.scrollView.isScrolling() 56 | let isAutoScrolling = this.listComp.scrollView.isAutoScrolling() 57 | this.listComp.scrollView.brake = 0.8 58 | this.listComp.scrollView.bounceDuration = 0.25 59 | this.listComp.scrollView.scrollToOffset(new math.Vec2(0, 200)) 60 | // ... 可以直接使用更多 ScrollView 属性或者方法 61 | ``` 62 | 63 | * 开启分区 64 | ```ts 65 | // 注意: 分区需要自定义 YXLayout 支持 66 | this.listComp.numberOfSections = () => 2 // 设置列表分 2 个区排列 67 | this.listComp.numberOfItems = (section, collectionView) => { 68 | if (section == 0) { 69 | return 10 // 第 1 个区返回 10 条数据 70 | } 71 | if (section == 1) { 72 | return 20 // 第 2 个区返回 20 条数据 73 | } 74 | return 0 // 默认情况 75 | } 76 | ``` 77 | 78 | * 节点显示状态回调 79 | ```ts 80 | this.listComp.onCellDisplay = (cell, indexPath, collectionView) => { 81 | log(`onCellDisplay: ${indexPath}`) 82 | } 83 | this.listComp.onCellEndDisplay = (cell, indexPath, collectionView) => { 84 | log(`onCellEndDisplay: ${indexPath}`) 85 | } 86 | ``` 87 | 88 | * 滚动至指定位置 89 | ```ts 90 | let indexPath = new YXIndexPath(0, 2) // 要滚动到的节点索引 91 | this.listComp.scrollTo(indexPath) 92 | ``` 93 | 94 | * 预加载相关接口 95 | ```ts 96 | this.listComp.preloadNodesLimitPerFrame = 2 // 每帧加载多少个节点 97 | this.listComp.preloadProgress = (current, total) => { 98 | log(`加载进度: ${current}/${total}`) 99 | } 100 | ``` 101 | 102 | ## 相关链接 103 | * [Github](https://github.com/568071718/creator-collection-view) 104 | * [Gitee](https://gitee.com/568071718/creator-collection-view) 105 | * [查看声明文件](./doc/declarations/yx-collection-view.d.ts) 106 | * [旧版文档](https://gitee.com/568071718/creator-collection-view-doc) 107 | 108 | 109 | -------------------------------------------------------------------------------- /doc/declarations/yx-collection-view.d.ts: -------------------------------------------------------------------------------- 1 | import { Component, math, Node, ScrollView } from 'cc'; 2 | /** 3 | * 定义列表的滚动方向 4 | */ 5 | declare enum _yx_collection_view_scroll_direction { 6 | /** 7 | * 水平滚动 8 | */ 9 | HORIZONTAL = 0, 10 | /** 11 | * 垂直滚动 12 | */ 13 | VERTICAL = 1 14 | } 15 | /** 16 | * 列表节点加载模式 17 | */ 18 | declare enum _yx_collection_view_list_mode { 19 | /** 20 | * 根据列表显示范围加载需要的节点,同类型的节点会进行复用 21 | * 优点: 控制总节点数量,不会创建大量节点 22 | * 缺点: 因为有复用逻辑,节点内容会频繁更新,cell 更新业务比较重的话列表会抖动,例如 Label (NONE) 很多的节点 23 | */ 24 | RECYCLE = 0, 25 | /** 26 | * 直接预加载所有的节点,处于列表显示范围外的节点透明化处理 27 | * 优点: 避免 cell 频繁更新,优化大量 Label (NONE) 场景下的卡顿问题 28 | * 缺点: 会实例化所有节点,并非真正的虚拟列表,仅仅是把显示范围外的节点透明了,如果列表数据量很大仍然会卡 29 | */ 30 | PRELOAD = 1 31 | } 32 | /** 33 | * 表示索引的对象 34 | */ 35 | export declare class YXIndexPath { 36 | static ZERO: Readonly; 37 | /** 38 | * 区索引 39 | */ 40 | get section(): number; 41 | /** 42 | * 单元格在区内的位置 43 | */ 44 | get item(): number; 45 | /** 46 | * item 别名 47 | */ 48 | get row(): number; 49 | constructor(section: number, item: number); 50 | clone(): YXIndexPath; 51 | equals(other: YXIndexPath): boolean; 52 | toString(): string; 53 | } 54 | /** 55 | * 表示边距的对象 56 | */ 57 | export declare class YXEdgeInsets { 58 | static ZERO: Readonly; 59 | top: number; 60 | left: number; 61 | bottom: number; 62 | right: number; 63 | constructor(top: number, left: number, bottom: number, right: number); 64 | clone(): YXEdgeInsets; 65 | equals(other: YXEdgeInsets): boolean; 66 | set(other: YXEdgeInsets): void; 67 | toString(): string; 68 | } 69 | /** 70 | * 节点的布局属性 71 | */ 72 | export declare class YXLayoutAttributes { 73 | /** 74 | * 创建一个 cell 布局属性实例 75 | */ 76 | static layoutAttributesForCell(indexPath: YXIndexPath): YXLayoutAttributes; 77 | /** 78 | * 创建一个 supplementary 布局属性实例 79 | * @param kinds 自定义类别标识,更多说明查看 supplementaryKinds 80 | */ 81 | static layoutAttributesForSupplementary(indexPath: YXIndexPath, kinds: string): YXLayoutAttributes; 82 | /** 83 | * 构造方法,外部禁止直接访问,需要通过上面的静态方法创建对象 84 | */ 85 | protected constructor(); 86 | /** 87 | * 节点索引 88 | */ 89 | get indexPath(): YXIndexPath; 90 | /** 91 | * 节点种类 92 | */ 93 | get elementCategory(): "Cell" | "Supplementary"; 94 | /** 95 | * Supplementary 种类,本身无实际意义,具体作用由自定义布局规则决定 96 | */ 97 | get supplementaryKinds(): string; 98 | /** 99 | * 节点在滚动视图中的位置和大小属性 100 | * origin 属性表示节点在父视图坐标系中的左上角的位置,size 属性表示节点的宽度和高度 101 | */ 102 | get frame(): math.Rect; 103 | /** 104 | * 节点层级 105 | * 越小会越早的添加到滚动视图上 106 | * https://docs.cocos.com/creator/manual/zh/ui-system/components/editor/ui-transform.html?h=uitrans 107 | * 备注: 内部暂时是通过节点的 siblingIndex 实现的,如果自定义 layout 有修改这个值的需求,需要重写 layout 的 @shouldUpdateAttributesZIndex 方法,默认情况下会忽略这个配置 108 | */ 109 | zIndex: number; 110 | /** 111 | * 节点透明度 112 | * 备注: 内部通过 UIOpacity 组件实现,会修改节点 UIOpacity 组件的 opacity 值,如果自定义 layout 有修改这个值的需求,需要重写 layout 的 @shouldUpdateAttributesOpacity 方法,默认情况下会忽略这个配置 113 | */ 114 | opacity: number; 115 | /** 116 | * 节点变换 - 缩放 117 | */ 118 | scale: math.Vec3; 119 | /** 120 | * 节点变换 - 平移 121 | */ 122 | offset: math.Vec3; 123 | /** 124 | * 节点变换 - 旋转 125 | * 备注: 3D 变换似乎需要透视相机??? 126 | */ 127 | eulerAngles: math.Vec3; 128 | } 129 | /** 130 | * 布局规则 131 | * 这里只是约定出了一套接口,内部只是一些基础实现,具体布局方案通过子类重载去实现 132 | */ 133 | export declare abstract class YXLayout { 134 | constructor(); 135 | /** 136 | * @required 137 | * 整个滚动区域大小 138 | * 需要在 prepare 内初始化 139 | */ 140 | contentSize: math.Size; 141 | /** 142 | * @required 143 | * 所有元素的布局属性 144 | * 需要在 prepare 内初始化 145 | * @todo 这个不应该限制为数组结构,准确来说是不应该限制开发者必须使用数组来保存所有布局属性,目前为了实现预加载模式暂时是必须要求数组结构,后续有好的方案的话应该考虑优化 146 | */ 147 | attributes: YXLayoutAttributes[]; 148 | /** 149 | * @required 150 | * 子类重写实现布局方案 151 | * 注意: 必须初始化滚动区域大小并赋值给 contentSize 属性 152 | * 注意: 必须初始化所有的元素布局属性,并保存到 attributes 数组 153 | * 可选: 根据 collectionView 的 scrollDirection 支持不同的滚动方向 154 | */ 155 | abstract prepare(collectionView: YXCollectionView): void; 156 | /** 157 | * @optional 158 | * 列表在首次更新数据后会执行这个方法 159 | * 在这个方法里设置滚动视图的初始偏移量 160 | * 161 | * @example 162 | * // 比如一个垂直列表希望初始化时从最顶部开始展示数据,那么可以在这个方法里通过 scrollToTop 实现 163 | * initOffset(collectionView: YXCollectionView): void { 164 | * collectionView.scrollView.scrollToTop() 165 | * } 166 | */ 167 | initOffset(collectionView: YXCollectionView): void; 168 | /** 169 | * @optional 170 | * 当一次手势拖动结束后会立即调用此方法,通过重写这个方法可以定制列表最终停留的位置 171 | * 172 | * @param collectionView 列表组件 173 | * @param touchMoveVelocity 手势速度 174 | * @param startOffset 此次手势开始时列表的偏移位置 175 | * @param originTargetOffset 接下来将要自动滚动到的位置 176 | * @param originScrollDuration 接下来的惯性滚动持续时间 177 | * @returns 可以返回 null ,返回 null 执行默认的惯性滚动逻辑 178 | * 179 | * 另外关于返回值的字段说明 180 | * @param offset 这个字段表示列表本次滚动结束时期望停留的位置,一旦返回了这个字段,列表最终将会停留至返回的这个位置 181 | * @param time 可选,默认为 originScrollDuration,这个字段表示自动滚动至期望停留位置需要的时间 182 | * @param attenuated 可选,默认为 true,这个字段表示惯性滚动速度是否衰减 183 | */ 184 | targetOffset(collectionView: YXCollectionView, touchMoveVelocity: math.Vec3, startOffset: math.Vec2, originTargetOffset: math.Vec2, originScrollDuration: number): { 185 | offset: math.Vec2; 186 | time?: number; 187 | attenuated?: boolean; 188 | } | null; 189 | /** 190 | * @optional 191 | * 列表每次滚动结束后会调用此方法 192 | */ 193 | onScrollEnded(collectionView: YXCollectionView): void; 194 | /** 195 | * @optional 196 | * 当滚动视图的可见范围变化后执行,这个方法会在列表滚动过程中频繁的执行 197 | * 在这个方法里可以调整节点属性以实现交互性的节点变换效果,(如果在这个方法里调整了节点变换属性,需要重写 shouldUpdateAttributesForBoundsChange 以支持实时变换) 198 | * 199 | * @param rect 当前滚动视图的可见区域 200 | * 201 | * @returns 202 | * 关于这个方法的返回值,最优的情况应该是根据实际的布局情况计算出当前显示区域内需要显示的所有布局属性 203 | * 列表在更新可见节点时会遍历这个方法返回的数组并依次检查节点是否需要添加到列表内,默认这个方法是直接返回所有的布局属性,也就是在更新可见节点时的时间复杂度默认为 O(attributes.length),除非有更优的算法,否则建议直接返回所有的布局属性 204 | */ 205 | layoutAttributesForElementsInRect(rect: math.Rect, collectionView: YXCollectionView): YXLayoutAttributes[]; 206 | layoutAttributesForItemAtIndexPath(indexPath: YXIndexPath, collectionView: YXCollectionView): YXLayoutAttributes; 207 | layoutAttributesForSupplementaryAtIndexPath(indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string): YXLayoutAttributes; 208 | /** 209 | * @optional 210 | * 列表组件在调用 scrollTo 方法时会触发这个方法,如果实现了这个方法,最终的滚动停止位置以这个方法返回的为准 211 | */ 212 | scrollTo(indexPath: YXIndexPath, collectionView: YXCollectionView): math.Vec2; 213 | /** 214 | * @optional 215 | * @see YXLayoutAttributes.zIndex 216 | */ 217 | shouldUpdateAttributesZIndex(): boolean; 218 | /** 219 | * @optional 220 | * @see YXLayoutAttributes.opacity 221 | */ 222 | shouldUpdateAttributesOpacity(): boolean; 223 | /** 224 | * @optional 225 | * 此布局下的节点,是否需要实时更新变换效果 226 | * @returns 返回 true 会忽略 YXCollectionView 的 frameInterval 设置,强制在滚动过程中实时更新节点 227 | */ 228 | shouldUpdateAttributesForBoundsChange(): boolean; 229 | /** 230 | * @optional 231 | * 列表组件销毁时执行 232 | */ 233 | onDestroy(): void; 234 | } 235 | /** 236 | * @see NodePool.poolHandlerComp 237 | * 节点的自定义组件可以通过这个接口跟 NodePool 的重用业务关联起来 238 | */ 239 | export interface YXCollectionViewCell extends Component { 240 | unuse(): void; 241 | reuse(args: any): void; 242 | } 243 | /** 244 | * 列表组件 245 | */ 246 | export declare class YXCollectionView extends Component { 247 | /** 248 | * 访问定义的私有枚举 249 | */ 250 | static ScrollDirection: typeof _yx_collection_view_scroll_direction; 251 | static Mode: typeof _yx_collection_view_list_mode; 252 | /** 253 | * 滚动视图组件 254 | */ 255 | get scrollView(): ScrollView; 256 | /** 257 | * 允许手势滚动 258 | */ 259 | scrollEnabled: boolean; 260 | /** 261 | * 允许鼠标滑轮滚动 262 | */ 263 | wheelScrollEnabled: boolean; 264 | /** 265 | * 列表滚动方向,默认垂直方向滚动 266 | * 自定义 YXLayout 应该尽量根据这个配置来实现不同方向的布局业务 267 | * 备注: 如果使用的 YXLayout 未支持对应的滚动方向,则此配置不会生效,严格来说这个滚动方向本就应该是由 YXLayout 决定的,定义在这里是为了编辑器配置方便 268 | */ 269 | scrollDirection: YXCollectionView.ScrollDirection; 270 | /** 271 | * 列表单元节点加载模式 272 | */ 273 | mode: YXCollectionView.Mode; 274 | /** 275 | * 预加载模式下每帧加载多少个节点 276 | */ 277 | preloadNodesLimitPerFrame: number; 278 | /** 279 | * 预加载进度 280 | */ 281 | preloadProgress: (current: number, total: number) => void; 282 | /** 283 | * 每多少帧刷新一次可见节点,1 表示每帧都刷 284 | */ 285 | frameInterval: number; 286 | /** 287 | * 滚动过程中,每多少帧回收一次不可见节点,1表示每帧都回收,0表示不在滚动过程中回收不可见节点 288 | * @bug 滚动过程中如果实时的回收不可见节点,有时候会收不到 scroll view 的 cancel 事件,导致 scroll view 的滚动状态不会更新 (且收不到滚动结束事件) 289 | * @fix 当这个属性设置为 0 时,只会在 `touch-up` 和 `scroll-ended` 里面回收不可见节点 290 | */ 291 | recycleInterval: number; 292 | /** 293 | * 注册 cell 294 | * 可多次注册不同种类的 cell,只要确保 identifier 的唯一性就好 295 | * @param identifier cell 标识符,通过 dequeueReusableCell 获取重用 cell 时,会根据这个值匹配 296 | * @param maker 生成节点,当重用池里没有可用的节点时,会通过这个回调获取节点,需要在这个回调里面生成节点 297 | * @param poolComp (可选) 节点自定义组件,可以通过这个组件跟 NodePool 的重用业务关联起来 298 | */ 299 | registerCell(identifier: string, maker: () => Node, poolComp?: (new (...args: any[]) => YXCollectionViewCell) | string | null): void; 300 | /** 301 | * 注册 supplementary 追加视图,用法跟 registerCell 一样 302 | */ 303 | registerSupplementary(identifier: string, maker: () => Node, poolComp?: (new (...args: any[]) => YXCollectionViewCell) | string | null): void; 304 | /** 305 | * 通过标识符从重用池里取出一个可用的 cell 节点 306 | * @param identifier 注册时候的标识符 307 | */ 308 | dequeueReusableCell(identifier: string): Node; 309 | /** 310 | * 通过标识符从重用池里取出一个可用的 supplementary 节点 311 | * @param identifier 注册时候的标识符 312 | */ 313 | dequeueReusableSupplementary(identifier: string): Node; 314 | /** 315 | * 内容要分几个区展示,默认 1 316 | * 没有分区展示的需求可以不管这个配置 317 | */ 318 | numberOfSections: number | ((collectionView: YXCollectionView) => number); 319 | getNumberOfSections(): number; 320 | /** 321 | * 每个区里要展示多少条内容 322 | */ 323 | numberOfItems: number | ((section: number, collectionView: YXCollectionView) => number); 324 | getNumberOfItems(section: number): number; 325 | /** 326 | * 配置每块内容对应的 UI 节点 327 | * 在这个方法里,需要确定 indexPath 这个位置对应的节点应该是用注册过的哪个类型的 Node 节点,然后通过 dequeueReusableCell 生成对应的 Node 328 | * 329 | * @example 330 | * yourList.cellForItemAt = (indexPath ,collectionView) => { 331 | * let cell = collectionView.dequeueReusableCell(`your identifier`) 332 | * let comp = cell.getComponent(YourCellComp) 333 | * comp.label.string = `${indexPath}` 334 | * return cell 335 | * } 336 | * 337 | * @returns 注意: 不要在这个方法里创建新的节点对象,这个方法返回的 Node,必须是通过 dequeueReusableCell 匹配到的 Node 338 | */ 339 | cellForItemAt: (indexPath: YXIndexPath, collectionView: YXCollectionView) => Node; 340 | /** 341 | * 用法跟 cellForItemAt 差不多,此方法内需要通过 dequeueReusableSupplementary 获取 Node 节点 342 | * @param kinds 关于这个字段的具体含义应该根据使用的自定义 layout 决定 343 | */ 344 | supplementaryForItemAt: (indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string) => Node; 345 | /** 346 | * cell 节点可见状态回调 347 | * 如果同类型的节点大小可能不一样,可以在这里调整子节点的位置 348 | */ 349 | onCellDisplay: (node: Node, indexPath: YXIndexPath, collectionView: YXCollectionView) => void; 350 | onCellEndDisplay: (node: Node, indexPath: YXIndexPath, collectionView: YXCollectionView) => void; 351 | /** 352 | * supplementary 节点可见状态回调 353 | */ 354 | onSupplementaryDisplay: (node: Node, indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string) => void; 355 | onSupplementaryEndDisplay: (node: Node, indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string) => void; 356 | /** 357 | * 点击到 cell 节点后执行 358 | */ 359 | onTouchCellAt: (indexPath: YXIndexPath, collectionView: YXCollectionView) => void; 360 | /** 361 | * 点击到 supplementary 节点后执行 362 | */ 363 | onTouchSupplementaryAt: (indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string) => void; 364 | /** 365 | * 布局规则 366 | */ 367 | layout: YXLayout; 368 | /** 369 | * 获取列表当前的可见范围 370 | */ 371 | getVisibleRect(): math.Rect; 372 | /** 373 | * 通过索引获取指定的可见的 cell 节点 374 | */ 375 | getVisibleCellNode(indexPath: YXIndexPath): Node; 376 | /** 377 | * 通过索引获取指定的可见的 supplementary 节点 378 | */ 379 | getVisibleSupplementaryNode(indexPath: YXIndexPath, kinds: string): Node; 380 | /** 381 | * 获取所有正在显示的 cell 节点 382 | */ 383 | getVisibleCellNodes(): Node[]; 384 | /** 385 | * 获取所有正在显示的 supplementary 节点 386 | * @param kinds 可选按种类筛选 387 | */ 388 | getVisibleSupplementaryNodes(kinds?: string): Node[]; 389 | /** 390 | * 获取指定节点绑定的布局属性对象 391 | */ 392 | getElementAttributes(node: Node): YXLayoutAttributes; 393 | /** 394 | * 刷新列表数据 395 | */ 396 | reloadData(): void; 397 | /** 398 | * 刷新当前可见节点 399 | * @param force true: 立即刷新; false: 根据设置的刷新帧间隔在合适的时候刷新 400 | */ 401 | markForUpdateVisibleData(force?: boolean): void; 402 | /** 403 | * 滚动到指定节点的位置 404 | * @todo 支持偏移方位,目前固定是按顶部的位置的,有特殊需求的建议直接通过 .scrollView.scrollToOffset() 实现 405 | */ 406 | scrollTo(indexPath: YXIndexPath, timeInSecond?: number, attenuated?: boolean): void; 407 | /** 408 | * 生命周期方法 409 | */ 410 | protected onLoad(): void; 411 | protected onDestroy(): void; 412 | protected update(dt: number): void; 413 | } 414 | export declare namespace YXCollectionView { 415 | /** 416 | * 重定义私有类型 417 | */ 418 | type ScrollDirection = _yx_collection_view_scroll_direction; 419 | type Mode = _yx_collection_view_list_mode; 420 | } 421 | /** 422 | * ***************************************************************************************** 423 | * ***************************************************************************************** 424 | * 把二分查找的规则抽出来封装一下,继承这个类的布局,默认通过二分查找实现查找业务 425 | * 这种查找规则对数据量很大的有序列表来说相对高效,具体是否使用还是要根据实际排列需求决定 426 | * ***************************************************************************************** 427 | * ***************************************************************************************** 428 | * 429 | * @deprecated 1.4.0 版本开始,在自定义布局规则的时候暂时不建议继承这个规则了,如何优化查找算法应该全靠开发者根据实际需求自行实现,目前保留这个是为了 flow-layout 使用,后续有更优方案的话可能会删除这部分代码 430 | */ 431 | export declare abstract class YXBinaryLayout extends YXLayout { 432 | /** 433 | * @bug 如果节点大小差距很大,可能会导致计算屏幕内节点时不准确,出现节点不被正确添加到滚动视图上的问题 434 | * @fix 可以通过此属性,追加屏幕显示的节点数量 435 | * 设置这个值会在检查是否可见的节点时,尝试检查更多的可能处于屏幕外的节点,具体设置多少要根据实际情况调试,一般如果都是正常大小的节点,不需要考虑这个配置 436 | * 设置负值会检查所有的节点 437 | */ 438 | extraVisibleCount: number; 439 | layoutAttributesForElementsInRect(rect: math.Rect, collectionView: YXCollectionView): YXLayoutAttributes[]; 440 | } 441 | export {}; 442 | -------------------------------------------------------------------------------- /doc/imgs/editor-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/568071718/creator-collection-view/bfcb19293ab8340c9c9863374d764034dabb3fc0/doc/imgs/editor-panel.png -------------------------------------------------------------------------------- /doc/imgs/title.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/568071718/creator-collection-view/bfcb19293ab8340c9c9863374d764034dabb3fc0/doc/imgs/title.png -------------------------------------------------------------------------------- /doc/md/layout.md: -------------------------------------------------------------------------------- 1 | 2 | YXCollectionView 的 layout 属性决定了单元节点在屏幕上的排列方式。 3 | 4 | 通过 YXLayout 作为布局管理器,YXCollectionView 将所有的布局和展示逻辑交给了 YXLayout 来处理。也就是说,YXCollectionView 本身不负责具体的布局实现,而是通过将布局职责委托给 YXLayout 来实现布局的完全解耦。 5 | 6 | 这一设计的最大优势是布局的独立性:你可以针对不同的需求实现不同的布局样式,无论是 TableView、网格布局,还是其他任意排列方式,都可以通过自定义 YXLayout 来实现。 7 | 8 | 每种布局都可以独立封装,不同布局之间相互隔离,互不依赖。使用时只需引入对应的布局规则,极大地提高了灵活性和可重用性。而且,由于布局规则是独立设计的,它们还可以很方便地被导出并分享给其他开发者使用。 9 | 10 | --- 11 | 12 | ## YXIndexPath 13 | 14 | YXIndexPath 代表了索引,常见的列表组件里数据索引一般来说都是直接用整形来定义的 (例如 index: number),但是因为 YXCollectionView 有分区的概念,所以是封装了一个对象用来表示节点的位置索引,通俗的来说,YXIndexPath 表示的是第 section 个区里的第 item 个节点,这个 item 就可以看做是 index,只不过表示的是在某个区内的位置 15 | 16 | ## YXLayoutAttributes 17 | 18 | YXLayoutAttributes 用来描述节点的 UI 相关的信息,在自定义布局的时候,需要对应的创建多个 YXLayoutAttributes 对象来描述节点的 UI 信息,假如说列表一共需要展示 100 条内容,那就是需要创建 100 个 YXLayoutAttributes 对象来描述这 100 个节点的位置,YXLayoutAttributes 通过关键属性 indexPath 记录这个布局对象对应的是哪个节点,通过 frame 属性记录这个节点实际的 UI 位置,总结来说 YXLayoutAttributes 就是它**表示了第 indexPath 个节点的位置是 frame** 19 | 20 | 需要注意的是,frame 是一个 Rect 类型,同时包含了节点的位置和大小信息,**参考坐标系为左上角原点坐标系**,也就是 origin (0,0) 的位置表示节点紧靠列表左边/上边的位置,举个例子,假如现在需要在列表内以左上角为起点垂直方向排列 3 个大小为 200x100 的节点,不考虑间距边距的情况下,最终的节点位置用 frame 来表示应该为: 21 | ```ts 22 | 1. (0, 0, 200, 100) 23 | 2. (0, 100, 200, 100) 24 | 3. (0, 200, 200, 100) 25 | ``` 26 | 27 | 如果需要给节点之间加上一个间距 10,则最终节点位置用 frame 来表示应该为: 28 | ```ts 29 | 1. (0, 0, 200, 100) 30 | 2. (0, 110, 200, 100) 31 | 3. (0, 220, 200, 100) 32 | ``` 33 | 34 | 如果还需要给节点加一个左边距 20 的话,则最终节点位置用 frame 来表示应该为: 35 | ```ts 36 | 1. (20, 0, 200, 100) 37 | 2. (20, 110, 200, 100) 38 | 3. (20, 220, 200, 100) 39 | ``` 40 | 41 | 42 | 把上面的例子通过代码实现的话就是: 43 | ```ts 44 | // 伪代码 45 | 46 | let spacing = 10 // 节点之间间距 47 | let section_left = 20 // 左边距 48 | let itemSize = new math.Size(200, 100) // 节点大小 49 | 50 | let attr1 = new YXLayoutAttributes() 51 | attr1.indexPath = new YXIndexPath(0, 0) // 第 0 个区第 0 个节点 52 | attr1.frame = new math.Rect(section_left, 0, itemSize.width, itemSize.height) // 这个节点的位置 53 | 54 | let attr2 = new YXLayoutAttributes() 55 | attr2.indexPath = new YXIndexPath(0, 1) // 第 0 个区第 1 个节点 56 | attr2.frame = new math.Rect(section_left, attr1.frame.yMax + spacing, itemSize.width, itemSize.height) // 这个节点的位置 57 | 58 | let attr3 = new YXLayoutAttributes() 59 | attr3.indexPath = new YXIndexPath(0, 2) // 第 0 个区第 2 个节点 60 | attr3.frame = new math.Rect(section_left, attr2.frame.yMax + spacing, itemSize.width, itemSize.height) // 这个节点的位置 61 | ``` 62 | 63 | ## YXLayout 64 | 65 | YXLayout 主要就是负责管理自定义的 YXLayoutAttributes 对象,在 YXLayout 里面无需考虑节点管理,开发者可以放心的定义全部的 YXLayoutAttributes 对象以描述所有的节点位置 66 | 67 | 可以参考项目里的 table-layout 的实现,了解 YXLayout 的基本工作流程 68 | 69 | ## table-layout 70 | 71 | 1. [实现一个类似 TableView 的布局规则 table-layout (了解基本的自定义布局流程)](./table-layout-1.md) 72 | 1. [使 table-layout 支持不同高度的节点 (了解如何支持不同大小的单元项)](./table-layout-2.md) 73 | 1. [使 table-layout 支持分区配置 (了解组件的分区概念)](./table-layout-3.md) 74 | 1. [使 table-layout 支持区头/区尾配置 (简单了解 supplementary 补充视图概念)](./table-layout-4.md) 75 | 1. [使 table-layout 支持区头/区尾吸附效果 (了解如何实时更新节点布局属性)](./table-layout-5.md) 76 | 1. [table-layout 性能优化 (了解组件的性能缺陷)](./table-layout-6.md) 77 | 78 | 以上就是 table-layout 完整的实现过程,有兴趣的可以了解一下 79 | 80 | 81 | -------------------------------------------------------------------------------- /doc/md/table-layout-1.md: -------------------------------------------------------------------------------- 1 | 2 | 基本的实现一个垂直方向排列的布局规则 3 | 4 | ```ts 5 | export class YXTableLayout extends YXLayout { 6 | 7 | /** 8 | * 行高 9 | */ 10 | rowHeight: number = 100 11 | 12 | /** 13 | * 内容上边距 14 | */ 15 | top: number = 0 16 | 17 | /** 18 | * 内容下边距 19 | */ 20 | bottom: number = 0 21 | 22 | /** 23 | * 节点之间间距 24 | */ 25 | spacing: number = 0 26 | 27 | prepare(collectionView: YXCollectionView): void { 28 | // 设置列表的滚动方向(这套布局固定为垂直方向滚动) 29 | collectionView.scrollView.horizontal = false 30 | collectionView.scrollView.vertical = true 31 | if (collectionView.scrollDirection === YXCollectionView.ScrollDirection.HORIZONTAL) { 32 | // 由于这套布局规则只支持垂直方向布局,当外部配置了水平方向滚动时这里可以给个警告 33 | warn(`YXTableLayout 仅支持垂直方向排列`) 34 | } 35 | 36 | // 获取列表内一共多少数据,这里传的这个 0 表示的区,在目前不支持分区的情况可以先不用考虑这个,后面会说到 37 | let numberOfItems = collectionView.getNumberOfItems(0) 38 | 39 | // 清空一下布局属性数组 40 | this.attributes = [] 41 | 42 | // 获取列表宽度 43 | const contentWidth = collectionView.node.getComponent(UITransform).width 44 | 45 | // 声明一个临时变量,用来记录当前所有内容的总高度 46 | let contentHeight = this.top 47 | 48 | // 为每条数据对应的生成一个布局属性 49 | for (let item = 0; item < numberOfItems; item++) { 50 | 51 | // 创建索引 52 | let indexPath = new YXIndexPath(0, item) 53 | 54 | // 通过索引创建一个 cell 节点的布局属性 55 | let attr = YXLayoutAttributes.layoutAttributesForCell(indexPath) 56 | 57 | // 确定这个节点的位置 58 | attr.frame.x = 0 59 | attr.frame.width = contentWidth 60 | attr.frame.height = this.rowHeight 61 | attr.frame.y = contentHeight + (item > 0 ? this.spacing : 0) 62 | 63 | // 重要: 保存布局属性 64 | this.attributes.push(attr) 65 | 66 | // 更新当前内容高度 67 | contentHeight = attr.frame.yMax 68 | } 69 | 70 | // 高度补一个底部间距 71 | contentHeight = contentHeight + this.bottom 72 | 73 | // 重要: 设置内容区域总大小,只有确定了滚动区域的大小列表才能滚动 74 | this.contentSize = new math.Size(contentWidth, contentHeight) 75 | } 76 | 77 | initOffset(collectionView: YXCollectionView): void { 78 | // 列表首次刷新时,调整一下列表的偏移位置 79 | collectionView.scrollView.scrollToTop() 80 | } 81 | } 82 | ``` 83 | 84 | -------------------------------------------------------------------------------- /doc/md/table-layout-2.md: -------------------------------------------------------------------------------- 1 | 2 | 使 table-layout 支持不同高度的节点 3 | 4 | ```ts 5 | export class YXTableLayout extends YXLayout { 6 | 7 | /** 8 | * 行高 9 | */ 10 | rowHeight: number | ((indexPath: YXIndexPath) => number) = 100 11 | 12 | /** 13 | * 内容上边距 14 | */ 15 | top: number = 0 16 | 17 | /** 18 | * 内容下边距 19 | */ 20 | bottom: number = 0 21 | 22 | /** 23 | * 节点之间间距 24 | */ 25 | spacing: number = 0 26 | 27 | prepare(collectionView: YXCollectionView): void { 28 | // 设置列表的滚动方向(这套布局固定为垂直方向滚动) 29 | collectionView.scrollView.horizontal = false 30 | collectionView.scrollView.vertical = true 31 | if (collectionView.scrollDirection === YXCollectionView.ScrollDirection.HORIZONTAL) { 32 | // 由于这套布局规则只支持垂直方向布局,当外部配置了水平方向滚动时这里可以给个警告 33 | warn(`YXTableLayout 仅支持垂直方向排列`) 34 | } 35 | 36 | // 获取列表内一共多少数据,这里传的这个 0 表示的区,在目前不支持分区的情况可以先不用考虑这个,后面会说到 37 | let numberOfItems = collectionView.getNumberOfItems(0) 38 | 39 | // 清空一下布局属性数组 40 | this.attributes = [] 41 | 42 | // 获取列表宽度 43 | const contentWidth = collectionView.node.getComponent(UITransform).width 44 | 45 | // 声明一个临时变量,用来记录当前所有内容的总高度 46 | let contentHeight = this.top 47 | 48 | // 为每条数据对应的生成一个布局属性 49 | for (let item = 0; item < numberOfItems; item++) { 50 | 51 | // 创建索引 52 | let indexPath = new YXIndexPath(0, item) 53 | 54 | // 通过索引创建一个 cell 节点的布局属性 55 | let attr = YXLayoutAttributes.layoutAttributesForCell(indexPath) 56 | 57 | // 通过索引获取这个节点的高度 58 | let rowHeight = this.rowHeight instanceof Function ? this.rowHeight(indexPath) : this.rowHeight 59 | 60 | // 确定这个节点的位置 61 | attr.frame.x = 0 62 | attr.frame.width = contentWidth 63 | attr.frame.height = rowHeight 64 | attr.frame.y = contentHeight + (item > 0 ? this.spacing : 0) 65 | 66 | // 重要: 保存布局属性 67 | this.attributes.push(attr) 68 | 69 | // 更新当前内容高度 70 | contentHeight = attr.frame.yMax 71 | } 72 | 73 | // 高度补一个底部间距 74 | contentHeight = contentHeight + this.bottom 75 | 76 | // 重要: 设置内容区域总大小,只有确定了滚动区域的大小列表才能滚动 77 | this.contentSize = new math.Size(contentWidth, contentHeight) 78 | } 79 | 80 | initOffset(collectionView: YXCollectionView): void { 81 | // 列表首次刷新时,调整一下列表的偏移位置 82 | collectionView.scrollView.scrollToTop() 83 | } 84 | } 85 | ``` 86 | 87 | -------------------------------------------------------------------------------- /doc/md/table-layout-3.md: -------------------------------------------------------------------------------- 1 | 2 | 使 table-layout 支持分区配置 3 | 4 | ```ts 5 | export class YXTableLayout extends YXLayout { 6 | 7 | /** 8 | * 行高 9 | */ 10 | rowHeight: number | ((indexPath: YXIndexPath) => number) = 100 11 | 12 | /** 13 | * 内容上边距 14 | */ 15 | top: number = 0 16 | 17 | /** 18 | * 内容下边距 19 | */ 20 | bottom: number = 0 21 | 22 | /** 23 | * 节点之间间距 24 | */ 25 | spacing: number = 0 26 | 27 | prepare(collectionView: YXCollectionView): void { 28 | // 设置列表的滚动方向(这套布局固定为垂直方向滚动) 29 | collectionView.scrollView.horizontal = false 30 | collectionView.scrollView.vertical = true 31 | if (collectionView.scrollDirection === YXCollectionView.ScrollDirection.HORIZONTAL) { 32 | // 由于这套布局规则只支持垂直方向布局,当外部配置了水平方向滚动时这里可以给个警告 33 | warn(`YXTableLayout 仅支持垂直方向排列`) 34 | } 35 | 36 | // 清空一下布局属性数组 37 | this.attributes = [] 38 | 39 | // 获取列表宽度 40 | const contentWidth = collectionView.node.getComponent(UITransform).width 41 | 42 | // 声明一个临时变量,用来记录当前所有内容的总高度 43 | let contentHeight = 0 44 | 45 | // 获取列表一共分多少个区 46 | let numberOfSections = collectionView.getNumberOfSections() 47 | 48 | // 为每条数据对应的生成一个布局属性 49 | for (let section = 0; section < numberOfSections; section++) { 50 | 51 | // 将 top 配置应用到每个区 52 | contentHeight = contentHeight + this.top 53 | 54 | // 获取这个区内的内容数量,注意这里传入的是 section 55 | let numberOfItems = collectionView.getNumberOfItems(section) 56 | 57 | for (let item = 0; item < numberOfItems; item++) { 58 | 59 | // 创建索引,注意这里的 section 已经改为正确的 section 了 60 | let indexPath = new YXIndexPath(section, item) 61 | 62 | // 通过索引创建一个 cell 节点的布局属性 63 | let attr = YXLayoutAttributes.layoutAttributesForCell(indexPath) 64 | 65 | // 通过索引获取这个节点的高度 66 | let rowHeight = this.rowHeight instanceof Function ? this.rowHeight(indexPath) : this.rowHeight 67 | 68 | // 确定这个节点的位置 69 | attr.frame.x = 0 70 | attr.frame.width = contentWidth 71 | attr.frame.height = rowHeight 72 | attr.frame.y = contentHeight + (item > 0 ? this.spacing : 0) 73 | 74 | // 重要: 保存布局属性 75 | this.attributes.push(attr) 76 | 77 | // 更新当前内容高度 78 | contentHeight = attr.frame.yMax 79 | } 80 | 81 | // 高度补一个底部间距,跟 top 一样,也是应用到每个区 82 | contentHeight = contentHeight + this.bottom 83 | } 84 | 85 | // 重要: 设置内容区域总大小,只有确定了滚动区域的大小列表才能滚动 86 | this.contentSize = new math.Size(contentWidth, contentHeight) 87 | } 88 | 89 | initOffset(collectionView: YXCollectionView): void { 90 | // 列表首次刷新时,调整一下列表的偏移位置 91 | collectionView.scrollView.scrollToTop() 92 | } 93 | } 94 | ``` 95 | 96 | -------------------------------------------------------------------------------- /doc/md/table-layout-4.md: -------------------------------------------------------------------------------- 1 | 2 | 使 table-layout 支持区头/区尾配置 3 | 4 | ```ts 5 | enum _yx_table_layout_supplementary_kinds { 6 | HEADER = 'header', 7 | FOOTER = 'footer', 8 | } 9 | 10 | export class YXTableLayout extends YXLayout { 11 | 12 | /** 13 | * 行高 14 | */ 15 | rowHeight: number | ((indexPath: YXIndexPath) => number) = 100 16 | 17 | /** 18 | * 内容上边距 19 | */ 20 | top: number = 0 21 | 22 | /** 23 | * 内容下边距 24 | */ 25 | bottom: number = 0 26 | 27 | /** 28 | * 节点之间间距 29 | */ 30 | spacing: number = 0 31 | 32 | /** 33 | * 区头高度 34 | */ 35 | sectionHeaderHeight: number | ((section: number) => number) = null 36 | 37 | /** 38 | * 区尾高度 39 | */ 40 | sectionFooterHeight: number | ((section: number) => number) = null 41 | 42 | /** 43 | * 区头/区尾标识 44 | */ 45 | static SupplementaryKinds = _yx_table_layout_supplementary_kinds 46 | 47 | prepare(collectionView: YXCollectionView): void { 48 | // 设置列表的滚动方向(这套布局固定为垂直方向滚动) 49 | collectionView.scrollView.horizontal = false 50 | collectionView.scrollView.vertical = true 51 | if (collectionView.scrollDirection === YXCollectionView.ScrollDirection.HORIZONTAL) { 52 | // 由于这套布局规则只支持垂直方向布局,当外部配置了水平方向滚动时这里可以给个警告 53 | warn(`YXTableLayout 仅支持垂直方向排列`) 54 | } 55 | 56 | // 清空一下布局属性数组 57 | this.attributes = [] 58 | 59 | // 获取列表宽度 60 | const contentWidth = collectionView.node.getComponent(UITransform).width 61 | 62 | // 声明一个临时变量,用来记录当前所有内容的总高度 63 | let contentHeight = 0 64 | 65 | // 获取列表一共分多少个区 66 | let numberOfSections = collectionView.getNumberOfSections() 67 | 68 | // 为每条数据对应的生成一个布局属性 69 | for (let section = 0; section < numberOfSections; section++) { 70 | 71 | // 创建一个区索引 72 | let sectionIndexPath = new YXIndexPath(section, 0) 73 | 74 | // 通过区索引创建一个区头节点布局属性 75 | let sectionHeaderHeight = 0 76 | if (this.sectionHeaderHeight) { 77 | sectionHeaderHeight = this.sectionHeaderHeight instanceof Function ? this.sectionHeaderHeight(section) : this.sectionHeaderHeight 78 | } 79 | if (sectionHeaderHeight > 0) { 80 | let headerAttr = YXLayoutAttributes.layoutAttributesForSupplementary(sectionIndexPath, YXTableLayout.SupplementaryKinds.HEADER) 81 | 82 | // 确定这个节点的位置 83 | headerAttr.frame.x = 0 84 | headerAttr.frame.width = contentWidth 85 | headerAttr.frame.height = sectionHeaderHeight 86 | headerAttr.frame.y = contentHeight 87 | 88 | // 重要: 保存布局属性 89 | this.attributes.push(headerAttr) 90 | 91 | // 更新整体内容高度 92 | contentHeight = headerAttr.frame.yMax 93 | } 94 | 95 | // 将 top 配置应用到每个区 96 | contentHeight = contentHeight + this.top 97 | 98 | // 获取这个区内的内容数量,注意这里传入的是 section 99 | let numberOfItems = collectionView.getNumberOfItems(section) 100 | 101 | for (let item = 0; item < numberOfItems; item++) { 102 | 103 | // 创建索引,注意这里的 section 已经改为正确的 section 了 104 | let indexPath = new YXIndexPath(section, item) 105 | 106 | // 通过索引创建一个 cell 节点的布局属性 107 | let attr = YXLayoutAttributes.layoutAttributesForCell(indexPath) 108 | 109 | // 通过索引获取这个节点的高度 110 | let rowHeight = this.rowHeight instanceof Function ? this.rowHeight(indexPath) : this.rowHeight 111 | 112 | // 确定这个节点的位置 113 | attr.frame.x = 0 114 | attr.frame.width = contentWidth 115 | attr.frame.height = rowHeight 116 | attr.frame.y = contentHeight + (item > 0 ? this.spacing : 0) 117 | 118 | // 重要: 保存布局属性 119 | this.attributes.push(attr) 120 | 121 | // 更新当前内容高度 122 | contentHeight = attr.frame.yMax 123 | } 124 | 125 | // 高度补一个底部间距,跟 top 一样,也是应用到每个区 126 | contentHeight = contentHeight + this.bottom 127 | 128 | // 通过区索引创建一个区尾节点布局属性 129 | let sectionFooterHeight = 0 130 | if (this.sectionFooterHeight) { 131 | sectionFooterHeight = this.sectionFooterHeight instanceof Function ? this.sectionFooterHeight(section) : this.sectionFooterHeight 132 | } 133 | if (sectionFooterHeight > 0) { 134 | let footerAttr = YXLayoutAttributes.layoutAttributesForSupplementary(sectionIndexPath, YXTableLayout.SupplementaryKinds.FOOTER) 135 | 136 | // 确定这个节点的位置 137 | footerAttr.frame.x = 0 138 | footerAttr.frame.width = contentWidth 139 | footerAttr.frame.height = sectionFooterHeight 140 | footerAttr.frame.y = contentHeight 141 | 142 | // 重要: 保存布局属性 143 | this.attributes.push(footerAttr) 144 | 145 | // 更新整体内容高度 146 | contentHeight = footerAttr.frame.yMax 147 | } 148 | } 149 | 150 | // 重要: 设置内容区域总大小,只有确定了滚动区域的大小列表才能滚动 151 | this.contentSize = new math.Size(contentWidth, contentHeight) 152 | } 153 | 154 | initOffset(collectionView: YXCollectionView): void { 155 | // 列表首次刷新时,调整一下列表的偏移位置 156 | collectionView.scrollView.scrollToTop() 157 | } 158 | } 159 | ``` 160 | -------------------------------------------------------------------------------- /doc/md/table-layout-5.md: -------------------------------------------------------------------------------- 1 | 2 | 使 table-layout 支持区头/区尾吸附效果 3 | 4 | ```ts 5 | enum _yx_table_layout_supplementary_kinds { 6 | HEADER = 'header', 7 | FOOTER = 'footer', 8 | } 9 | 10 | export class YXTableLayout extends YXLayout { 11 | 12 | /** 13 | * 行高 14 | */ 15 | rowHeight: number | ((indexPath: YXIndexPath) => number) = 100 16 | 17 | /** 18 | * 内容上边距 19 | */ 20 | top: number = 0 21 | 22 | /** 23 | * 内容下边距 24 | */ 25 | bottom: number = 0 26 | 27 | /** 28 | * 节点之间间距 29 | */ 30 | spacing: number = 0 31 | 32 | /** 33 | * 区头高度 34 | */ 35 | sectionHeaderHeight: number | ((section: number) => number) = null 36 | 37 | /** 38 | * 区尾高度 39 | */ 40 | sectionFooterHeight: number | ((section: number) => number) = null 41 | 42 | /** 43 | * 钉住 header 的位置 ( header 吸附在列表可见范围内 ) 44 | */ 45 | sectionHeadersPinToVisibleBounds: boolean = false 46 | 47 | /** 48 | * 钉住 footer 的位置 ( footer 吸附在列表可见范围内 ) 49 | */ 50 | sectionFootersPinToVisibleBounds: boolean = false 51 | 52 | /** 53 | * 区头/区尾标识 54 | */ 55 | static SupplementaryKinds = _yx_table_layout_supplementary_kinds 56 | 57 | protected originalHeaderRect: Map = new Map() // 保存所有 header 的原始位置 58 | protected originalFooterRect: Map = new Map() // 保存所有 footer 的原始位置 59 | 60 | prepare(collectionView: YXCollectionView): void { 61 | // 设置列表的滚动方向(这套布局固定为垂直方向滚动) 62 | collectionView.scrollView.horizontal = false 63 | collectionView.scrollView.vertical = true 64 | if (collectionView.scrollDirection === YXCollectionView.ScrollDirection.HORIZONTAL) { 65 | // 由于这套布局规则只支持垂直方向布局,当外部配置了水平方向滚动时这里可以给个警告 66 | warn(`YXTableLayout 仅支持垂直方向排列`) 67 | } 68 | 69 | // 清空一下布局属性数组 70 | this.attributes = [] 71 | this.originalHeaderRect.clear() 72 | this.originalFooterRect.clear() 73 | 74 | // 获取列表宽度 75 | const contentWidth = collectionView.node.getComponent(UITransform).width 76 | 77 | // 声明一个临时变量,用来记录当前所有内容的总高度 78 | let contentHeight = 0 79 | 80 | // 获取列表一共分多少个区 81 | let numberOfSections = collectionView.getNumberOfSections() 82 | 83 | // 为每条数据对应的生成一个布局属性 84 | for (let section = 0; section < numberOfSections; section++) { 85 | 86 | // 创建一个区索引 87 | let sectionIndexPath = new YXIndexPath(section, 0) 88 | 89 | // 通过区索引创建一个区头节点布局属性 90 | let sectionHeaderHeight = 0 91 | if (this.sectionHeaderHeight) { 92 | sectionHeaderHeight = this.sectionHeaderHeight instanceof Function ? this.sectionHeaderHeight(section) : this.sectionHeaderHeight 93 | } 94 | if (sectionHeaderHeight > 0) { 95 | let headerAttr = YXLayoutAttributes.layoutAttributesForSupplementary(sectionIndexPath, YXTableLayout.SupplementaryKinds.HEADER) 96 | 97 | // 确定这个节点的位置 98 | headerAttr.frame.x = 0 99 | headerAttr.frame.width = contentWidth 100 | headerAttr.frame.height = sectionHeaderHeight 101 | headerAttr.frame.y = contentHeight 102 | 103 | // 调整层级 104 | headerAttr.zIndex = 1 105 | 106 | // 重要: 保存布局属性 107 | this.attributes.push(headerAttr) 108 | this.originalHeaderRect.set(section, headerAttr.frame.clone()) 109 | 110 | // 更新整体内容高度 111 | contentHeight = headerAttr.frame.yMax 112 | } 113 | 114 | // 将 top 配置应用到每个区 115 | contentHeight = contentHeight + this.top 116 | 117 | // 获取这个区内的内容数量,注意这里传入的是 section 118 | let numberOfItems = collectionView.getNumberOfItems(section) 119 | 120 | for (let item = 0; item < numberOfItems; item++) { 121 | 122 | // 创建索引,注意这里的 section 已经改为正确的 section 了 123 | let indexPath = new YXIndexPath(section, item) 124 | 125 | // 通过索引创建一个 cell 节点的布局属性 126 | let attr = YXLayoutAttributes.layoutAttributesForCell(indexPath) 127 | 128 | // 通过索引获取这个节点的高度 129 | let rowHeight = this.rowHeight instanceof Function ? this.rowHeight(indexPath) : this.rowHeight 130 | 131 | // 确定这个节点的位置 132 | attr.frame.x = 0 133 | attr.frame.width = contentWidth 134 | attr.frame.height = rowHeight 135 | attr.frame.y = contentHeight + (item > 0 ? this.spacing : 0) 136 | 137 | // 重要: 保存布局属性 138 | this.attributes.push(attr) 139 | 140 | // 更新当前内容高度 141 | contentHeight = attr.frame.yMax 142 | } 143 | 144 | // 高度补一个底部间距,跟 top 一样,也是应用到每个区 145 | contentHeight = contentHeight + this.bottom 146 | 147 | // 通过区索引创建一个区尾节点布局属性 148 | let sectionFooterHeight = 0 149 | if (this.sectionFooterHeight) { 150 | sectionFooterHeight = this.sectionFooterHeight instanceof Function ? this.sectionFooterHeight(section) : this.sectionFooterHeight 151 | } 152 | if (sectionFooterHeight > 0) { 153 | let footerAttr = YXLayoutAttributes.layoutAttributesForSupplementary(sectionIndexPath, YXTableLayout.SupplementaryKinds.FOOTER) 154 | 155 | // 确定这个节点的位置 156 | footerAttr.frame.x = 0 157 | footerAttr.frame.width = contentWidth 158 | footerAttr.frame.height = sectionFooterHeight 159 | footerAttr.frame.y = contentHeight 160 | 161 | // 调整层级 162 | footerAttr.zIndex = 1 163 | 164 | // 重要: 保存布局属性 165 | this.attributes.push(footerAttr) 166 | this.originalFooterRect.set(section, footerAttr.frame.clone()) 167 | 168 | // 更新整体内容高度 169 | contentHeight = footerAttr.frame.yMax 170 | } 171 | } 172 | 173 | // 重要: 设置内容区域总大小,只有确定了滚动区域的大小列表才能滚动 174 | this.contentSize = new math.Size(contentWidth, contentHeight) 175 | } 176 | 177 | initOffset(collectionView: YXCollectionView): void { 178 | // 列表首次刷新时,调整一下列表的偏移位置 179 | collectionView.scrollView.scrollToTop() 180 | } 181 | 182 | layoutAttributesForElementsInRect(rect: math.Rect, collectionView: YXCollectionView): YXLayoutAttributes[] { 183 | let numberOfSections = collectionView.getNumberOfSections() 184 | let scrollOffset = collectionView.scrollView.getScrollOffset() 185 | for (let index = 0; index < this.attributes.length; index++) { 186 | const element = this.attributes[index]; 187 | if (element.elementCategory === 'Supplementary') { 188 | 189 | if (this.sectionHeadersPinToVisibleBounds && element.supplementaryKinds === YXTableLayout.SupplementaryKinds.HEADER) { 190 | const originalFrame = this.originalHeaderRect.get(element.indexPath.section) 191 | element.frame.y = originalFrame.y 192 | if (scrollOffset.y > originalFrame.y) { 193 | element.frame.y = scrollOffset.y 194 | } 195 | const nextOriginalFrame = this.getNextOriginalFrame(element.indexPath.section, YXTableLayout.SupplementaryKinds.FOOTER, numberOfSections) 196 | if (nextOriginalFrame) { 197 | if (element.frame.yMax > nextOriginalFrame.y) { 198 | element.frame.y = nextOriginalFrame.y - element.frame.height 199 | } 200 | } 201 | } 202 | 203 | if (this.sectionFootersPinToVisibleBounds && element.supplementaryKinds === YXTableLayout.SupplementaryKinds.FOOTER) { 204 | let bottom = scrollOffset.y + collectionView.scrollView.view.height 205 | const originalFrame = this.originalFooterRect.get(element.indexPath.section) 206 | const previousOriginalFrame = this.getPreviousOriginalFrame(element.indexPath.section, YXTableLayout.SupplementaryKinds.HEADER) 207 | element.frame.y = originalFrame.y 208 | if (bottom < originalFrame.yMax) { 209 | element.frame.y = bottom - element.frame.height 210 | if (previousOriginalFrame) { 211 | if (element.frame.y < previousOriginalFrame.yMax) { 212 | element.frame.y = previousOriginalFrame.yMax 213 | } 214 | } 215 | } 216 | } 217 | } 218 | } 219 | return this.attributes 220 | } 221 | 222 | shouldUpdateAttributesZIndex(): boolean { 223 | return this.sectionHeadersPinToVisibleBounds || this.sectionFootersPinToVisibleBounds 224 | } 225 | 226 | shouldUpdateAttributesForBoundsChange(): boolean { 227 | return this.sectionHeadersPinToVisibleBounds || this.sectionFootersPinToVisibleBounds 228 | } 229 | 230 | /** 231 | * 获取 `section` 下一个 header 或者 footer 的位置 232 | */ 233 | protected getNextOriginalFrame(section: number, kinds: _yx_table_layout_supplementary_kinds, total: number) { 234 | if (section >= total) { return null } 235 | if (kinds === YXTableLayout.SupplementaryKinds.HEADER) { 236 | let result = this.originalHeaderRect.get(section) 237 | if (result) { return result } 238 | return this.getNextOriginalFrame(section, YXTableLayout.SupplementaryKinds.FOOTER, total) 239 | } 240 | if (kinds === YXTableLayout.SupplementaryKinds.FOOTER) { 241 | let result = this.originalFooterRect.get(section) 242 | if (result) { return result } 243 | return this.getNextOriginalFrame(section + 1, YXTableLayout.SupplementaryKinds.HEADER, total) 244 | } 245 | return null 246 | } 247 | 248 | /** 249 | * 获取 `section` 前一个 header 或者 footer 的位置 250 | */ 251 | protected getPreviousOriginalFrame(section: number, kinds: _yx_table_layout_supplementary_kinds) { 252 | if (section < 0) { return null } 253 | if (kinds === YXTableLayout.SupplementaryKinds.HEADER) { 254 | let result = this.originalHeaderRect.get(section) 255 | if (result) { return result } 256 | return this.getPreviousOriginalFrame(section - 1, YXTableLayout.SupplementaryKinds.FOOTER) 257 | } 258 | if (kinds === YXTableLayout.SupplementaryKinds.FOOTER) { 259 | let result = this.originalFooterRect.get(section) 260 | if (result) { return result } 261 | return this.getPreviousOriginalFrame(section, YXTableLayout.SupplementaryKinds.HEADER) 262 | } 263 | return null 264 | } 265 | } 266 | ``` 267 | 268 | -------------------------------------------------------------------------------- /doc/md/table-layout-6.md: -------------------------------------------------------------------------------- 1 | 2 | 通过二分查找优化 table-layout 性能 3 | 4 | ```ts 5 | enum _yx_table_layout_supplementary_kinds { 6 | HEADER = 'header', 7 | FOOTER = 'footer', 8 | } 9 | 10 | export class YXTableLayout extends YXLayout { 11 | 12 | /** 13 | * 行高 14 | */ 15 | rowHeight: number | ((indexPath: YXIndexPath) => number) = 100 16 | 17 | /** 18 | * 内容上边距 19 | */ 20 | top: number = 0 21 | 22 | /** 23 | * 内容下边距 24 | */ 25 | bottom: number = 0 26 | 27 | /** 28 | * 节点之间间距 29 | */ 30 | spacing: number = 0 31 | 32 | /** 33 | * 区头高度 34 | */ 35 | sectionHeaderHeight: number | ((section: number) => number) = null 36 | 37 | /** 38 | * 区尾高度 39 | */ 40 | sectionFooterHeight: number | ((section: number) => number) = null 41 | 42 | /** 43 | * 钉住 header 的位置 ( header 吸附在列表可见范围内 ) 44 | */ 45 | sectionHeadersPinToVisibleBounds: boolean = false 46 | 47 | /** 48 | * 钉住 footer 的位置 ( footer 吸附在列表可见范围内 ) 49 | */ 50 | sectionFootersPinToVisibleBounds: boolean = false 51 | 52 | /** 53 | * 区头/区尾标识 54 | */ 55 | static SupplementaryKinds = _yx_table_layout_supplementary_kinds 56 | 57 | protected originalHeaderRect: Map = new Map() // 保存所有 header 的原始位置 58 | protected originalFooterRect: Map = new Map() // 保存所有 footer 的原始位置 59 | 60 | // 为了优化查找,额外维护几个数组按类别管理所有的布局属性,空间换时间 61 | protected allCellAttributes: YXLayoutAttributes[] = [] 62 | protected allHeaderAttributes: YXLayoutAttributes[] = [] 63 | protected allFooterAttributes: YXLayoutAttributes[] = [] 64 | 65 | prepare(collectionView: YXCollectionView): void { 66 | // 设置列表的滚动方向(这套布局固定为垂直方向滚动) 67 | collectionView.scrollView.horizontal = false 68 | collectionView.scrollView.vertical = true 69 | if (collectionView.scrollDirection === YXCollectionView.ScrollDirection.HORIZONTAL) { 70 | // 由于这套布局规则只支持垂直方向布局,当外部配置了水平方向滚动时这里可以给个警告 71 | warn(`YXTableLayout 仅支持垂直方向排列`) 72 | } 73 | 74 | // 清空一下布局属性数组 75 | this.attributes = [] 76 | this.allCellAttributes = [] 77 | this.allHeaderAttributes = [] 78 | this.allFooterAttributes = [] 79 | this.originalHeaderRect.clear() 80 | this.originalFooterRect.clear() 81 | 82 | // 获取列表宽度 83 | const contentWidth = collectionView.node.getComponent(UITransform).width 84 | 85 | // 声明一个临时变量,用来记录当前所有内容的总高度 86 | let contentHeight = 0 87 | 88 | // 获取列表一共分多少个区 89 | let numberOfSections = collectionView.getNumberOfSections() 90 | 91 | // 为每条数据对应的生成一个布局属性 92 | for (let section = 0; section < numberOfSections; section++) { 93 | 94 | // 创建一个区索引 95 | let sectionIndexPath = new YXIndexPath(section, 0) 96 | 97 | // 通过区索引创建一个区头节点布局属性 98 | let sectionHeaderHeight = 0 99 | if (this.sectionHeaderHeight) { 100 | sectionHeaderHeight = this.sectionHeaderHeight instanceof Function ? this.sectionHeaderHeight(section) : this.sectionHeaderHeight 101 | } 102 | if (sectionHeaderHeight > 0) { 103 | let headerAttr = YXLayoutAttributes.layoutAttributesForSupplementary(sectionIndexPath, YXTableLayout.SupplementaryKinds.HEADER) 104 | 105 | // 确定这个节点的位置 106 | headerAttr.frame.x = 0 107 | headerAttr.frame.width = contentWidth 108 | headerAttr.frame.height = sectionHeaderHeight 109 | headerAttr.frame.y = contentHeight 110 | 111 | // 调整层级 112 | headerAttr.zIndex = 1 113 | 114 | // 重要: 保存布局属性 115 | this.attributes.push(headerAttr) 116 | this.originalHeaderRect.set(section, headerAttr.frame.clone()) 117 | this.allHeaderAttributes.push(headerAttr) 118 | 119 | // 更新整体内容高度 120 | contentHeight = headerAttr.frame.yMax 121 | } 122 | 123 | // 将 top 配置应用到每个区 124 | contentHeight = contentHeight + this.top 125 | 126 | // 获取这个区内的内容数量,注意这里传入的是 section 127 | let numberOfItems = collectionView.getNumberOfItems(section) 128 | 129 | for (let item = 0; item < numberOfItems; item++) { 130 | 131 | // 创建索引,注意这里的 section 已经改为正确的 section 了 132 | let indexPath = new YXIndexPath(section, item) 133 | 134 | // 通过索引创建一个 cell 节点的布局属性 135 | let attr = YXLayoutAttributes.layoutAttributesForCell(indexPath) 136 | 137 | // 通过索引获取这个节点的高度 138 | let rowHeight = this.rowHeight instanceof Function ? this.rowHeight(indexPath) : this.rowHeight 139 | 140 | // 确定这个节点的位置 141 | attr.frame.x = 0 142 | attr.frame.width = contentWidth 143 | attr.frame.height = rowHeight 144 | attr.frame.y = contentHeight + (item > 0 ? this.spacing : 0) 145 | 146 | // 重要: 保存布局属性 147 | this.attributes.push(attr) 148 | this.allCellAttributes.push(attr) 149 | 150 | // 更新当前内容高度 151 | contentHeight = attr.frame.yMax 152 | } 153 | 154 | // 高度补一个底部间距,跟 top 一样,也是应用到每个区 155 | contentHeight = contentHeight + this.bottom 156 | 157 | // 通过区索引创建一个区尾节点布局属性 158 | let sectionFooterHeight = 0 159 | if (this.sectionFooterHeight) { 160 | sectionFooterHeight = this.sectionFooterHeight instanceof Function ? this.sectionFooterHeight(section) : this.sectionFooterHeight 161 | } 162 | if (sectionFooterHeight > 0) { 163 | let footerAttr = YXLayoutAttributes.layoutAttributesForSupplementary(sectionIndexPath, YXTableLayout.SupplementaryKinds.FOOTER) 164 | 165 | // 确定这个节点的位置 166 | footerAttr.frame.x = 0 167 | footerAttr.frame.width = contentWidth 168 | footerAttr.frame.height = sectionFooterHeight 169 | footerAttr.frame.y = contentHeight 170 | 171 | // 调整层级 172 | footerAttr.zIndex = 1 173 | 174 | // 重要: 保存布局属性 175 | this.attributes.push(footerAttr) 176 | this.originalFooterRect.set(section, footerAttr.frame.clone()) 177 | this.allFooterAttributes.push(footerAttr) 178 | 179 | // 更新整体内容高度 180 | contentHeight = footerAttr.frame.yMax 181 | } 182 | } 183 | 184 | // 重要: 设置内容区域总大小,只有确定了滚动区域的大小列表才能滚动 185 | this.contentSize = new math.Size(contentWidth, contentHeight) 186 | } 187 | 188 | initOffset(collectionView: YXCollectionView): void { 189 | // 列表首次刷新时,调整一下列表的偏移位置 190 | collectionView.scrollView.scrollToTop() 191 | } 192 | 193 | layoutAttributesForElementsInRect(rect: math.Rect, collectionView: YXCollectionView): YXLayoutAttributes[] { 194 | let result = this.visibleElementsInRect(rect, collectionView) 195 | if (this.sectionHeadersPinToVisibleBounds == false && this.sectionFootersPinToVisibleBounds == false) { 196 | return result // 不需要调整节点位置,直接返回就好 197 | } 198 | 199 | let numberOfSections = collectionView.getNumberOfSections() 200 | let scrollOffset = collectionView.scrollView.getScrollOffset() 201 | for (let index = 0; index < result.length; index++) { 202 | const element = result[index]; 203 | if (element.elementCategory === 'Supplementary') { 204 | 205 | if (this.sectionHeadersPinToVisibleBounds && element.supplementaryKinds === YXTableLayout.SupplementaryKinds.HEADER) { 206 | const originalFrame = this.originalHeaderRect.get(element.indexPath.section) 207 | element.frame.y = originalFrame.y 208 | if (scrollOffset.y > originalFrame.y) { 209 | element.frame.y = scrollOffset.y 210 | } 211 | const nextOriginalFrame = this.getNextOriginalFrame(element.indexPath.section, YXTableLayout.SupplementaryKinds.FOOTER, numberOfSections) 212 | if (nextOriginalFrame) { 213 | if (element.frame.yMax > nextOriginalFrame.y) { 214 | element.frame.y = nextOriginalFrame.y - element.frame.height 215 | } 216 | } 217 | } 218 | 219 | if (this.sectionFootersPinToVisibleBounds && element.supplementaryKinds === YXTableLayout.SupplementaryKinds.FOOTER) { 220 | let bottom = scrollOffset.y + collectionView.scrollView.view.height 221 | const originalFrame = this.originalFooterRect.get(element.indexPath.section) 222 | const previousOriginalFrame = this.getPreviousOriginalFrame(element.indexPath.section, YXTableLayout.SupplementaryKinds.HEADER) 223 | element.frame.y = originalFrame.y 224 | if (bottom < originalFrame.yMax) { 225 | element.frame.y = bottom - element.frame.height 226 | if (previousOriginalFrame) { 227 | if (element.frame.y < previousOriginalFrame.yMax) { 228 | element.frame.y = previousOriginalFrame.yMax 229 | } 230 | } 231 | } 232 | } 233 | } 234 | } 235 | return result 236 | } 237 | 238 | shouldUpdateAttributesZIndex(): boolean { 239 | return this.sectionHeadersPinToVisibleBounds || this.sectionFootersPinToVisibleBounds 240 | } 241 | 242 | shouldUpdateAttributesForBoundsChange(): boolean { 243 | return this.sectionHeadersPinToVisibleBounds || this.sectionFootersPinToVisibleBounds 244 | } 245 | 246 | /** 247 | * 获取 `section` 下一个 header 或者 footer 的位置 248 | */ 249 | protected getNextOriginalFrame(section: number, kinds: _yx_table_layout_supplementary_kinds, total: number) { 250 | if (section >= total) { return null } 251 | if (kinds === YXTableLayout.SupplementaryKinds.HEADER) { 252 | let result = this.originalHeaderRect.get(section) 253 | if (result) { return result } 254 | return this.getNextOriginalFrame(section, YXTableLayout.SupplementaryKinds.FOOTER, total) 255 | } 256 | if (kinds === YXTableLayout.SupplementaryKinds.FOOTER) { 257 | let result = this.originalFooterRect.get(section) 258 | if (result) { return result } 259 | return this.getNextOriginalFrame(section + 1, YXTableLayout.SupplementaryKinds.HEADER, total) 260 | } 261 | return null 262 | } 263 | 264 | /** 265 | * 获取 `section` 前一个 header 或者 footer 的位置 266 | */ 267 | protected getPreviousOriginalFrame(section: number, kinds: _yx_table_layout_supplementary_kinds) { 268 | if (section < 0) { return null } 269 | if (kinds === YXTableLayout.SupplementaryKinds.HEADER) { 270 | let result = this.originalHeaderRect.get(section) 271 | if (result) { return result } 272 | return this.getPreviousOriginalFrame(section - 1, YXTableLayout.SupplementaryKinds.FOOTER) 273 | } 274 | if (kinds === YXTableLayout.SupplementaryKinds.FOOTER) { 275 | let result = this.originalFooterRect.get(section) 276 | if (result) { return result } 277 | return this.getPreviousOriginalFrame(section, YXTableLayout.SupplementaryKinds.HEADER) 278 | } 279 | return null 280 | } 281 | 282 | /** 283 | * 抽出来一个方法用来优化列表性能 284 | * 在优化之前,可以先看一下 @see YXLayout.layoutAttributesForElementsInRect 关于返回值的说明 285 | * 对于有序列表来说,一般都是可以通过二分查找来进行优化 286 | */ 287 | protected visibleElementsInRect(rect: math.Rect, collectionView: YXCollectionView) { 288 | if (this.attributes.length <= 100) { return this.attributes } // 少量数据就不查了,直接返回全部 289 | 290 | let result: YXLayoutAttributes[] = [] 291 | 292 | // header 跟 footer 暂时不考虑,数据相对来说不算很多,直接全部返回 293 | result.push(...this.allHeaderAttributes) 294 | result.push(...this.allFooterAttributes) 295 | 296 | // 关于 cell,这里用二分查找来优化一下 297 | // 首先通过二分先查出个大概位置 298 | let midIdx = -1 299 | let left = 0 300 | let right = this.allCellAttributes.length - 1 301 | 302 | while (left <= right && right >= 0) { 303 | let mid = left + (right - left) / 2 304 | mid = Math.floor(mid) 305 | let attr = this.allCellAttributes[mid] 306 | if (rect.intersects(attr.frame)) { 307 | midIdx = mid 308 | break 309 | } 310 | if (rect.yMax < attr.frame.yMin || rect.xMax < attr.frame.xMin) { 311 | right = mid - 1 312 | } else { 313 | left = mid + 1 314 | } 315 | } 316 | 317 | // 二分查找出错了,返回全部的布局属性 318 | if (midIdx < 0) { 319 | return this.attributes 320 | } 321 | 322 | // 把模糊查到这个先加进来 323 | result.push(this.allCellAttributes[midIdx]) 324 | 325 | // 然后依次往前检查,直到超出当前的显示范围 326 | let startIdx = midIdx 327 | while (startIdx > 0) { 328 | let idx = startIdx - 1 329 | let attr = this.allCellAttributes[idx] 330 | if (rect.intersects(attr.frame) == false) { 331 | break 332 | } 333 | result.push(attr) 334 | startIdx = idx 335 | } 336 | 337 | // 依次往后检查,直到超出当前的显示范围 338 | let endIdx = midIdx 339 | while (endIdx < this.allCellAttributes.length - 1) { 340 | let idx = endIdx + 1 341 | let attr = this.allCellAttributes[idx] 342 | if (rect.intersects(attr.frame) == false) { 343 | break 344 | } 345 | result.push(attr) 346 | endIdx = idx 347 | } 348 | 349 | return result 350 | } 351 | } 352 | 353 | ``` 354 | 355 | -------------------------------------------------------------------------------- /list-2x/.gitignore: -------------------------------------------------------------------------------- 1 | #///////////////////////////////////////////////////////////////////////////// 2 | # Fireball Projects 3 | #///////////////////////////////////////////////////////////////////////////// 4 | 5 | /library/ 6 | /temp/ 7 | /local/ 8 | /build/ 9 | native 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 | *.csproj 31 | *.pidb 32 | *.unityproj 33 | *.suo 34 | 35 | #///////////////////////////////////////////////////////////////////////////// 36 | # OS generated files 37 | #///////////////////////////////////////////////////////////////////////////// 38 | 39 | .DS_Store 40 | ehthumbs.db 41 | Thumbs.db 42 | 43 | #///////////////////////////////////////////////////////////////////////////// 44 | # WebStorm files 45 | #///////////////////////////////////////////////////////////////////////////// 46 | 47 | .idea/ 48 | 49 | #////////////////////////// 50 | # VS Code files 51 | #////////////////////////// 52 | 53 | .vscode/ 54 | -------------------------------------------------------------------------------- /list-2x/assets/common-cells.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.3", 3 | "uuid": "22383302-b592-44d1-b41d-afd4d2949ce4", 4 | "importer": "folder", 5 | "isBundle": false, 6 | "bundleName": "", 7 | "priority": 1, 8 | "compressionType": {}, 9 | "optimizeHotUpdate": {}, 10 | "inlineSpriteFrames": {}, 11 | "isRemoteBundle": {}, 12 | "subMetas": {} 13 | } -------------------------------------------------------------------------------- /list-2x/assets/common-cells/shape-label-cell.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": "shape-label-cell", 17 | "_objFlags": 0, 18 | "_parent": null, 19 | "_children": [ 20 | { 21 | "__id__": 2 22 | }, 23 | { 24 | "__id__": 6 25 | } 26 | ], 27 | "_active": true, 28 | "_components": [], 29 | "_prefab": { 30 | "__id__": 9 31 | }, 32 | "_opacity": 255, 33 | "_color": { 34 | "__type__": "cc.Color", 35 | "r": 255, 36 | "g": 255, 37 | "b": 255, 38 | "a": 255 39 | }, 40 | "_contentSize": { 41 | "__type__": "cc.Size", 42 | "width": 200, 43 | "height": 200 44 | }, 45 | "_anchorPoint": { 46 | "__type__": "cc.Vec2", 47 | "x": 0.5, 48 | "y": 0.5 49 | }, 50 | "_trs": { 51 | "__type__": "TypedArray", 52 | "ctor": "Float64Array", 53 | "array": [ 54 | 0, 55 | 0, 56 | 0, 57 | 0, 58 | 0, 59 | 0, 60 | 1, 61 | 1, 62 | 1, 63 | 1 64 | ] 65 | }, 66 | "_eulerAngles": { 67 | "__type__": "cc.Vec3", 68 | "x": 0, 69 | "y": 0, 70 | "z": 0 71 | }, 72 | "_skewX": 0, 73 | "_skewY": 0, 74 | "_is3DNode": false, 75 | "_groupIndex": 0, 76 | "groupIndex": 0, 77 | "_id": "" 78 | }, 79 | { 80 | "__type__": "cc.Node", 81 | "_name": "shape", 82 | "_objFlags": 0, 83 | "_parent": { 84 | "__id__": 1 85 | }, 86 | "_children": [], 87 | "_active": true, 88 | "_components": [ 89 | { 90 | "__id__": 3 91 | }, 92 | { 93 | "__id__": 4 94 | } 95 | ], 96 | "_prefab": { 97 | "__id__": 5 98 | }, 99 | "_opacity": 255, 100 | "_color": { 101 | "__type__": "cc.Color", 102 | "r": 83, 103 | "g": 109, 104 | "b": 125, 105 | "a": 255 106 | }, 107 | "_contentSize": { 108 | "__type__": "cc.Size", 109 | "width": 200, 110 | "height": 200 111 | }, 112 | "_anchorPoint": { 113 | "__type__": "cc.Vec2", 114 | "x": 0.5, 115 | "y": 0.5 116 | }, 117 | "_trs": { 118 | "__type__": "TypedArray", 119 | "ctor": "Float64Array", 120 | "array": [ 121 | 0, 122 | 0, 123 | 0, 124 | 0, 125 | 0, 126 | 0, 127 | 1, 128 | 1, 129 | 1, 130 | 1 131 | ] 132 | }, 133 | "_eulerAngles": { 134 | "__type__": "cc.Vec3", 135 | "x": 0, 136 | "y": 0, 137 | "z": 0 138 | }, 139 | "_skewX": 0, 140 | "_skewY": 0, 141 | "_is3DNode": false, 142 | "_groupIndex": 0, 143 | "groupIndex": 0, 144 | "_id": "" 145 | }, 146 | { 147 | "__type__": "cc.Widget", 148 | "_name": "", 149 | "_objFlags": 0, 150 | "node": { 151 | "__id__": 2 152 | }, 153 | "_enabled": true, 154 | "alignMode": 2, 155 | "_target": null, 156 | "_alignFlags": 45, 157 | "_left": 0, 158 | "_right": 0, 159 | "_top": 0, 160 | "_bottom": 0, 161 | "_verticalCenter": 0, 162 | "_horizontalCenter": 0, 163 | "_isAbsLeft": true, 164 | "_isAbsRight": true, 165 | "_isAbsTop": true, 166 | "_isAbsBottom": true, 167 | "_isAbsHorizontalCenter": true, 168 | "_isAbsVerticalCenter": true, 169 | "_originalWidth": 0, 170 | "_originalHeight": 0, 171 | "_id": "" 172 | }, 173 | { 174 | "__type__": "cc.Sprite", 175 | "_name": "", 176 | "_objFlags": 0, 177 | "node": { 178 | "__id__": 2 179 | }, 180 | "_enabled": true, 181 | "_materials": [ 182 | { 183 | "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" 184 | } 185 | ], 186 | "_srcBlendFactor": 770, 187 | "_dstBlendFactor": 771, 188 | "_spriteFrame": { 189 | "__uuid__": "a23235d1-15db-4b95-8439-a2e005bfff91" 190 | }, 191 | "_type": 0, 192 | "_sizeMode": 0, 193 | "_fillType": 0, 194 | "_fillCenter": { 195 | "__type__": "cc.Vec2", 196 | "x": 0, 197 | "y": 0 198 | }, 199 | "_fillStart": 0, 200 | "_fillRange": 0, 201 | "_isTrimmedMode": true, 202 | "_atlas": null, 203 | "_id": "" 204 | }, 205 | { 206 | "__type__": "cc.PrefabInfo", 207 | "root": { 208 | "__id__": 1 209 | }, 210 | "asset": { 211 | "__id__": 0 212 | }, 213 | "fileId": "2csXz9tJJAZadXoIqoXSsJ", 214 | "sync": false 215 | }, 216 | { 217 | "__type__": "cc.Node", 218 | "_name": "label", 219 | "_objFlags": 0, 220 | "_parent": { 221 | "__id__": 1 222 | }, 223 | "_children": [], 224 | "_active": true, 225 | "_components": [ 226 | { 227 | "__id__": 7 228 | } 229 | ], 230 | "_prefab": { 231 | "__id__": 8 232 | }, 233 | "_opacity": 255, 234 | "_color": { 235 | "__type__": "cc.Color", 236 | "r": 255, 237 | "g": 255, 238 | "b": 255, 239 | "a": 255 240 | }, 241 | "_contentSize": { 242 | "__type__": "cc.Size", 243 | "width": 50.05, 244 | "height": 50.4 245 | }, 246 | "_anchorPoint": { 247 | "__type__": "cc.Vec2", 248 | "x": 0.5, 249 | "y": 0.5 250 | }, 251 | "_trs": { 252 | "__type__": "TypedArray", 253 | "ctor": "Float64Array", 254 | "array": [ 255 | 0, 256 | 0, 257 | 0, 258 | 0, 259 | 0, 260 | 0, 261 | 1, 262 | 1, 263 | 1, 264 | 1 265 | ] 266 | }, 267 | "_eulerAngles": { 268 | "__type__": "cc.Vec3", 269 | "x": 0, 270 | "y": 0, 271 | "z": 0 272 | }, 273 | "_skewX": 0, 274 | "_skewY": 0, 275 | "_is3DNode": false, 276 | "_groupIndex": 0, 277 | "groupIndex": 0, 278 | "_id": "" 279 | }, 280 | { 281 | "__type__": "cc.Label", 282 | "_name": "", 283 | "_objFlags": 0, 284 | "node": { 285 | "__id__": 6 286 | }, 287 | "_enabled": true, 288 | "_materials": [ 289 | { 290 | "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" 291 | } 292 | ], 293 | "_srcBlendFactor": 770, 294 | "_dstBlendFactor": 771, 295 | "_string": "123", 296 | "_N$string": "123", 297 | "_fontSize": 30, 298 | "_lineHeight": 40, 299 | "_enableWrapText": true, 300 | "_N$file": null, 301 | "_isSystemFontUsed": true, 302 | "_spacingX": 0, 303 | "_batchAsBitmap": false, 304 | "_styleFlags": 0, 305 | "_underlineHeight": 0, 306 | "_N$horizontalAlign": 1, 307 | "_N$verticalAlign": 1, 308 | "_N$fontFamily": "Arial", 309 | "_N$overflow": 0, 310 | "_N$cacheMode": 0, 311 | "_id": "" 312 | }, 313 | { 314 | "__type__": "cc.PrefabInfo", 315 | "root": { 316 | "__id__": 1 317 | }, 318 | "asset": { 319 | "__id__": 0 320 | }, 321 | "fileId": "21Z/Loit1FuYHTkvdmDCvj", 322 | "sync": false 323 | }, 324 | { 325 | "__type__": "cc.PrefabInfo", 326 | "root": { 327 | "__id__": 1 328 | }, 329 | "asset": { 330 | "__id__": 0 331 | }, 332 | "fileId": "", 333 | "sync": false 334 | } 335 | ] -------------------------------------------------------------------------------- /list-2x/assets/common-cells/shape-label-cell.prefab.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.3.2", 3 | "uuid": "8fa9c17d-2552-411c-9848-7b16bf0208a8", 4 | "importer": "prefab", 5 | "optimizationPolicy": "AUTO", 6 | "asyncLoadAssets": false, 7 | "readonly": false, 8 | "subMetas": {} 9 | } -------------------------------------------------------------------------------- /list-2x/assets/home.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.3", 3 | "uuid": "d1f44971-4e4c-498a-b1a6-7ec63f89ca8b", 4 | "importer": "folder", 5 | "isBundle": false, 6 | "bundleName": "", 7 | "priority": 1, 8 | "compressionType": {}, 9 | "optimizeHotUpdate": {}, 10 | "inlineSpriteFrames": {}, 11 | "isRemoteBundle": {}, 12 | "subMetas": {} 13 | } -------------------------------------------------------------------------------- /list-2x/assets/home/home.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": true, 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": "4d9576dd-d871-436d-9711-395150fe11ee" 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__": 8 79 | }, 80 | { 81 | "__id__": 12 82 | } 83 | ], 84 | "_active": true, 85 | "_components": [ 86 | { 87 | "__id__": 16 88 | }, 89 | { 90 | "__id__": 17 91 | }, 92 | { 93 | "__id__": 18 94 | } 95 | ], 96 | "_prefab": null, 97 | "_opacity": 255, 98 | "_color": { 99 | "__type__": "cc.Color", 100 | "r": 255, 101 | "g": 255, 102 | "b": 255, 103 | "a": 255 104 | }, 105 | "_contentSize": { 106 | "__type__": "cc.Size", 107 | "width": 960, 108 | "height": 640 109 | }, 110 | "_anchorPoint": { 111 | "__type__": "cc.Vec2", 112 | "x": 0.5, 113 | "y": 0.5 114 | }, 115 | "_trs": { 116 | "__type__": "TypedArray", 117 | "ctor": "Float64Array", 118 | "array": [ 119 | 480, 120 | 320, 121 | 0, 122 | 0, 123 | 0, 124 | 0, 125 | 1, 126 | 1, 127 | 1, 128 | 1 129 | ] 130 | }, 131 | "_eulerAngles": { 132 | "__type__": "cc.Vec3", 133 | "x": 0, 134 | "y": 0, 135 | "z": 0 136 | }, 137 | "_skewX": 0, 138 | "_skewY": 0, 139 | "_is3DNode": false, 140 | "_groupIndex": 0, 141 | "groupIndex": 0, 142 | "_id": "a5esZu+45LA5mBpvttspPD" 143 | }, 144 | { 145 | "__type__": "cc.Node", 146 | "_name": "Main Camera", 147 | "_objFlags": 0, 148 | "_parent": { 149 | "__id__": 2 150 | }, 151 | "_children": [], 152 | "_active": true, 153 | "_components": [ 154 | { 155 | "__id__": 4 156 | } 157 | ], 158 | "_prefab": null, 159 | "_opacity": 255, 160 | "_color": { 161 | "__type__": "cc.Color", 162 | "r": 255, 163 | "g": 255, 164 | "b": 255, 165 | "a": 255 166 | }, 167 | "_contentSize": { 168 | "__type__": "cc.Size", 169 | "width": 960, 170 | "height": 640 171 | }, 172 | "_anchorPoint": { 173 | "__type__": "cc.Vec2", 174 | "x": 0.5, 175 | "y": 0.5 176 | }, 177 | "_trs": { 178 | "__type__": "TypedArray", 179 | "ctor": "Float64Array", 180 | "array": [ 181 | 0, 182 | 0, 183 | 0, 184 | 0, 185 | 0, 186 | 0, 187 | 1, 188 | 1, 189 | 1, 190 | 1 191 | ] 192 | }, 193 | "_eulerAngles": { 194 | "__type__": "cc.Vec3", 195 | "x": 0, 196 | "y": 0, 197 | "z": 0 198 | }, 199 | "_skewX": 0, 200 | "_skewY": 0, 201 | "_is3DNode": false, 202 | "_groupIndex": 0, 203 | "groupIndex": 0, 204 | "_id": "e1WoFrQ79G7r4ZuQE3HlNb" 205 | }, 206 | { 207 | "__type__": "cc.Camera", 208 | "_name": "", 209 | "_objFlags": 0, 210 | "node": { 211 | "__id__": 3 212 | }, 213 | "_enabled": true, 214 | "_cullingMask": 4294967295, 215 | "_clearFlags": 7, 216 | "_backgroundColor": { 217 | "__type__": "cc.Color", 218 | "r": 0, 219 | "g": 0, 220 | "b": 0, 221 | "a": 255 222 | }, 223 | "_depth": -1, 224 | "_zoomRatio": 1, 225 | "_targetTexture": null, 226 | "_fov": 60, 227 | "_orthoSize": 10, 228 | "_nearClip": 1, 229 | "_farClip": 4096, 230 | "_ortho": true, 231 | "_rect": { 232 | "__type__": "cc.Rect", 233 | "x": 0, 234 | "y": 0, 235 | "width": 1, 236 | "height": 1 237 | }, 238 | "_renderStages": 1, 239 | "_alignWithScreen": true, 240 | "_id": "81GN3uXINKVLeW4+iKSlim" 241 | }, 242 | { 243 | "__type__": "cc.Node", 244 | "_name": "list1", 245 | "_objFlags": 0, 246 | "_parent": { 247 | "__id__": 2 248 | }, 249 | "_children": [], 250 | "_active": true, 251 | "_components": [ 252 | { 253 | "__id__": 6 254 | } 255 | ], 256 | "_prefab": null, 257 | "_opacity": 255, 258 | "_color": { 259 | "__type__": "cc.Color", 260 | "r": 255, 261 | "g": 255, 262 | "b": 255, 263 | "a": 255 264 | }, 265 | "_contentSize": { 266 | "__type__": "cc.Size", 267 | "width": 310, 268 | "height": 640 269 | }, 270 | "_anchorPoint": { 271 | "__type__": "cc.Vec2", 272 | "x": 0.5, 273 | "y": 0.5 274 | }, 275 | "_trs": { 276 | "__type__": "TypedArray", 277 | "ctor": "Float64Array", 278 | "array": [ 279 | -325, 280 | 0, 281 | 0, 282 | 0, 283 | 0, 284 | 0, 285 | 1, 286 | 1, 287 | 1, 288 | 1 289 | ] 290 | }, 291 | "_eulerAngles": { 292 | "__type__": "cc.Vec3", 293 | "x": 0, 294 | "y": 0, 295 | "z": 0 296 | }, 297 | "_skewX": 0, 298 | "_skewY": 0, 299 | "_is3DNode": false, 300 | "_groupIndex": 0, 301 | "groupIndex": 0, 302 | "_id": "37KB4+AFFHxrAaBW5L4Ycp" 303 | }, 304 | { 305 | "__type__": "62dc08kS7dIiIYJEawwSV8U", 306 | "_name": "", 307 | "_objFlags": 0, 308 | "node": { 309 | "__id__": 5 310 | }, 311 | "_enabled": true, 312 | "mask": true, 313 | "scrollEnabled": true, 314 | "wheelScrollEnabled": true, 315 | "scrollDirection": 1, 316 | "mode": 0, 317 | "preloadNodesLimitPerFrame": 2, 318 | "frameInterval": 1, 319 | "recycleInterval": 1, 320 | "registerCellForEditor": [ 321 | { 322 | "__id__": 7 323 | } 324 | ], 325 | "registerSupplementaryForEditor": [], 326 | "_id": "d7ZzsT1NNNuKRUYTA6KIhf" 327 | }, 328 | { 329 | "__type__": "_yx_editor_register_element_info", 330 | "prefab": { 331 | "__uuid__": "8fa9c17d-2552-411c-9848-7b16bf0208a8" 332 | }, 333 | "identifier": "cell", 334 | "comp": "" 335 | }, 336 | { 337 | "__type__": "cc.Node", 338 | "_name": "list2", 339 | "_objFlags": 0, 340 | "_parent": { 341 | "__id__": 2 342 | }, 343 | "_children": [], 344 | "_active": true, 345 | "_components": [ 346 | { 347 | "__id__": 9 348 | } 349 | ], 350 | "_prefab": null, 351 | "_opacity": 255, 352 | "_color": { 353 | "__type__": "cc.Color", 354 | "r": 255, 355 | "g": 255, 356 | "b": 255, 357 | "a": 255 358 | }, 359 | "_contentSize": { 360 | "__type__": "cc.Size", 361 | "width": 310, 362 | "height": 640 363 | }, 364 | "_anchorPoint": { 365 | "__type__": "cc.Vec2", 366 | "x": 0.5, 367 | "y": 0.5 368 | }, 369 | "_trs": { 370 | "__type__": "TypedArray", 371 | "ctor": "Float64Array", 372 | "array": [ 373 | 0, 374 | 0, 375 | 0, 376 | 0, 377 | 0, 378 | 0, 379 | 1, 380 | 1, 381 | 1, 382 | 1 383 | ] 384 | }, 385 | "_eulerAngles": { 386 | "__type__": "cc.Vec3", 387 | "x": 0, 388 | "y": 0, 389 | "z": 0 390 | }, 391 | "_skewX": 0, 392 | "_skewY": 0, 393 | "_is3DNode": false, 394 | "_groupIndex": 0, 395 | "groupIndex": 0, 396 | "_id": "316iCgSztKvpYfUuRpjZpB" 397 | }, 398 | { 399 | "__type__": "62dc08kS7dIiIYJEawwSV8U", 400 | "_name": "", 401 | "_objFlags": 0, 402 | "node": { 403 | "__id__": 8 404 | }, 405 | "_enabled": true, 406 | "mask": true, 407 | "scrollEnabled": true, 408 | "wheelScrollEnabled": true, 409 | "scrollDirection": 1, 410 | "mode": 0, 411 | "preloadNodesLimitPerFrame": 2, 412 | "frameInterval": 1, 413 | "recycleInterval": 1, 414 | "registerCellForEditor": [ 415 | { 416 | "__id__": 10 417 | } 418 | ], 419 | "registerSupplementaryForEditor": [ 420 | { 421 | "__id__": 11 422 | } 423 | ], 424 | "_id": "6168pMrAdHy5egsiy1RWAl" 425 | }, 426 | { 427 | "__type__": "_yx_editor_register_element_info", 428 | "prefab": { 429 | "__uuid__": "8fa9c17d-2552-411c-9848-7b16bf0208a8" 430 | }, 431 | "identifier": "cell", 432 | "comp": "" 433 | }, 434 | { 435 | "__type__": "_yx_editor_register_element_info", 436 | "prefab": { 437 | "__uuid__": "8fa9c17d-2552-411c-9848-7b16bf0208a8" 438 | }, 439 | "identifier": "supplementary", 440 | "comp": "" 441 | }, 442 | { 443 | "__type__": "cc.Node", 444 | "_name": "list3", 445 | "_objFlags": 0, 446 | "_parent": { 447 | "__id__": 2 448 | }, 449 | "_children": [], 450 | "_active": true, 451 | "_components": [ 452 | { 453 | "__id__": 13 454 | } 455 | ], 456 | "_prefab": null, 457 | "_opacity": 255, 458 | "_color": { 459 | "__type__": "cc.Color", 460 | "r": 255, 461 | "g": 255, 462 | "b": 255, 463 | "a": 255 464 | }, 465 | "_contentSize": { 466 | "__type__": "cc.Size", 467 | "width": 310, 468 | "height": 640 469 | }, 470 | "_anchorPoint": { 471 | "__type__": "cc.Vec2", 472 | "x": 0.5, 473 | "y": 0.5 474 | }, 475 | "_trs": { 476 | "__type__": "TypedArray", 477 | "ctor": "Float64Array", 478 | "array": [ 479 | 325, 480 | 0, 481 | 0, 482 | 0, 483 | 0, 484 | 0, 485 | 1, 486 | 1, 487 | 1, 488 | 1 489 | ] 490 | }, 491 | "_eulerAngles": { 492 | "__type__": "cc.Vec3", 493 | "x": 0, 494 | "y": 0, 495 | "z": 0 496 | }, 497 | "_skewX": 0, 498 | "_skewY": 0, 499 | "_is3DNode": false, 500 | "_groupIndex": 0, 501 | "groupIndex": 0, 502 | "_id": "99jY+AojdA0YZq4ysmw84S" 503 | }, 504 | { 505 | "__type__": "62dc08kS7dIiIYJEawwSV8U", 506 | "_name": "", 507 | "_objFlags": 0, 508 | "node": { 509 | "__id__": 12 510 | }, 511 | "_enabled": true, 512 | "mask": true, 513 | "scrollEnabled": true, 514 | "wheelScrollEnabled": true, 515 | "scrollDirection": 1, 516 | "mode": 0, 517 | "preloadNodesLimitPerFrame": 2, 518 | "frameInterval": 1, 519 | "recycleInterval": 1, 520 | "registerCellForEditor": [ 521 | { 522 | "__id__": 14 523 | } 524 | ], 525 | "registerSupplementaryForEditor": [ 526 | { 527 | "__id__": 15 528 | } 529 | ], 530 | "_id": "4bts8zampKvaCz5ILMmZJA" 531 | }, 532 | { 533 | "__type__": "_yx_editor_register_element_info", 534 | "prefab": { 535 | "__uuid__": "8fa9c17d-2552-411c-9848-7b16bf0208a8" 536 | }, 537 | "identifier": "cell", 538 | "comp": "" 539 | }, 540 | { 541 | "__type__": "_yx_editor_register_element_info", 542 | "prefab": { 543 | "__uuid__": "8fa9c17d-2552-411c-9848-7b16bf0208a8" 544 | }, 545 | "identifier": "supplementary", 546 | "comp": "" 547 | }, 548 | { 549 | "__type__": "cc.Canvas", 550 | "_name": "", 551 | "_objFlags": 0, 552 | "node": { 553 | "__id__": 2 554 | }, 555 | "_enabled": true, 556 | "_designResolution": { 557 | "__type__": "cc.Size", 558 | "width": 960, 559 | "height": 640 560 | }, 561 | "_fitWidth": false, 562 | "_fitHeight": true, 563 | "_id": "59Cd0ovbdF4byw5sbjJDx7" 564 | }, 565 | { 566 | "__type__": "cc.Widget", 567 | "_name": "", 568 | "_objFlags": 0, 569 | "node": { 570 | "__id__": 2 571 | }, 572 | "_enabled": true, 573 | "alignMode": 1, 574 | "_target": null, 575 | "_alignFlags": 45, 576 | "_left": 0, 577 | "_right": 0, 578 | "_top": 0, 579 | "_bottom": 0, 580 | "_verticalCenter": 0, 581 | "_horizontalCenter": 0, 582 | "_isAbsLeft": true, 583 | "_isAbsRight": true, 584 | "_isAbsTop": true, 585 | "_isAbsBottom": true, 586 | "_isAbsHorizontalCenter": true, 587 | "_isAbsVerticalCenter": true, 588 | "_originalWidth": 0, 589 | "_originalHeight": 0, 590 | "_id": "29zXboiXFBKoIV4PQ2liTe" 591 | }, 592 | { 593 | "__type__": "9c45eiLP5NAxIVn+btZm8W1", 594 | "_name": "", 595 | "_objFlags": 0, 596 | "node": { 597 | "__id__": 2 598 | }, 599 | "_enabled": true, 600 | "_id": "560cKL+1lFIZFZSgoBz8At" 601 | } 602 | ] -------------------------------------------------------------------------------- /list-2x/assets/home/home.fire.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.3.2", 3 | "uuid": "4d9576dd-d871-436d-9711-395150fe11ee", 4 | "importer": "scene", 5 | "asyncLoadAssets": false, 6 | "autoReleaseAssets": false, 7 | "subMetas": {} 8 | } -------------------------------------------------------------------------------- /list-2x/assets/home/home.ts: -------------------------------------------------------------------------------- 1 | // Learn TypeScript: 2 | // - https://docs.cocos.com/creator/2.4/manual/en/scripting/typescript.html 3 | // Learn Attribute: 4 | // - https://docs.cocos.com/creator/2.4/manual/en/scripting/reference/attributes.html 5 | // Learn life-cycle callbacks: 6 | // - https://docs.cocos.com/creator/2.4/manual/en/scripting/life-cycle-callbacks.html 7 | 8 | import { YXCollectionView } from "../lib/yx-collection-view"; 9 | import { YXTableLayout } from "../lib/yx-table-layout"; 10 | 11 | const { ccclass, property } = cc._decorator; 12 | 13 | 14 | @ccclass 15 | export default class NewClass extends cc.Component { 16 | 17 | protected start(): void { 18 | this.setup_list1() 19 | this.setup_list2() 20 | this.setup_list3() 21 | } 22 | 23 | setup_list1() { 24 | const listComp = this.node.getChildByName('list1').getComponent(YXCollectionView) 25 | 26 | listComp.numberOfItems = () => 10000 27 | listComp.cellForItemAt = (indexPath, collectionView) => { 28 | const cell = collectionView.dequeueReusableCell(`cell`) 29 | cell.getChildByName('label').getComponent(cc.Label).string = `${indexPath}` 30 | return cell 31 | } 32 | 33 | let layout = new YXTableLayout() 34 | layout.spacing = 20 35 | layout.rowHeight = 100 36 | listComp.layout = layout 37 | 38 | listComp.reloadData() 39 | } 40 | 41 | setup_list2() { 42 | const listComp = this.node.getChildByName('list2').getComponent(YXCollectionView) 43 | 44 | listComp.numberOfSections = () => 100 45 | listComp.supplementaryForItemAt = (indexPath, collectionView, kinds) => { 46 | if (kinds === YXTableLayout.SupplementaryKinds.HEADER) { 47 | const supplementary = collectionView.dequeueReusableSupplementary('supplementary') 48 | supplementary.getChildByName('label').getComponent(cc.Label).string = `header ${indexPath}` 49 | const shape = supplementary.getChildByName('shape') 50 | shape.color = new cc.Color(100, 100, 150) 51 | return supplementary 52 | } 53 | if (kinds === YXTableLayout.SupplementaryKinds.FOOTER) { 54 | const supplementary = collectionView.dequeueReusableSupplementary('supplementary') 55 | supplementary.getChildByName('label').getComponent(cc.Label).string = `footer ${indexPath}` 56 | const shape = supplementary.getChildByName('shape') 57 | shape.color = new cc.Color(150, 100, 100) 58 | return supplementary 59 | } 60 | return null 61 | } 62 | 63 | listComp.numberOfItems = () => 20 64 | listComp.cellForItemAt = (indexPath, collectionView) => { 65 | const cell = collectionView.dequeueReusableCell(`cell`) 66 | cell.getChildByName('label').getComponent(cc.Label).string = `${indexPath}` 67 | return cell 68 | } 69 | 70 | let layout = new YXTableLayout() 71 | layout.spacing = 20 72 | layout.top = 20 73 | layout.bottom = 20 74 | layout.rowHeight = 100 75 | layout.sectionHeaderHeight = 120 76 | layout.sectionFooterHeight = 120 77 | listComp.layout = layout 78 | 79 | listComp.reloadData() 80 | } 81 | 82 | setup_list3() { 83 | const listComp = this.node.getChildByName('list3').getComponent(YXCollectionView) 84 | 85 | listComp.numberOfSections = () => 100 86 | listComp.supplementaryForItemAt = (indexPath, collectionView, kinds) => { 87 | if (kinds === YXTableLayout.SupplementaryKinds.HEADER) { 88 | const supplementary = collectionView.dequeueReusableSupplementary('supplementary') 89 | supplementary.getChildByName('label').getComponent(cc.Label).string = `header ${indexPath}` 90 | const shape = supplementary.getChildByName('shape') 91 | shape.color = new cc.Color(100, 100, 150) 92 | return supplementary 93 | } 94 | if (kinds === YXTableLayout.SupplementaryKinds.FOOTER) { 95 | const supplementary = collectionView.dequeueReusableSupplementary('supplementary') 96 | supplementary.getChildByName('label').getComponent(cc.Label).string = `footer ${indexPath}` 97 | const shape = supplementary.getChildByName('shape') 98 | shape.color = new cc.Color(150, 100, 100) 99 | return supplementary 100 | } 101 | return null 102 | } 103 | 104 | listComp.numberOfItems = () => 20 105 | listComp.cellForItemAt = (indexPath, collectionView) => { 106 | const cell = collectionView.dequeueReusableCell(`cell`) 107 | cell.getChildByName('label').getComponent(cc.Label).string = `${indexPath}` 108 | return cell 109 | } 110 | 111 | let layout = new YXTableLayout() 112 | layout.spacing = 20 113 | layout.top = 20 114 | layout.bottom = 20 115 | layout.rowHeight = 100 116 | layout.sectionHeaderHeight = 120 117 | layout.sectionFooterHeight = 120 118 | layout.sectionHeadersPinToVisibleBounds = true 119 | layout.sectionFootersPinToVisibleBounds = true 120 | listComp.layout = layout 121 | 122 | listComp.reloadData() 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /list-2x/assets/home/home.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.0", 3 | "uuid": "9c45e88b-3f93-40c4-8567-f9bb599bc5b5", 4 | "importer": "typescript", 5 | "isPlugin": false, 6 | "loadPluginInWeb": true, 7 | "loadPluginInNative": true, 8 | "loadPluginInEditor": false, 9 | "subMetas": {} 10 | } -------------------------------------------------------------------------------- /list-2x/assets/lib.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.3", 3 | "uuid": "1c87e019-389f-4b4e-a02d-708e2ae0e462", 4 | "importer": "folder", 5 | "isBundle": false, 6 | "bundleName": "", 7 | "priority": 1, 8 | "compressionType": {}, 9 | "optimizeHotUpdate": {}, 10 | "inlineSpriteFrames": {}, 11 | "isRemoteBundle": {}, 12 | "subMetas": {} 13 | } -------------------------------------------------------------------------------- /list-2x/assets/lib/yx-collection-view.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.0", 3 | "uuid": "62dc0f24-4bb7-4888-8609-11ac30495f14", 4 | "importer": "typescript", 5 | "isPlugin": false, 6 | "loadPluginInWeb": true, 7 | "loadPluginInNative": true, 8 | "loadPluginInEditor": false, 9 | "subMetas": {} 10 | } -------------------------------------------------------------------------------- /list-2x/assets/lib/yx-table-layout.ts: -------------------------------------------------------------------------------- 1 | import { YXCollectionView, YXIndexPath, YXLayout, YXLayoutAttributes } from "./yx-collection-view"; 2 | 3 | enum _yx_table_layout_supplementary_kinds { 4 | HEADER = 'header', 5 | FOOTER = 'footer', 6 | } 7 | 8 | export class YXTableLayout extends YXLayout { 9 | 10 | /** 11 | * 行高 12 | */ 13 | rowHeight: number | ((indexPath: YXIndexPath) => number) = 100 14 | 15 | /** 16 | * 内容上边距 17 | */ 18 | top: number = 0 19 | 20 | /** 21 | * 内容下边距 22 | */ 23 | bottom: number = 0 24 | 25 | /** 26 | * 节点之间间距 27 | */ 28 | spacing: number = 0 29 | 30 | /** 31 | * 区头高度 32 | */ 33 | sectionHeaderHeight: number | ((section: number) => number) = null 34 | 35 | /** 36 | * 区尾高度 37 | */ 38 | sectionFooterHeight: number | ((section: number) => number) = null 39 | 40 | /** 41 | * 钉住 header 的位置 ( header 吸附在列表可见范围内 ) 42 | */ 43 | sectionHeadersPinToVisibleBounds: boolean = false 44 | 45 | /** 46 | * 钉住 footer 的位置 ( footer 吸附在列表可见范围内 ) 47 | */ 48 | sectionFootersPinToVisibleBounds: boolean = false 49 | 50 | /** 51 | * 区头/区尾标识 52 | */ 53 | static SupplementaryKinds = _yx_table_layout_supplementary_kinds 54 | 55 | protected originalHeaderRect: Map = new Map() // 保存所有 header 的原始位置 56 | protected originalFooterRect: Map = new Map() // 保存所有 footer 的原始位置 57 | 58 | // 为了优化查找,额外维护几个数组按类别管理所有的布局属性,空间换时间 59 | protected allCellAttributes: YXLayoutAttributes[] = [] 60 | protected allHeaderAttributes: YXLayoutAttributes[] = [] 61 | protected allFooterAttributes: YXLayoutAttributes[] = [] 62 | 63 | prepare(collectionView: YXCollectionView): void { 64 | // 设置列表的滚动方向(这套布局固定为垂直方向滚动) 65 | collectionView.scrollView.horizontal = false 66 | collectionView.scrollView.vertical = true 67 | if (collectionView.scrollDirection === YXCollectionView.ScrollDirection.HORIZONTAL) { 68 | // 由于这套布局规则只支持垂直方向布局,当外部配置了水平方向滚动时这里可以给个警告 69 | cc.warn(`YXTableLayout 仅支持垂直方向排列`) 70 | } 71 | 72 | // 清空一下布局属性数组 73 | this.attributes = [] 74 | this.allCellAttributes = [] 75 | this.allHeaderAttributes = [] 76 | this.allFooterAttributes = [] 77 | this.originalHeaderRect.clear() 78 | this.originalFooterRect.clear() 79 | 80 | // 获取列表宽度 81 | const contentWidth = collectionView.node.width 82 | 83 | // 声明一个临时变量,用来记录当前所有内容的总高度 84 | let contentHeight = 0 85 | 86 | // 获取列表一共分多少个区 87 | let numberOfSections = collectionView.getNumberOfSections() 88 | 89 | // 为每条数据对应的生成一个布局属性 90 | for (let section = 0; section < numberOfSections; section++) { 91 | 92 | // 创建一个区索引 93 | let sectionIndexPath = new YXIndexPath(section, 0) 94 | 95 | // 通过区索引创建一个区头节点布局属性 96 | let sectionHeaderHeight = 0 97 | if (this.sectionHeaderHeight) { 98 | sectionHeaderHeight = this.sectionHeaderHeight instanceof Function ? this.sectionHeaderHeight(section) : this.sectionHeaderHeight 99 | } 100 | if (sectionHeaderHeight > 0) { 101 | let headerAttr = YXLayoutAttributes.layoutAttributesForSupplementary(sectionIndexPath, YXTableLayout.SupplementaryKinds.HEADER) 102 | 103 | // 确定这个节点的位置 104 | headerAttr.frame.x = 0 105 | headerAttr.frame.width = contentWidth 106 | headerAttr.frame.height = sectionHeaderHeight 107 | headerAttr.frame.y = contentHeight 108 | 109 | // 调整层级 110 | headerAttr.zIndex = 1 111 | 112 | // 重要: 保存布局属性 113 | this.attributes.push(headerAttr) 114 | this.originalHeaderRect.set(section, headerAttr.frame.clone()) 115 | this.allHeaderAttributes.push(headerAttr) 116 | 117 | // 更新整体内容高度 118 | contentHeight = headerAttr.frame.yMax 119 | } 120 | 121 | // 将 top 配置应用到每个区 122 | contentHeight = contentHeight + this.top 123 | 124 | // 获取这个区内的内容数量,注意这里传入的是 section 125 | let numberOfItems = collectionView.getNumberOfItems(section) 126 | 127 | for (let item = 0; item < numberOfItems; item++) { 128 | 129 | // 创建索引,注意这里的 section 已经改为正确的 section 了 130 | let indexPath = new YXIndexPath(section, item) 131 | 132 | // 通过索引创建一个 cell 节点的布局属性 133 | let attr = YXLayoutAttributes.layoutAttributesForCell(indexPath) 134 | 135 | // 通过索引获取这个节点的高度 136 | let rowHeight = this.rowHeight instanceof Function ? this.rowHeight(indexPath) : this.rowHeight 137 | 138 | // 确定这个节点的位置 139 | attr.frame.x = 0 140 | attr.frame.width = contentWidth 141 | attr.frame.height = rowHeight 142 | attr.frame.y = contentHeight + (item > 0 ? this.spacing : 0) 143 | 144 | // 重要: 保存布局属性 145 | this.attributes.push(attr) 146 | this.allCellAttributes.push(attr) 147 | 148 | // 更新当前内容高度 149 | contentHeight = attr.frame.yMax 150 | } 151 | 152 | // 高度补一个底部间距,跟 top 一样,也是应用到每个区 153 | contentHeight = contentHeight + this.bottom 154 | 155 | // 通过区索引创建一个区尾节点布局属性 156 | let sectionFooterHeight = 0 157 | if (this.sectionFooterHeight) { 158 | sectionFooterHeight = this.sectionFooterHeight instanceof Function ? this.sectionFooterHeight(section) : this.sectionFooterHeight 159 | } 160 | if (sectionFooterHeight > 0) { 161 | let footerAttr = YXLayoutAttributes.layoutAttributesForSupplementary(sectionIndexPath, YXTableLayout.SupplementaryKinds.FOOTER) 162 | 163 | // 确定这个节点的位置 164 | footerAttr.frame.x = 0 165 | footerAttr.frame.width = contentWidth 166 | footerAttr.frame.height = sectionFooterHeight 167 | footerAttr.frame.y = contentHeight 168 | 169 | // 调整层级 170 | footerAttr.zIndex = 1 171 | 172 | // 重要: 保存布局属性 173 | this.attributes.push(footerAttr) 174 | this.originalFooterRect.set(section, footerAttr.frame.clone()) 175 | this.allFooterAttributes.push(footerAttr) 176 | 177 | // 更新整体内容高度 178 | contentHeight = footerAttr.frame.yMax 179 | } 180 | } 181 | 182 | // 重要: 设置内容区域总大小,只有确定了滚动区域的大小列表才能滚动 183 | this.contentSize = new cc.Size(contentWidth, contentHeight) 184 | } 185 | 186 | initOffset(collectionView: YXCollectionView): void { 187 | // 列表首次刷新时,调整一下列表的偏移位置 188 | collectionView.scrollView.scrollToTop() 189 | } 190 | 191 | layoutAttributesForElementsInRect(rect: cc.Rect, collectionView: YXCollectionView): YXLayoutAttributes[] { 192 | let result = this.visibleElementsInRect(rect, collectionView) 193 | if (this.sectionHeadersPinToVisibleBounds == false && this.sectionFootersPinToVisibleBounds == false) { 194 | return result // 不需要调整节点位置,直接返回就好 195 | } 196 | 197 | let numberOfSections = collectionView.getNumberOfSections() 198 | let scrollOffset = collectionView.scrollView.getScrollOffset() 199 | for (let index = 0; index < result.length; index++) { 200 | const element = result[index]; 201 | if (element.elementCategory === 'Supplementary') { 202 | 203 | if (this.sectionHeadersPinToVisibleBounds && element.supplementaryKinds === YXTableLayout.SupplementaryKinds.HEADER) { 204 | const originalFrame = this.originalHeaderRect.get(element.indexPath.section) 205 | element.frame.y = originalFrame.y 206 | if (scrollOffset.y > originalFrame.y) { 207 | element.frame.y = scrollOffset.y 208 | } 209 | const nextOriginalFrame = this.getNextOriginalFrame(element.indexPath.section, YXTableLayout.SupplementaryKinds.FOOTER, numberOfSections) 210 | if (nextOriginalFrame) { 211 | if (element.frame.yMax > nextOriginalFrame.y) { 212 | element.frame.y = nextOriginalFrame.y - element.frame.height 213 | } 214 | } 215 | } 216 | 217 | if (this.sectionFootersPinToVisibleBounds && element.supplementaryKinds === YXTableLayout.SupplementaryKinds.FOOTER) { 218 | let bottom = scrollOffset.y + collectionView.scrollView.node.height 219 | const originalFrame = this.originalFooterRect.get(element.indexPath.section) 220 | const previousOriginalFrame = this.getPreviousOriginalFrame(element.indexPath.section, YXTableLayout.SupplementaryKinds.HEADER) 221 | element.frame.y = originalFrame.y 222 | if (bottom < originalFrame.yMax) { 223 | element.frame.y = bottom - element.frame.height 224 | if (previousOriginalFrame) { 225 | if (element.frame.y < previousOriginalFrame.yMax) { 226 | element.frame.y = previousOriginalFrame.yMax 227 | } 228 | } 229 | } 230 | } 231 | } 232 | } 233 | return result 234 | } 235 | 236 | shouldUpdateAttributesZIndex(): boolean { 237 | return this.sectionHeadersPinToVisibleBounds || this.sectionFootersPinToVisibleBounds 238 | } 239 | 240 | shouldUpdateAttributesForBoundsChange(): boolean { 241 | return this.sectionHeadersPinToVisibleBounds || this.sectionFootersPinToVisibleBounds 242 | } 243 | 244 | /** 245 | * 获取 `section` 下一个 header 或者 footer 的位置 246 | */ 247 | protected getNextOriginalFrame(section: number, kinds: _yx_table_layout_supplementary_kinds, total: number) { 248 | if (section >= total) { return null } 249 | if (kinds === YXTableLayout.SupplementaryKinds.HEADER) { 250 | let result = this.originalHeaderRect.get(section) 251 | if (result) { return result } 252 | return this.getNextOriginalFrame(section, YXTableLayout.SupplementaryKinds.FOOTER, total) 253 | } 254 | if (kinds === YXTableLayout.SupplementaryKinds.FOOTER) { 255 | let result = this.originalFooterRect.get(section) 256 | if (result) { return result } 257 | return this.getNextOriginalFrame(section + 1, YXTableLayout.SupplementaryKinds.HEADER, total) 258 | } 259 | return null 260 | } 261 | 262 | /** 263 | * 获取 `section` 前一个 header 或者 footer 的位置 264 | */ 265 | protected getPreviousOriginalFrame(section: number, kinds: _yx_table_layout_supplementary_kinds) { 266 | if (section < 0) { return null } 267 | if (kinds === YXTableLayout.SupplementaryKinds.HEADER) { 268 | let result = this.originalHeaderRect.get(section) 269 | if (result) { return result } 270 | return this.getPreviousOriginalFrame(section - 1, YXTableLayout.SupplementaryKinds.FOOTER) 271 | } 272 | if (kinds === YXTableLayout.SupplementaryKinds.FOOTER) { 273 | let result = this.originalFooterRect.get(section) 274 | if (result) { return result } 275 | return this.getPreviousOriginalFrame(section, YXTableLayout.SupplementaryKinds.HEADER) 276 | } 277 | return null 278 | } 279 | 280 | /** 281 | * 抽出来一个方法用来优化列表性能 282 | * 在优化之前,可以先看一下 @see YXLayout.layoutAttributesForElementsInRect 关于返回值的说明 283 | * 对于有序列表来说,一般都是可以通过二分查找来进行优化 284 | */ 285 | protected visibleElementsInRect(rect: cc.Rect, collectionView: YXCollectionView) { 286 | if (this.attributes.length <= 100) { return this.attributes } // 少量数据就不查了,直接返回全部 287 | 288 | let result: YXLayoutAttributes[] = [] 289 | 290 | // header 跟 footer 暂时不考虑,数据相对来说不算很多,直接全部返回 291 | result.push(...this.allHeaderAttributes) 292 | result.push(...this.allFooterAttributes) 293 | 294 | // 关于 cell,这里用二分查找来优化一下 295 | // 首先通过二分先查出个大概位置 296 | let midIdx = -1 297 | let left = 0 298 | let right = this.allCellAttributes.length - 1 299 | 300 | while (left <= right && right >= 0) { 301 | let mid = left + (right - left) / 2 302 | mid = Math.floor(mid) 303 | let attr = this.allCellAttributes[mid] 304 | if (rect.intersects(attr.frame)) { 305 | midIdx = mid 306 | break 307 | } 308 | if (rect.yMax < attr.frame.yMin || rect.xMax < attr.frame.xMin) { 309 | right = mid - 1 310 | } else { 311 | left = mid + 1 312 | } 313 | } 314 | 315 | // 二分查找出错了,返回全部的布局属性 316 | if (midIdx < 0) { 317 | return this.attributes 318 | } 319 | 320 | // 把模糊查到这个先加进来 321 | result.push(this.allCellAttributes[midIdx]) 322 | 323 | // 然后依次往前检查,直到超出当前的显示范围 324 | let startIdx = midIdx 325 | while (startIdx > 0) { 326 | let idx = startIdx - 1 327 | let attr = this.allCellAttributes[idx] 328 | if (rect.intersects(attr.frame) == false) { 329 | break 330 | } 331 | result.push(attr) 332 | startIdx = idx 333 | } 334 | 335 | // 依次往后检查,直到超出当前的显示范围 336 | let endIdx = midIdx 337 | while (endIdx < this.allCellAttributes.length - 1) { 338 | let idx = endIdx + 1 339 | let attr = this.allCellAttributes[idx] 340 | if (rect.intersects(attr.frame) == false) { 341 | break 342 | } 343 | result.push(attr) 344 | endIdx = idx 345 | } 346 | 347 | return result 348 | } 349 | } -------------------------------------------------------------------------------- /list-2x/assets/lib/yx-table-layout.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.0", 3 | "uuid": "5f2b7203-9892-41ce-a233-17f1824d0dd8", 4 | "importer": "typescript", 5 | "isPlugin": false, 6 | "loadPluginInWeb": true, 7 | "loadPluginInNative": true, 8 | "loadPluginInEditor": false, 9 | "subMetas": {} 10 | } -------------------------------------------------------------------------------- /list-2x/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "experimentalDecorators": true 6 | }, 7 | "exclude": [ 8 | "node_modules", 9 | ".vscode", 10 | "library", 11 | "local", 12 | "settings", 13 | "temp" 14 | ] 15 | } -------------------------------------------------------------------------------- /list-2x/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "engine": "cocos-creator-js", 3 | "packages": "packages", 4 | "name": "list-2x", 5 | "id": "ad2910f0-61a5-4082-95be-098c958b8cb0", 6 | "version": "2.4.13", 7 | "isNew": false 8 | } -------------------------------------------------------------------------------- /list-2x/settings/project.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /list-2x/settings/services.json: -------------------------------------------------------------------------------- 1 | { 2 | "game": { 3 | "name": "未知游戏", 4 | "appid": "UNKNOW" 5 | } 6 | } -------------------------------------------------------------------------------- /list-2x/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 | } -------------------------------------------------------------------------------- /list-3x/.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 -------------------------------------------------------------------------------- /list-3x/.creator/default-meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": { 3 | "type": "sprite-frame" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /list-3x/.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/ -------------------------------------------------------------------------------- /list-3x/assets/common-cells.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.2.0", 3 | "importer": "directory", 4 | "imported": true, 5 | "uuid": "22474814-fac5-49b1-aece-dfc04017fc3f", 6 | "files": [], 7 | "subMetas": {}, 8 | "userData": {} 9 | } 10 | -------------------------------------------------------------------------------- /list-3x/assets/common-cells/shape-label-cell.prefab: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "__type__": "cc.Prefab", 4 | "_name": "shape-label-cell", 5 | "_objFlags": 0, 6 | "__editorExtras__": {}, 7 | "_native": "", 8 | "data": { 9 | "__id__": 1 10 | }, 11 | "optimizationPolicy": 0, 12 | "persistent": false 13 | }, 14 | { 15 | "__type__": "cc.Node", 16 | "_name": "shape-label-cell", 17 | "_objFlags": 0, 18 | "__editorExtras__": {}, 19 | "_parent": null, 20 | "_children": [ 21 | { 22 | "__id__": 2 23 | }, 24 | { 25 | "__id__": 10 26 | } 27 | ], 28 | "_active": true, 29 | "_components": [ 30 | { 31 | "__id__": 16 32 | } 33 | ], 34 | "_prefab": { 35 | "__id__": 18 36 | }, 37 | "_lpos": { 38 | "__type__": "cc.Vec3", 39 | "x": 0, 40 | "y": 0, 41 | "z": 0 42 | }, 43 | "_lrot": { 44 | "__type__": "cc.Quat", 45 | "x": 0, 46 | "y": 0, 47 | "z": 0, 48 | "w": 1 49 | }, 50 | "_lscale": { 51 | "__type__": "cc.Vec3", 52 | "x": 1, 53 | "y": 1, 54 | "z": 1 55 | }, 56 | "_mobility": 0, 57 | "_layer": 33554432, 58 | "_euler": { 59 | "__type__": "cc.Vec3", 60 | "x": 0, 61 | "y": 0, 62 | "z": 0 63 | }, 64 | "_id": "" 65 | }, 66 | { 67 | "__type__": "cc.Node", 68 | "_name": "shape", 69 | "_objFlags": 0, 70 | "__editorExtras__": {}, 71 | "_parent": { 72 | "__id__": 1 73 | }, 74 | "_children": [], 75 | "_active": true, 76 | "_components": [ 77 | { 78 | "__id__": 3 79 | }, 80 | { 81 | "__id__": 5 82 | }, 83 | { 84 | "__id__": 7 85 | } 86 | ], 87 | "_prefab": { 88 | "__id__": 9 89 | }, 90 | "_lpos": { 91 | "__type__": "cc.Vec3", 92 | "x": 0, 93 | "y": 0, 94 | "z": 0 95 | }, 96 | "_lrot": { 97 | "__type__": "cc.Quat", 98 | "x": 0, 99 | "y": 0, 100 | "z": 0, 101 | "w": 1 102 | }, 103 | "_lscale": { 104 | "__type__": "cc.Vec3", 105 | "x": 1, 106 | "y": 1, 107 | "z": 1 108 | }, 109 | "_mobility": 0, 110 | "_layer": 33554432, 111 | "_euler": { 112 | "__type__": "cc.Vec3", 113 | "x": 0, 114 | "y": 0, 115 | "z": 0 116 | }, 117 | "_id": "" 118 | }, 119 | { 120 | "__type__": "cc.UITransform", 121 | "_name": "", 122 | "_objFlags": 0, 123 | "__editorExtras__": {}, 124 | "node": { 125 | "__id__": 2 126 | }, 127 | "_enabled": true, 128 | "__prefab": { 129 | "__id__": 4 130 | }, 131 | "_contentSize": { 132 | "__type__": "cc.Size", 133 | "width": 400, 134 | "height": 200 135 | }, 136 | "_anchorPoint": { 137 | "__type__": "cc.Vec2", 138 | "x": 0.5, 139 | "y": 0.5 140 | }, 141 | "_id": "" 142 | }, 143 | { 144 | "__type__": "cc.CompPrefabInfo", 145 | "fileId": "38Ctl8rrVBIIzUsgoEVl18" 146 | }, 147 | { 148 | "__type__": "cc.Sprite", 149 | "_name": "", 150 | "_objFlags": 0, 151 | "__editorExtras__": {}, 152 | "node": { 153 | "__id__": 2 154 | }, 155 | "_enabled": true, 156 | "__prefab": { 157 | "__id__": 6 158 | }, 159 | "_customMaterial": null, 160 | "_srcBlendFactor": 2, 161 | "_dstBlendFactor": 4, 162 | "_color": { 163 | "__type__": "cc.Color", 164 | "r": 56, 165 | "g": 81, 166 | "b": 85, 167 | "a": 255 168 | }, 169 | "_spriteFrame": { 170 | "__uuid__": "7d8f9b89-4fd1-4c9f-a3ab-38ec7cded7ca@f9941", 171 | "__expectedType__": "cc.SpriteFrame" 172 | }, 173 | "_type": 0, 174 | "_fillType": 0, 175 | "_sizeMode": 0, 176 | "_fillCenter": { 177 | "__type__": "cc.Vec2", 178 | "x": 0, 179 | "y": 0 180 | }, 181 | "_fillStart": 0, 182 | "_fillRange": 0, 183 | "_isTrimmedMode": true, 184 | "_useGrayscale": false, 185 | "_atlas": null, 186 | "_id": "" 187 | }, 188 | { 189 | "__type__": "cc.CompPrefabInfo", 190 | "fileId": "e5z4rNsp5IqKHKYrZmbbGr" 191 | }, 192 | { 193 | "__type__": "cc.Widget", 194 | "_name": "", 195 | "_objFlags": 0, 196 | "__editorExtras__": {}, 197 | "node": { 198 | "__id__": 2 199 | }, 200 | "_enabled": true, 201 | "__prefab": { 202 | "__id__": 8 203 | }, 204 | "_alignFlags": 45, 205 | "_target": null, 206 | "_left": 0, 207 | "_right": 0, 208 | "_top": 0, 209 | "_bottom": 0, 210 | "_horizontalCenter": 0, 211 | "_verticalCenter": 0, 212 | "_isAbsLeft": true, 213 | "_isAbsRight": true, 214 | "_isAbsTop": true, 215 | "_isAbsBottom": true, 216 | "_isAbsHorizontalCenter": true, 217 | "_isAbsVerticalCenter": true, 218 | "_originalWidth": 100, 219 | "_originalHeight": 100, 220 | "_alignMode": 1, 221 | "_lockFlags": 45, 222 | "_id": "" 223 | }, 224 | { 225 | "__type__": "cc.CompPrefabInfo", 226 | "fileId": "16YuS9Y1RIHL3+RkLAY9ri" 227 | }, 228 | { 229 | "__type__": "cc.PrefabInfo", 230 | "root": { 231 | "__id__": 1 232 | }, 233 | "asset": { 234 | "__id__": 0 235 | }, 236 | "fileId": "50E3C8mxhHcLBLSMVIqx2D", 237 | "instance": null, 238 | "targetOverrides": null, 239 | "nestedPrefabInstanceRoots": null 240 | }, 241 | { 242 | "__type__": "cc.Node", 243 | "_name": "label", 244 | "_objFlags": 0, 245 | "__editorExtras__": {}, 246 | "_parent": { 247 | "__id__": 1 248 | }, 249 | "_children": [], 250 | "_active": true, 251 | "_components": [ 252 | { 253 | "__id__": 11 254 | }, 255 | { 256 | "__id__": 13 257 | } 258 | ], 259 | "_prefab": { 260 | "__id__": 15 261 | }, 262 | "_lpos": { 263 | "__type__": "cc.Vec3", 264 | "x": 0, 265 | "y": 0, 266 | "z": 0 267 | }, 268 | "_lrot": { 269 | "__type__": "cc.Quat", 270 | "x": 0, 271 | "y": 0, 272 | "z": 0, 273 | "w": 1 274 | }, 275 | "_lscale": { 276 | "__type__": "cc.Vec3", 277 | "x": 1, 278 | "y": 1, 279 | "z": 1 280 | }, 281 | "_mobility": 0, 282 | "_layer": 33554432, 283 | "_euler": { 284 | "__type__": "cc.Vec3", 285 | "x": 0, 286 | "y": 0, 287 | "z": 0 288 | }, 289 | "_id": "" 290 | }, 291 | { 292 | "__type__": "cc.UITransform", 293 | "_name": "", 294 | "_objFlags": 0, 295 | "__editorExtras__": {}, 296 | "node": { 297 | "__id__": 10 298 | }, 299 | "_enabled": true, 300 | "__prefab": { 301 | "__id__": 12 302 | }, 303 | "_contentSize": { 304 | "__type__": "cc.Size", 305 | "width": 63.37689490176152, 306 | "height": 50.4 307 | }, 308 | "_anchorPoint": { 309 | "__type__": "cc.Vec2", 310 | "x": 0.5, 311 | "y": 0.5 312 | }, 313 | "_id": "" 314 | }, 315 | { 316 | "__type__": "cc.CompPrefabInfo", 317 | "fileId": "227SaOS3lF15buT8rVMsrU" 318 | }, 319 | { 320 | "__type__": "cc.Label", 321 | "_name": "", 322 | "_objFlags": 0, 323 | "__editorExtras__": {}, 324 | "node": { 325 | "__id__": 10 326 | }, 327 | "_enabled": true, 328 | "__prefab": { 329 | "__id__": 14 330 | }, 331 | "_customMaterial": null, 332 | "_srcBlendFactor": 2, 333 | "_dstBlendFactor": 4, 334 | "_color": { 335 | "__type__": "cc.Color", 336 | "r": 255, 337 | "g": 255, 338 | "b": 255, 339 | "a": 255 340 | }, 341 | "_string": "label", 342 | "_horizontalAlign": 1, 343 | "_verticalAlign": 1, 344 | "_actualFontSize": 34.59375, 345 | "_fontSize": 30, 346 | "_fontFamily": "Arial", 347 | "_lineHeight": 40, 348 | "_overflow": 0, 349 | "_enableWrapText": true, 350 | "_font": null, 351 | "_isSystemFontUsed": true, 352 | "_spacingX": 0, 353 | "_isItalic": false, 354 | "_isBold": false, 355 | "_isUnderline": false, 356 | "_underlineHeight": 2, 357 | "_cacheMode": 0, 358 | "_id": "" 359 | }, 360 | { 361 | "__type__": "cc.CompPrefabInfo", 362 | "fileId": "452rD9FHJNEZN1iBdm4Ra3" 363 | }, 364 | { 365 | "__type__": "cc.PrefabInfo", 366 | "root": { 367 | "__id__": 1 368 | }, 369 | "asset": { 370 | "__id__": 0 371 | }, 372 | "fileId": "db55RKuw1JC52K7YOxNE+s", 373 | "instance": null, 374 | "targetOverrides": null, 375 | "nestedPrefabInstanceRoots": null 376 | }, 377 | { 378 | "__type__": "cc.UITransform", 379 | "_name": "", 380 | "_objFlags": 0, 381 | "__editorExtras__": {}, 382 | "node": { 383 | "__id__": 1 384 | }, 385 | "_enabled": true, 386 | "__prefab": { 387 | "__id__": 17 388 | }, 389 | "_contentSize": { 390 | "__type__": "cc.Size", 391 | "width": 400, 392 | "height": 200 393 | }, 394 | "_anchorPoint": { 395 | "__type__": "cc.Vec2", 396 | "x": 0.5, 397 | "y": 0.5 398 | }, 399 | "_id": "" 400 | }, 401 | { 402 | "__type__": "cc.CompPrefabInfo", 403 | "fileId": "65OHSX1lBN0p7gbe3Wj+iU" 404 | }, 405 | { 406 | "__type__": "cc.PrefabInfo", 407 | "root": { 408 | "__id__": 1 409 | }, 410 | "asset": { 411 | "__id__": 0 412 | }, 413 | "fileId": "aanzQWNlJP9JX9MJIPpZk7", 414 | "targetOverrides": null 415 | } 416 | ] -------------------------------------------------------------------------------- /list-3x/assets/common-cells/shape-label-cell.prefab.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.45", 3 | "importer": "prefab", 4 | "imported": true, 5 | "uuid": "c8b689a4-ae47-4041-8c9c-7298939e76be", 6 | "files": [ 7 | ".json" 8 | ], 9 | "subMetas": {}, 10 | "userData": { 11 | "syncNodeName": "shape-label-cell" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /list-3x/assets/home.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.2.0", 3 | "importer": "directory", 4 | "imported": true, 5 | "uuid": "46e5f5cb-a3ef-4dd6-a3e0-f2475af0089d", 6 | "files": [], 7 | "subMetas": {}, 8 | "userData": {} 9 | } 10 | -------------------------------------------------------------------------------- /list-3x/assets/home/home.scene: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "__type__": "cc.SceneAsset", 4 | "_name": "home", 5 | "_objFlags": 0, 6 | "__editorExtras__": {}, 7 | "_native": "", 8 | "scene": { 9 | "__id__": 1 10 | } 11 | }, 12 | { 13 | "__type__": "cc.Scene", 14 | "_name": "home", 15 | "_objFlags": 0, 16 | "__editorExtras__": {}, 17 | "_parent": null, 18 | "_children": [ 19 | { 20 | "__id__": 2 21 | } 22 | ], 23 | "_active": true, 24 | "_components": [], 25 | "_prefab": { 26 | "__id__": 23 27 | }, 28 | "_lpos": { 29 | "__type__": "cc.Vec3", 30 | "x": 0, 31 | "y": 0, 32 | "z": 0 33 | }, 34 | "_lrot": { 35 | "__type__": "cc.Quat", 36 | "x": 0, 37 | "y": 0, 38 | "z": 0, 39 | "w": 1 40 | }, 41 | "_lscale": { 42 | "__type__": "cc.Vec3", 43 | "x": 1, 44 | "y": 1, 45 | "z": 1 46 | }, 47 | "_mobility": 0, 48 | "_layer": 1073741824, 49 | "_euler": { 50 | "__type__": "cc.Vec3", 51 | "x": 0, 52 | "y": 0, 53 | "z": 0 54 | }, 55 | "autoReleaseAssets": false, 56 | "_globals": { 57 | "__id__": 24 58 | }, 59 | "_id": "5d2f3c26-c7d2-48bd-bd13-ca5aa20a93c4" 60 | }, 61 | { 62 | "__type__": "cc.Node", 63 | "_name": "Canvas", 64 | "_objFlags": 0, 65 | "__editorExtras__": {}, 66 | "_parent": { 67 | "__id__": 1 68 | }, 69 | "_children": [ 70 | { 71 | "__id__": 3 72 | }, 73 | { 74 | "__id__": 5 75 | }, 76 | { 77 | "__id__": 9 78 | }, 79 | { 80 | "__id__": 14 81 | } 82 | ], 83 | "_active": true, 84 | "_components": [ 85 | { 86 | "__id__": 19 87 | }, 88 | { 89 | "__id__": 20 90 | }, 91 | { 92 | "__id__": 21 93 | }, 94 | { 95 | "__id__": 22 96 | } 97 | ], 98 | "_prefab": null, 99 | "_lpos": { 100 | "__type__": "cc.Vec3", 101 | "x": 640, 102 | "y": 360.00000000000006, 103 | "z": 0 104 | }, 105 | "_lrot": { 106 | "__type__": "cc.Quat", 107 | "x": 0, 108 | "y": 0, 109 | "z": 0, 110 | "w": 1 111 | }, 112 | "_lscale": { 113 | "__type__": "cc.Vec3", 114 | "x": 1, 115 | "y": 1, 116 | "z": 1 117 | }, 118 | "_mobility": 0, 119 | "_layer": 33554432, 120 | "_euler": { 121 | "__type__": "cc.Vec3", 122 | "x": 0, 123 | "y": 0, 124 | "z": 0 125 | }, 126 | "_id": "beI88Z2HpFELqR4T5EMHpg" 127 | }, 128 | { 129 | "__type__": "cc.Node", 130 | "_name": "Camera", 131 | "_objFlags": 0, 132 | "__editorExtras__": {}, 133 | "_parent": { 134 | "__id__": 2 135 | }, 136 | "_children": [], 137 | "_active": true, 138 | "_components": [ 139 | { 140 | "__id__": 4 141 | } 142 | ], 143 | "_prefab": null, 144 | "_lpos": { 145 | "__type__": "cc.Vec3", 146 | "x": 0, 147 | "y": 0, 148 | "z": 1000 149 | }, 150 | "_lrot": { 151 | "__type__": "cc.Quat", 152 | "x": 0, 153 | "y": 0, 154 | "z": 0, 155 | "w": 1 156 | }, 157 | "_lscale": { 158 | "__type__": "cc.Vec3", 159 | "x": 1, 160 | "y": 1, 161 | "z": 1 162 | }, 163 | "_mobility": 0, 164 | "_layer": 1073741824, 165 | "_euler": { 166 | "__type__": "cc.Vec3", 167 | "x": 0, 168 | "y": 0, 169 | "z": 0 170 | }, 171 | "_id": "ebFwiq8gBFaYpqYbdoDODe" 172 | }, 173 | { 174 | "__type__": "cc.Camera", 175 | "_name": "", 176 | "_objFlags": 0, 177 | "__editorExtras__": {}, 178 | "node": { 179 | "__id__": 3 180 | }, 181 | "_enabled": true, 182 | "__prefab": null, 183 | "_projection": 0, 184 | "_priority": 0, 185 | "_fov": 45, 186 | "_fovAxis": 0, 187 | "_orthoHeight": 415.3929539295393, 188 | "_near": 0, 189 | "_far": 1000, 190 | "_color": { 191 | "__type__": "cc.Color", 192 | "r": 0, 193 | "g": 0, 194 | "b": 0, 195 | "a": 255 196 | }, 197 | "_depth": 1, 198 | "_stencil": 0, 199 | "_clearFlags": 7, 200 | "_rect": { 201 | "__type__": "cc.Rect", 202 | "x": 0, 203 | "y": 0, 204 | "width": 1, 205 | "height": 1 206 | }, 207 | "_aperture": 19, 208 | "_shutter": 7, 209 | "_iso": 0, 210 | "_screenScale": 1, 211 | "_visibility": 1108344832, 212 | "_targetTexture": null, 213 | "_postProcess": null, 214 | "_usePostProcess": false, 215 | "_cameraType": -1, 216 | "_trackingType": 0, 217 | "_id": "63WIch3o5BEYRlXzTT0oWc" 218 | }, 219 | { 220 | "__type__": "cc.Node", 221 | "_name": "list1", 222 | "_objFlags": 0, 223 | "__editorExtras__": {}, 224 | "_parent": { 225 | "__id__": 2 226 | }, 227 | "_children": [], 228 | "_active": true, 229 | "_components": [ 230 | { 231 | "__id__": 6 232 | }, 233 | { 234 | "__id__": 7 235 | } 236 | ], 237 | "_prefab": null, 238 | "_lpos": { 239 | "__type__": "cc.Vec3", 240 | "x": -420, 241 | "y": 0, 242 | "z": 0 243 | }, 244 | "_lrot": { 245 | "__type__": "cc.Quat", 246 | "x": 0, 247 | "y": 0, 248 | "z": 0, 249 | "w": 1 250 | }, 251 | "_lscale": { 252 | "__type__": "cc.Vec3", 253 | "x": 1, 254 | "y": 1, 255 | "z": 1 256 | }, 257 | "_mobility": 0, 258 | "_layer": 33554432, 259 | "_euler": { 260 | "__type__": "cc.Vec3", 261 | "x": 0, 262 | "y": 0, 263 | "z": 0 264 | }, 265 | "_id": "d1/dCc9yNJ44lI4TRGfy4z" 266 | }, 267 | { 268 | "__type__": "cc.UITransform", 269 | "_name": "", 270 | "_objFlags": 0, 271 | "__editorExtras__": {}, 272 | "node": { 273 | "__id__": 5 274 | }, 275 | "_enabled": true, 276 | "__prefab": null, 277 | "_contentSize": { 278 | "__type__": "cc.Size", 279 | "width": 400, 280 | "height": 700 281 | }, 282 | "_anchorPoint": { 283 | "__type__": "cc.Vec2", 284 | "x": 0.5, 285 | "y": 0.5 286 | }, 287 | "_id": "315xpp6rxGVIpwfAByHAu2" 288 | }, 289 | { 290 | "__type__": "f94cbxum3xAsomhVNW1piY0", 291 | "_name": "", 292 | "_objFlags": 0, 293 | "__editorExtras__": {}, 294 | "node": { 295 | "__id__": 5 296 | }, 297 | "_enabled": true, 298 | "__prefab": null, 299 | "mask": true, 300 | "scrollEnabled": true, 301 | "wheelScrollEnabled": true, 302 | "scrollDirection": 1, 303 | "mode": 0, 304 | "preloadNodesLimitPerFrame": 2, 305 | "frameInterval": 1, 306 | "recycleInterval": 1, 307 | "registerCellForEditor": [ 308 | { 309 | "__id__": 8 310 | } 311 | ], 312 | "registerSupplementaryForEditor": [], 313 | "_id": "540sP7RuVMr66Fyji3wiJs" 314 | }, 315 | { 316 | "__type__": "_yx_editor_register_element_info", 317 | "prefab": { 318 | "__uuid__": "c8b689a4-ae47-4041-8c9c-7298939e76be", 319 | "__expectedType__": "cc.Prefab" 320 | }, 321 | "identifier": "cell", 322 | "comp": "" 323 | }, 324 | { 325 | "__type__": "cc.Node", 326 | "_name": "list2", 327 | "_objFlags": 0, 328 | "__editorExtras__": {}, 329 | "_parent": { 330 | "__id__": 2 331 | }, 332 | "_children": [], 333 | "_active": true, 334 | "_components": [ 335 | { 336 | "__id__": 10 337 | }, 338 | { 339 | "__id__": 11 340 | } 341 | ], 342 | "_prefab": null, 343 | "_lpos": { 344 | "__type__": "cc.Vec3", 345 | "x": 0, 346 | "y": 0, 347 | "z": 0 348 | }, 349 | "_lrot": { 350 | "__type__": "cc.Quat", 351 | "x": 0, 352 | "y": 0, 353 | "z": 0, 354 | "w": 1 355 | }, 356 | "_lscale": { 357 | "__type__": "cc.Vec3", 358 | "x": 1, 359 | "y": 1, 360 | "z": 1 361 | }, 362 | "_mobility": 0, 363 | "_layer": 33554432, 364 | "_euler": { 365 | "__type__": "cc.Vec3", 366 | "x": 0, 367 | "y": 0, 368 | "z": 0 369 | }, 370 | "_id": "15UUoyfNhFtISYnWUZ/NMT" 371 | }, 372 | { 373 | "__type__": "cc.UITransform", 374 | "_name": "", 375 | "_objFlags": 0, 376 | "__editorExtras__": {}, 377 | "node": { 378 | "__id__": 9 379 | }, 380 | "_enabled": true, 381 | "__prefab": null, 382 | "_contentSize": { 383 | "__type__": "cc.Size", 384 | "width": 400, 385 | "height": 700 386 | }, 387 | "_anchorPoint": { 388 | "__type__": "cc.Vec2", 389 | "x": 0.5, 390 | "y": 0.5 391 | }, 392 | "_id": "61HeNMUnxBJ4RjTTIkj7jX" 393 | }, 394 | { 395 | "__type__": "f94cbxum3xAsomhVNW1piY0", 396 | "_name": "", 397 | "_objFlags": 0, 398 | "__editorExtras__": {}, 399 | "node": { 400 | "__id__": 9 401 | }, 402 | "_enabled": true, 403 | "__prefab": null, 404 | "mask": true, 405 | "scrollEnabled": true, 406 | "wheelScrollEnabled": true, 407 | "scrollDirection": 1, 408 | "mode": 0, 409 | "preloadNodesLimitPerFrame": 2, 410 | "frameInterval": 1, 411 | "recycleInterval": 1, 412 | "registerCellForEditor": [ 413 | { 414 | "__id__": 12 415 | } 416 | ], 417 | "registerSupplementaryForEditor": [ 418 | { 419 | "__id__": 13 420 | } 421 | ], 422 | "_id": "557IDOUVFMgaXUVLj9KNOh" 423 | }, 424 | { 425 | "__type__": "_yx_editor_register_element_info", 426 | "prefab": { 427 | "__uuid__": "c8b689a4-ae47-4041-8c9c-7298939e76be", 428 | "__expectedType__": "cc.Prefab" 429 | }, 430 | "identifier": "cell", 431 | "comp": "" 432 | }, 433 | { 434 | "__type__": "_yx_editor_register_element_info", 435 | "prefab": { 436 | "__uuid__": "c8b689a4-ae47-4041-8c9c-7298939e76be", 437 | "__expectedType__": "cc.Prefab" 438 | }, 439 | "identifier": "supplementary", 440 | "comp": "" 441 | }, 442 | { 443 | "__type__": "cc.Node", 444 | "_name": "list3", 445 | "_objFlags": 0, 446 | "__editorExtras__": {}, 447 | "_parent": { 448 | "__id__": 2 449 | }, 450 | "_children": [], 451 | "_active": true, 452 | "_components": [ 453 | { 454 | "__id__": 15 455 | }, 456 | { 457 | "__id__": 16 458 | } 459 | ], 460 | "_prefab": null, 461 | "_lpos": { 462 | "__type__": "cc.Vec3", 463 | "x": 420, 464 | "y": 0, 465 | "z": 0 466 | }, 467 | "_lrot": { 468 | "__type__": "cc.Quat", 469 | "x": 0, 470 | "y": 0, 471 | "z": 0, 472 | "w": 1 473 | }, 474 | "_lscale": { 475 | "__type__": "cc.Vec3", 476 | "x": 1, 477 | "y": 1, 478 | "z": 1 479 | }, 480 | "_mobility": 0, 481 | "_layer": 33554432, 482 | "_euler": { 483 | "__type__": "cc.Vec3", 484 | "x": 0, 485 | "y": 0, 486 | "z": 0 487 | }, 488 | "_id": "fdY0keATpIKb8Mxuy2Wwhd" 489 | }, 490 | { 491 | "__type__": "cc.UITransform", 492 | "_name": "", 493 | "_objFlags": 0, 494 | "__editorExtras__": {}, 495 | "node": { 496 | "__id__": 14 497 | }, 498 | "_enabled": true, 499 | "__prefab": null, 500 | "_contentSize": { 501 | "__type__": "cc.Size", 502 | "width": 400, 503 | "height": 700 504 | }, 505 | "_anchorPoint": { 506 | "__type__": "cc.Vec2", 507 | "x": 0.5, 508 | "y": 0.5 509 | }, 510 | "_id": "6agiejTfZKQIWskmHSuyHv" 511 | }, 512 | { 513 | "__type__": "f94cbxum3xAsomhVNW1piY0", 514 | "_name": "", 515 | "_objFlags": 0, 516 | "__editorExtras__": {}, 517 | "node": { 518 | "__id__": 14 519 | }, 520 | "_enabled": true, 521 | "__prefab": null, 522 | "mask": true, 523 | "scrollEnabled": true, 524 | "wheelScrollEnabled": true, 525 | "scrollDirection": 1, 526 | "mode": 0, 527 | "preloadNodesLimitPerFrame": 2, 528 | "frameInterval": 1, 529 | "recycleInterval": 1, 530 | "registerCellForEditor": [ 531 | { 532 | "__id__": 17 533 | } 534 | ], 535 | "registerSupplementaryForEditor": [ 536 | { 537 | "__id__": 18 538 | } 539 | ], 540 | "_id": "e75snCyRxN9KrcOvFeGEnP" 541 | }, 542 | { 543 | "__type__": "_yx_editor_register_element_info", 544 | "prefab": { 545 | "__uuid__": "c8b689a4-ae47-4041-8c9c-7298939e76be", 546 | "__expectedType__": "cc.Prefab" 547 | }, 548 | "identifier": "cell", 549 | "comp": "" 550 | }, 551 | { 552 | "__type__": "_yx_editor_register_element_info", 553 | "prefab": { 554 | "__uuid__": "c8b689a4-ae47-4041-8c9c-7298939e76be", 555 | "__expectedType__": "cc.Prefab" 556 | }, 557 | "identifier": "supplementary", 558 | "comp": "" 559 | }, 560 | { 561 | "__type__": "cc.UITransform", 562 | "_name": "", 563 | "_objFlags": 0, 564 | "__editorExtras__": {}, 565 | "node": { 566 | "__id__": 2 567 | }, 568 | "_enabled": true, 569 | "__prefab": null, 570 | "_contentSize": { 571 | "__type__": "cc.Size", 572 | "width": 1280, 573 | "height": 720 574 | }, 575 | "_anchorPoint": { 576 | "__type__": "cc.Vec2", 577 | "x": 0.5, 578 | "y": 0.5 579 | }, 580 | "_id": "d6rUX5yfhMlKoWX2bSbawx" 581 | }, 582 | { 583 | "__type__": "cc.Canvas", 584 | "_name": "", 585 | "_objFlags": 0, 586 | "__editorExtras__": {}, 587 | "node": { 588 | "__id__": 2 589 | }, 590 | "_enabled": true, 591 | "__prefab": null, 592 | "_cameraComponent": { 593 | "__id__": 4 594 | }, 595 | "_alignCanvasWithScreen": true, 596 | "_id": "12O/ljcVlEqLmVm3U2gEOQ" 597 | }, 598 | { 599 | "__type__": "cc.Widget", 600 | "_name": "", 601 | "_objFlags": 0, 602 | "__editorExtras__": {}, 603 | "node": { 604 | "__id__": 2 605 | }, 606 | "_enabled": true, 607 | "__prefab": null, 608 | "_alignFlags": 45, 609 | "_target": null, 610 | "_left": 0, 611 | "_right": 0, 612 | "_top": 5.684341886080802e-14, 613 | "_bottom": 5.684341886080802e-14, 614 | "_horizontalCenter": 0, 615 | "_verticalCenter": 0, 616 | "_isAbsLeft": true, 617 | "_isAbsRight": true, 618 | "_isAbsTop": true, 619 | "_isAbsBottom": true, 620 | "_isAbsHorizontalCenter": true, 621 | "_isAbsVerticalCenter": true, 622 | "_originalWidth": 0, 623 | "_originalHeight": 0, 624 | "_alignMode": 2, 625 | "_lockFlags": 0, 626 | "_id": "c5V1EV8IpMtrIvY1OE9t2u" 627 | }, 628 | { 629 | "__type__": "f5a88umqwxKvohw3IWqZkJR", 630 | "_name": "", 631 | "_objFlags": 0, 632 | "__editorExtras__": {}, 633 | "node": { 634 | "__id__": 2 635 | }, 636 | "_enabled": true, 637 | "__prefab": null, 638 | "_id": "acPDbfi09KmLOkYo+Irf3Y" 639 | }, 640 | { 641 | "__type__": "cc.PrefabInfo", 642 | "root": null, 643 | "asset": null, 644 | "fileId": "5d2f3c26-c7d2-48bd-bd13-ca5aa20a93c4", 645 | "instance": null, 646 | "targetOverrides": null 647 | }, 648 | { 649 | "__type__": "cc.SceneGlobals", 650 | "ambient": { 651 | "__id__": 25 652 | }, 653 | "shadows": { 654 | "__id__": 26 655 | }, 656 | "_skybox": { 657 | "__id__": 27 658 | }, 659 | "fog": { 660 | "__id__": 28 661 | }, 662 | "octree": { 663 | "__id__": 29 664 | }, 665 | "skin": { 666 | "__id__": 30 667 | }, 668 | "lightProbeInfo": { 669 | "__id__": 31 670 | }, 671 | "bakedWithStationaryMainLight": false, 672 | "bakedWithHighpLightmap": false 673 | }, 674 | { 675 | "__type__": "cc.AmbientInfo", 676 | "_skyColorHDR": { 677 | "__type__": "cc.Vec4", 678 | "x": 0, 679 | "y": 0, 680 | "z": 0, 681 | "w": 0.520833125 682 | }, 683 | "_skyColor": { 684 | "__type__": "cc.Vec4", 685 | "x": 0, 686 | "y": 0, 687 | "z": 0, 688 | "w": 0.520833125 689 | }, 690 | "_skyIllumHDR": 20000, 691 | "_skyIllum": 20000, 692 | "_groundAlbedoHDR": { 693 | "__type__": "cc.Vec4", 694 | "x": 0, 695 | "y": 0, 696 | "z": 0, 697 | "w": 0 698 | }, 699 | "_groundAlbedo": { 700 | "__type__": "cc.Vec4", 701 | "x": 0, 702 | "y": 0, 703 | "z": 0, 704 | "w": 0 705 | }, 706 | "_skyColorLDR": { 707 | "__type__": "cc.Vec4", 708 | "x": 0.2, 709 | "y": 0.5, 710 | "z": 0.8, 711 | "w": 1 712 | }, 713 | "_skyIllumLDR": 20000, 714 | "_groundAlbedoLDR": { 715 | "__type__": "cc.Vec4", 716 | "x": 0.2, 717 | "y": 0.2, 718 | "z": 0.2, 719 | "w": 1 720 | } 721 | }, 722 | { 723 | "__type__": "cc.ShadowsInfo", 724 | "_enabled": false, 725 | "_type": 0, 726 | "_normal": { 727 | "__type__": "cc.Vec3", 728 | "x": 0, 729 | "y": 1, 730 | "z": 0 731 | }, 732 | "_distance": 0, 733 | "_shadowColor": { 734 | "__type__": "cc.Color", 735 | "r": 76, 736 | "g": 76, 737 | "b": 76, 738 | "a": 255 739 | }, 740 | "_maxReceived": 4, 741 | "_size": { 742 | "__type__": "cc.Vec2", 743 | "x": 512, 744 | "y": 512 745 | } 746 | }, 747 | { 748 | "__type__": "cc.SkyboxInfo", 749 | "_envLightingType": 0, 750 | "_envmapHDR": null, 751 | "_envmap": null, 752 | "_envmapLDR": null, 753 | "_diffuseMapHDR": null, 754 | "_diffuseMapLDR": null, 755 | "_enabled": false, 756 | "_useHDR": true, 757 | "_editableMaterial": null, 758 | "_reflectionHDR": null, 759 | "_reflectionLDR": null, 760 | "_rotationAngle": 0 761 | }, 762 | { 763 | "__type__": "cc.FogInfo", 764 | "_type": 0, 765 | "_fogColor": { 766 | "__type__": "cc.Color", 767 | "r": 200, 768 | "g": 200, 769 | "b": 200, 770 | "a": 255 771 | }, 772 | "_enabled": false, 773 | "_fogDensity": 0.3, 774 | "_fogStart": 0.5, 775 | "_fogEnd": 300, 776 | "_fogAtten": 5, 777 | "_fogTop": 1.5, 778 | "_fogRange": 1.2, 779 | "_accurate": false 780 | }, 781 | { 782 | "__type__": "cc.OctreeInfo", 783 | "_enabled": false, 784 | "_minPos": { 785 | "__type__": "cc.Vec3", 786 | "x": -1024, 787 | "y": -1024, 788 | "z": -1024 789 | }, 790 | "_maxPos": { 791 | "__type__": "cc.Vec3", 792 | "x": 1024, 793 | "y": 1024, 794 | "z": 1024 795 | }, 796 | "_depth": 8 797 | }, 798 | { 799 | "__type__": "cc.SkinInfo", 800 | "_enabled": false, 801 | "_blurRadius": 0.01, 802 | "_sssIntensity": 3 803 | }, 804 | { 805 | "__type__": "cc.LightProbeInfo", 806 | "_giScale": 1, 807 | "_giSamples": 1024, 808 | "_bounces": 2, 809 | "_reduceRinging": 0, 810 | "_showProbe": true, 811 | "_showWireframe": true, 812 | "_showConvex": false, 813 | "_data": null, 814 | "_lightProbeSphereVolume": 1 815 | } 816 | ] -------------------------------------------------------------------------------- /list-3x/assets/home/home.scene.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.1.45", 3 | "importer": "scene", 4 | "imported": true, 5 | "uuid": "5d2f3c26-c7d2-48bd-bd13-ca5aa20a93c4", 6 | "files": [ 7 | ".json" 8 | ], 9 | "subMetas": {}, 10 | "userData": {} 11 | } 12 | -------------------------------------------------------------------------------- /list-3x/assets/home/home.ts: -------------------------------------------------------------------------------- 1 | import { _decorator, Component, Label, math, Node, Sprite } from 'cc'; 2 | import { YXCollectionView } from '../lib/yx-collection-view'; 3 | import { YXTableLayout } from '../lib/yx-table-layout'; 4 | const { ccclass, property } = _decorator; 5 | 6 | 7 | @ccclass('home') 8 | export class home extends Component { 9 | 10 | protected start(): void { 11 | this.setup_list1() 12 | this.setup_list2() 13 | this.setup_list3() 14 | } 15 | 16 | setup_list1() { 17 | const listComp = this.node.getChildByName('list1').getComponent(YXCollectionView) 18 | 19 | listComp.numberOfItems = () => 10000 20 | listComp.cellForItemAt = (indexPath, collectionView) => { 21 | const cell = collectionView.dequeueReusableCell(`cell`) 22 | cell.getChildByName('label').getComponent(Label).string = `${indexPath}` 23 | return cell 24 | } 25 | 26 | let layout = new YXTableLayout() 27 | layout.spacing = 20 28 | layout.rowHeight = 100 29 | listComp.layout = layout 30 | 31 | listComp.reloadData() 32 | } 33 | 34 | setup_list2() { 35 | const listComp = this.node.getChildByName('list2').getComponent(YXCollectionView) 36 | 37 | listComp.numberOfSections = () => 100 38 | listComp.supplementaryForItemAt = (indexPath, collectionView, kinds) => { 39 | if (kinds === YXTableLayout.SupplementaryKinds.HEADER) { 40 | const supplementary = collectionView.dequeueReusableSupplementary('supplementary') 41 | supplementary.getChildByName('label').getComponent(Label).string = `header ${indexPath}` 42 | const shape = supplementary.getChildByName('shape') 43 | shape.getComponent(Sprite).color = new math.Color(100, 100, 150) 44 | return supplementary 45 | } 46 | if (kinds === YXTableLayout.SupplementaryKinds.FOOTER) { 47 | const supplementary = collectionView.dequeueReusableSupplementary('supplementary') 48 | supplementary.getChildByName('label').getComponent(Label).string = `footer ${indexPath}` 49 | const shape = supplementary.getChildByName('shape') 50 | shape.getComponent(Sprite).color = new math.Color(150, 100, 100) 51 | return supplementary 52 | } 53 | return null 54 | } 55 | 56 | listComp.numberOfItems = () => 20 57 | listComp.cellForItemAt = (indexPath, collectionView) => { 58 | const cell = collectionView.dequeueReusableCell(`cell`) 59 | cell.getChildByName('label').getComponent(Label).string = `${indexPath}` 60 | return cell 61 | } 62 | 63 | let layout = new YXTableLayout() 64 | layout.spacing = 20 65 | layout.top = 20 66 | layout.bottom = 20 67 | layout.rowHeight = 100 68 | layout.sectionHeaderHeight = 120 69 | layout.sectionFooterHeight = 120 70 | listComp.layout = layout 71 | 72 | listComp.reloadData() 73 | } 74 | 75 | setup_list3() { 76 | const listComp = this.node.getChildByName('list3').getComponent(YXCollectionView) 77 | 78 | listComp.numberOfSections = () => 100 79 | listComp.supplementaryForItemAt = (indexPath, collectionView, kinds) => { 80 | if (kinds === YXTableLayout.SupplementaryKinds.HEADER) { 81 | const supplementary = collectionView.dequeueReusableSupplementary('supplementary') 82 | supplementary.getChildByName('label').getComponent(Label).string = `header ${indexPath}` 83 | const shape = supplementary.getChildByName('shape') 84 | shape.getComponent(Sprite).color = new math.Color(100, 100, 150) 85 | return supplementary 86 | } 87 | if (kinds === YXTableLayout.SupplementaryKinds.FOOTER) { 88 | const supplementary = collectionView.dequeueReusableSupplementary('supplementary') 89 | supplementary.getChildByName('label').getComponent(Label).string = `footer ${indexPath}` 90 | const shape = supplementary.getChildByName('shape') 91 | shape.getComponent(Sprite).color = new math.Color(150, 100, 100) 92 | return supplementary 93 | } 94 | return null 95 | } 96 | 97 | listComp.numberOfItems = () => 20 98 | listComp.cellForItemAt = (indexPath, collectionView) => { 99 | const cell = collectionView.dequeueReusableCell(`cell`) 100 | cell.getChildByName('label').getComponent(Label).string = `${indexPath}` 101 | return cell 102 | } 103 | 104 | let layout = new YXTableLayout() 105 | layout.spacing = 20 106 | layout.top = 20 107 | layout.bottom = 20 108 | layout.rowHeight = 100 109 | layout.sectionHeaderHeight = 120 110 | layout.sectionFooterHeight = 120 111 | layout.sectionHeadersPinToVisibleBounds = true 112 | layout.sectionFootersPinToVisibleBounds = true 113 | listComp.layout = layout 114 | 115 | listComp.reloadData() 116 | } 117 | } 118 | 119 | -------------------------------------------------------------------------------- /list-3x/assets/home/home.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "4.0.23", 3 | "importer": "typescript", 4 | "imported": true, 5 | "uuid": "f5a88ba6-ab0c-4abe-8870-dc85aa664251", 6 | "files": [], 7 | "subMetas": {}, 8 | "userData": {} 9 | } 10 | -------------------------------------------------------------------------------- /list-3x/assets/lib.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "1.2.0", 3 | "importer": "directory", 4 | "imported": true, 5 | "uuid": "bf1ca834-a556-4dcc-a699-1a5c368bda61", 6 | "files": [], 7 | "subMetas": {}, 8 | "userData": {} 9 | } 10 | -------------------------------------------------------------------------------- /list-3x/assets/lib/grid-layout.ts: -------------------------------------------------------------------------------- 1 | import { math, UITransform, warn } from "cc"; 2 | import { YXCollectionView, YXIndexPath, YXLayout, YXLayoutAttributes } from "./yx-collection-view"; 3 | 4 | export class GridLayout extends YXLayout { 5 | 6 | /** 7 | * 节点大小 8 | */ 9 | itemSize: math.Size = new math.Size(100, 100) 10 | 11 | /** 12 | * 垂直间距 13 | */ 14 | horizontalSpacing: number = 0 15 | 16 | /** 17 | * 水平间距 18 | */ 19 | verticalSpacing: number = 0 20 | 21 | /** 22 | * 整体对齐方式 23 | * 0靠左 1居中 2靠右 24 | */ 25 | alignment: number = 1 26 | 27 | /** 28 | * 获取每行最多可以容纳多少个节点 29 | */ 30 | protected getMaxItemsPerRow(collectionView: YXCollectionView): number { 31 | if (this._maxItemsPerRow == null) { 32 | let num = 1 33 | const width = collectionView.node.getComponent(UITransform).contentSize.width 34 | while ((num * this.itemSize.width + (num - 1) * this.horizontalSpacing) <= width) { num++ } 35 | num = Math.max(1, num - 1) 36 | this._maxItemsPerRow = num 37 | } 38 | return this._maxItemsPerRow 39 | } 40 | protected _maxItemsPerRow: number = null 41 | 42 | prepare(collectionView: YXCollectionView): void { 43 | if (collectionView.scrollDirection === YXCollectionView.ScrollDirection.VERTICAL) { 44 | this._prepare_vertical(collectionView) 45 | return 46 | } 47 | if (collectionView.scrollDirection === YXCollectionView.ScrollDirection.HORIZONTAL) { 48 | warn(`GridLayout 仅支持垂直方向排列`) 49 | this._prepare_vertical(collectionView) 50 | return 51 | } 52 | } 53 | 54 | protected _prepare_vertical(collectionView: YXCollectionView) { 55 | collectionView.scrollView.horizontal = false 56 | collectionView.scrollView.vertical = true 57 | 58 | let attrs: YXLayoutAttributes[] = [] 59 | let contentSize = collectionView.node.getComponent(UITransform).contentSize.clone() 60 | 61 | // 容器宽度 62 | const width = contentSize.width 63 | 64 | // 计算每行最多可以放多少个节点 65 | this._maxItemsPerRow = null 66 | let num = this.getMaxItemsPerRow(collectionView) 67 | 68 | // 根据设置的对齐方式计算左边距 69 | let left = 0 70 | if (this.alignment == 1) { 71 | let maxWidth = (num * this.itemSize.width + (num - 1) * this.horizontalSpacing) // 每行节点总宽度 72 | left = (width - maxWidth) * 0.5 73 | } 74 | if (this.alignment == 2) { 75 | let maxWidth = (num * this.itemSize.width + (num - 1) * this.horizontalSpacing) // 每行节点总宽度 76 | left = width - maxWidth 77 | } 78 | 79 | const numberOfSections = collectionView.getNumberOfSections() 80 | if (numberOfSections > 1) { warn(`GridLayout 暂时不支持分区模式`) } 81 | 82 | const numberOfItems = collectionView.getNumberOfItems(0) 83 | for (let index = 0; index < numberOfItems; index++) { 84 | 85 | // 计算这个节点是第几行 86 | let row = Math.floor(index / num) 87 | 88 | // 计算这个节点是第几列 89 | let column = index % num 90 | 91 | // 计算节点 origin 92 | let x = left + (this.itemSize.width + this.horizontalSpacing) * column 93 | let y = (this.itemSize.height + this.verticalSpacing) * row 94 | 95 | let attr = YXLayoutAttributes.layoutAttributesForCell(new YXIndexPath(0, index)) 96 | attr.frame.x = x 97 | attr.frame.y = y 98 | attr.frame.width = this.itemSize.width 99 | attr.frame.height = this.itemSize.height 100 | attrs.push(attr) 101 | 102 | // 更新内容高度 103 | contentSize.height = Math.max(contentSize.height, attr.frame.yMax) 104 | } 105 | 106 | this.attributes = attrs 107 | this.contentSize = contentSize 108 | } 109 | 110 | initOffset(collectionView: YXCollectionView): void { 111 | collectionView.scrollView.scrollToTop() 112 | } 113 | 114 | layoutAttributesForElementsInRect(rect: math.Rect, collectionView: YXCollectionView): YXLayoutAttributes[] { 115 | return this.visibleElementsInRect(rect, collectionView) 116 | } 117 | 118 | /** 119 | * 抽出来一个方法用来优化列表性能 120 | * 在优化之前,可以先看一下 @see YXLayout.layoutAttributesForElementsInRect 关于返回值的说明 121 | */ 122 | protected visibleElementsInRect(rect: math.Rect, collectionView: YXCollectionView) { 123 | if (this.attributes.length <= 100) { return this.attributes } // 少量数据就不查了,直接返回全部 124 | 125 | // 根据当前范围直接计算出一个区间 126 | const startRow = Math.floor(rect.y / (this.itemSize.height + this.verticalSpacing)) 127 | const endRow = Math.ceil(rect.yMax / (this.itemSize.height + this.verticalSpacing)) 128 | 129 | // 计算每行最多可以放多少个节点 130 | let num = this.getMaxItemsPerRow(collectionView) 131 | 132 | // 计算索引区间 133 | const startIdx = Math.max(startRow * num, 0) // 防止<0:当列表置顶往下滑时(rect.y < 0)得出startIdx为负数,导致slice截取为空(表现是回弹过程列表元素截断) 134 | const endIdx = endRow * num 135 | 136 | // 只返回区间节点的布局属性 137 | return this.attributes.slice(startIdx, endIdx) 138 | } 139 | } 140 | 141 | -------------------------------------------------------------------------------- /list-3x/assets/lib/grid-layout.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "4.0.23", 3 | "importer": "typescript", 4 | "imported": true, 5 | "uuid": "02a96aac-8c52-4201-b8af-c7ed49aae6d4", 6 | "files": [], 7 | "subMetas": {}, 8 | "userData": {} 9 | } 10 | -------------------------------------------------------------------------------- /list-3x/assets/lib/yx-collection-view.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "4.0.23", 3 | "importer": "typescript", 4 | "imported": true, 5 | "uuid": "f94cbc6e-9b7c-40b2-89a1-54d5b5a62634", 6 | "files": [], 7 | "subMetas": {}, 8 | "userData": {} 9 | } 10 | -------------------------------------------------------------------------------- /list-3x/assets/lib/yx-table-layout.ts: -------------------------------------------------------------------------------- 1 | import { log, math, UITransform, warn } from "cc"; 2 | import { YXCollectionView, YXIndexPath, YXLayout, YXLayoutAttributes } from "./yx-collection-view"; 3 | 4 | enum _yx_table_layout_supplementary_kinds { 5 | HEADER = 'header', 6 | FOOTER = 'footer', 7 | } 8 | 9 | export class YXTableLayout extends YXLayout { 10 | 11 | /** 12 | * 行高 13 | */ 14 | rowHeight: number | ((indexPath: YXIndexPath) => number) = 100 15 | 16 | /** 17 | * 内容上边距 18 | */ 19 | top: number = 0 20 | 21 | /** 22 | * 内容下边距 23 | */ 24 | bottom: number = 0 25 | 26 | /** 27 | * 节点之间间距 28 | */ 29 | spacing: number = 0 30 | 31 | /** 32 | * 区头高度 33 | */ 34 | sectionHeaderHeight: number | ((section: number) => number) = null 35 | 36 | /** 37 | * 区尾高度 38 | */ 39 | sectionFooterHeight: number | ((section: number) => number) = null 40 | 41 | /** 42 | * 钉住 header 的位置 ( header 吸附在列表可见范围内 ) 43 | */ 44 | sectionHeadersPinToVisibleBounds: boolean = false 45 | 46 | /** 47 | * 钉住 footer 的位置 ( footer 吸附在列表可见范围内 ) 48 | */ 49 | sectionFootersPinToVisibleBounds: boolean = false 50 | 51 | /** 52 | * 区头/区尾标识 53 | */ 54 | static SupplementaryKinds = _yx_table_layout_supplementary_kinds 55 | 56 | protected originalHeaderRect: Map = new Map() // 保存所有 header 的原始位置 57 | protected originalFooterRect: Map = new Map() // 保存所有 footer 的原始位置 58 | 59 | // 为了优化查找,额外维护几个数组按类别管理所有的布局属性,空间换时间 60 | protected allCellAttributes: YXLayoutAttributes[] = [] 61 | protected allHeaderAttributes: YXLayoutAttributes[] = [] 62 | protected allFooterAttributes: YXLayoutAttributes[] = [] 63 | 64 | prepare(collectionView: YXCollectionView): void { 65 | // 设置列表的滚动方向(这套布局固定为垂直方向滚动) 66 | collectionView.scrollView.horizontal = false 67 | collectionView.scrollView.vertical = true 68 | if (collectionView.scrollDirection === YXCollectionView.ScrollDirection.HORIZONTAL) { 69 | // 由于这套布局规则只支持垂直方向布局,当外部配置了水平方向滚动时这里可以给个警告 70 | warn(`YXTableLayout 仅支持垂直方向排列`) 71 | } 72 | 73 | // 清空一下布局属性数组 74 | this.attributes = [] 75 | this.allCellAttributes = [] 76 | this.allHeaderAttributes = [] 77 | this.allFooterAttributes = [] 78 | this.originalHeaderRect.clear() 79 | this.originalFooterRect.clear() 80 | 81 | // 获取列表宽度 82 | const contentWidth = collectionView.node.getComponent(UITransform).width 83 | 84 | // 声明一个临时变量,用来记录当前所有内容的总高度 85 | let contentHeight = 0 86 | 87 | // 获取列表一共分多少个区 88 | let numberOfSections = collectionView.getNumberOfSections() 89 | 90 | // 为每条数据对应的生成一个布局属性 91 | for (let section = 0; section < numberOfSections; section++) { 92 | 93 | // 创建一个区索引 94 | let sectionIndexPath = new YXIndexPath(section, 0) 95 | 96 | // 通过区索引创建一个区头节点布局属性 97 | let sectionHeaderHeight = 0 98 | if (this.sectionHeaderHeight) { 99 | sectionHeaderHeight = this.sectionHeaderHeight instanceof Function ? this.sectionHeaderHeight(section) : this.sectionHeaderHeight 100 | } 101 | if (sectionHeaderHeight > 0) { 102 | let headerAttr = YXLayoutAttributes.layoutAttributesForSupplementary(sectionIndexPath, YXTableLayout.SupplementaryKinds.HEADER) 103 | 104 | // 确定这个节点的位置 105 | headerAttr.frame.x = 0 106 | headerAttr.frame.width = contentWidth 107 | headerAttr.frame.height = sectionHeaderHeight 108 | headerAttr.frame.y = contentHeight 109 | 110 | // 调整层级 111 | headerAttr.zIndex = 1 112 | 113 | // 重要: 保存布局属性 114 | this.attributes.push(headerAttr) 115 | this.originalHeaderRect.set(section, headerAttr.frame.clone()) 116 | this.allHeaderAttributes.push(headerAttr) 117 | 118 | // 更新整体内容高度 119 | contentHeight = headerAttr.frame.yMax 120 | } 121 | 122 | // 将 top 配置应用到每个区 123 | contentHeight = contentHeight + this.top 124 | 125 | // 获取这个区内的内容数量,注意这里传入的是 section 126 | let numberOfItems = collectionView.getNumberOfItems(section) 127 | 128 | for (let item = 0; item < numberOfItems; item++) { 129 | 130 | // 创建索引,注意这里的 section 已经改为正确的 section 了 131 | let indexPath = new YXIndexPath(section, item) 132 | 133 | // 通过索引创建一个 cell 节点的布局属性 134 | let attr = YXLayoutAttributes.layoutAttributesForCell(indexPath) 135 | 136 | // 通过索引获取这个节点的高度 137 | let rowHeight = this.rowHeight instanceof Function ? this.rowHeight(indexPath) : this.rowHeight 138 | 139 | // 确定这个节点的位置 140 | attr.frame.x = 0 141 | attr.frame.width = contentWidth 142 | attr.frame.height = rowHeight 143 | attr.frame.y = contentHeight + (item > 0 ? this.spacing : 0) 144 | 145 | // 重要: 保存布局属性 146 | this.attributes.push(attr) 147 | this.allCellAttributes.push(attr) 148 | 149 | // 更新当前内容高度 150 | contentHeight = attr.frame.yMax 151 | } 152 | 153 | // 高度补一个底部间距,跟 top 一样,也是应用到每个区 154 | contentHeight = contentHeight + this.bottom 155 | 156 | // 通过区索引创建一个区尾节点布局属性 157 | let sectionFooterHeight = 0 158 | if (this.sectionFooterHeight) { 159 | sectionFooterHeight = this.sectionFooterHeight instanceof Function ? this.sectionFooterHeight(section) : this.sectionFooterHeight 160 | } 161 | if (sectionFooterHeight > 0) { 162 | let footerAttr = YXLayoutAttributes.layoutAttributesForSupplementary(sectionIndexPath, YXTableLayout.SupplementaryKinds.FOOTER) 163 | 164 | // 确定这个节点的位置 165 | footerAttr.frame.x = 0 166 | footerAttr.frame.width = contentWidth 167 | footerAttr.frame.height = sectionFooterHeight 168 | footerAttr.frame.y = contentHeight 169 | 170 | // 调整层级 171 | footerAttr.zIndex = 1 172 | 173 | // 重要: 保存布局属性 174 | this.attributes.push(footerAttr) 175 | this.originalFooterRect.set(section, footerAttr.frame.clone()) 176 | this.allFooterAttributes.push(footerAttr) 177 | 178 | // 更新整体内容高度 179 | contentHeight = footerAttr.frame.yMax 180 | } 181 | } 182 | 183 | // 重要: 设置内容区域总大小,只有确定了滚动区域的大小列表才能滚动 184 | this.contentSize = new math.Size(contentWidth, contentHeight) 185 | } 186 | 187 | initOffset(collectionView: YXCollectionView): void { 188 | // 列表首次刷新时,调整一下列表的偏移位置 189 | collectionView.scrollView.scrollToTop() 190 | } 191 | 192 | layoutAttributesForElementsInRect(rect: math.Rect, collectionView: YXCollectionView): YXLayoutAttributes[] { 193 | let result = this.visibleElementsInRect(rect, collectionView) 194 | if (this.sectionHeadersPinToVisibleBounds == false && this.sectionFootersPinToVisibleBounds == false) { 195 | return result // 不需要调整节点位置,直接返回就好 196 | } 197 | 198 | let numberOfSections = collectionView.getNumberOfSections() 199 | let scrollOffset = collectionView.scrollView.getScrollOffset() 200 | for (let index = 0; index < result.length; index++) { 201 | const element = result[index]; 202 | if (element.elementCategory === 'Supplementary') { 203 | 204 | if (this.sectionHeadersPinToVisibleBounds && element.supplementaryKinds === YXTableLayout.SupplementaryKinds.HEADER) { 205 | const originalFrame = this.originalHeaderRect.get(element.indexPath.section) 206 | element.frame.y = originalFrame.y 207 | if (scrollOffset.y > originalFrame.y) { 208 | element.frame.y = scrollOffset.y 209 | } 210 | const nextOriginalFrame = this.getNextOriginalFrame(element.indexPath.section, YXTableLayout.SupplementaryKinds.FOOTER, numberOfSections) 211 | if (nextOriginalFrame) { 212 | if (element.frame.yMax > nextOriginalFrame.y) { 213 | element.frame.y = nextOriginalFrame.y - element.frame.height 214 | } 215 | } 216 | } 217 | 218 | if (this.sectionFootersPinToVisibleBounds && element.supplementaryKinds === YXTableLayout.SupplementaryKinds.FOOTER) { 219 | let bottom = scrollOffset.y + collectionView.scrollView.view.height 220 | const originalFrame = this.originalFooterRect.get(element.indexPath.section) 221 | const previousOriginalFrame = this.getPreviousOriginalFrame(element.indexPath.section, YXTableLayout.SupplementaryKinds.HEADER) 222 | element.frame.y = originalFrame.y 223 | if (bottom < originalFrame.yMax) { 224 | element.frame.y = bottom - element.frame.height 225 | if (previousOriginalFrame) { 226 | if (element.frame.y < previousOriginalFrame.yMax) { 227 | element.frame.y = previousOriginalFrame.yMax 228 | } 229 | } 230 | } 231 | } 232 | } 233 | } 234 | return result 235 | } 236 | 237 | shouldUpdateAttributesZIndex(): boolean { 238 | return this.sectionHeadersPinToVisibleBounds || this.sectionFootersPinToVisibleBounds 239 | } 240 | 241 | shouldUpdateAttributesForBoundsChange(): boolean { 242 | return this.sectionHeadersPinToVisibleBounds || this.sectionFootersPinToVisibleBounds 243 | } 244 | 245 | /** 246 | * 获取 `section` 下一个 header 或者 footer 的位置 247 | */ 248 | protected getNextOriginalFrame(section: number, kinds: _yx_table_layout_supplementary_kinds, total: number) { 249 | if (section >= total) { return null } 250 | if (kinds === YXTableLayout.SupplementaryKinds.HEADER) { 251 | let result = this.originalHeaderRect.get(section) 252 | if (result) { return result } 253 | return this.getNextOriginalFrame(section, YXTableLayout.SupplementaryKinds.FOOTER, total) 254 | } 255 | if (kinds === YXTableLayout.SupplementaryKinds.FOOTER) { 256 | let result = this.originalFooterRect.get(section) 257 | if (result) { return result } 258 | return this.getNextOriginalFrame(section + 1, YXTableLayout.SupplementaryKinds.HEADER, total) 259 | } 260 | return null 261 | } 262 | 263 | /** 264 | * 获取 `section` 前一个 header 或者 footer 的位置 265 | */ 266 | protected getPreviousOriginalFrame(section: number, kinds: _yx_table_layout_supplementary_kinds) { 267 | if (section < 0) { return null } 268 | if (kinds === YXTableLayout.SupplementaryKinds.HEADER) { 269 | let result = this.originalHeaderRect.get(section) 270 | if (result) { return result } 271 | return this.getPreviousOriginalFrame(section - 1, YXTableLayout.SupplementaryKinds.FOOTER) 272 | } 273 | if (kinds === YXTableLayout.SupplementaryKinds.FOOTER) { 274 | let result = this.originalFooterRect.get(section) 275 | if (result) { return result } 276 | return this.getPreviousOriginalFrame(section, YXTableLayout.SupplementaryKinds.HEADER) 277 | } 278 | return null 279 | } 280 | 281 | /** 282 | * 抽出来一个方法用来优化列表性能 283 | * 在优化之前,可以先看一下 @see YXLayout.layoutAttributesForElementsInRect 关于返回值的说明 284 | * 对于有序列表来说,一般都是可以通过二分查找来进行优化 285 | */ 286 | protected visibleElementsInRect(rect: math.Rect, collectionView: YXCollectionView) { 287 | if (this.attributes.length <= 100) { return this.attributes } // 少量数据就不查了,直接返回全部 288 | 289 | let result: YXLayoutAttributes[] = [] 290 | 291 | // header 跟 footer 暂时不考虑,数据相对来说不算很多,直接全部返回 292 | result.push(...this.allHeaderAttributes) 293 | result.push(...this.allFooterAttributes) 294 | 295 | // 关于 cell,这里用二分查找来优化一下 296 | // 首先通过二分先查出个大概位置 297 | let midIdx = -1 298 | let left = 0 299 | let right = this.allCellAttributes.length - 1 300 | 301 | while (left <= right && right >= 0) { 302 | let mid = left + (right - left) / 2 303 | mid = Math.floor(mid) 304 | let attr = this.allCellAttributes[mid] 305 | if (rect.intersects(attr.frame)) { 306 | midIdx = mid 307 | break 308 | } 309 | if (rect.yMax < attr.frame.yMin || rect.xMax < attr.frame.xMin) { 310 | right = mid - 1 311 | } else { 312 | left = mid + 1 313 | } 314 | } 315 | 316 | // 二分查找出错了,返回全部的布局属性 317 | if (midIdx < 0) { 318 | return this.attributes 319 | } 320 | 321 | // 把模糊查到这个先加进来 322 | result.push(this.allCellAttributes[midIdx]) 323 | 324 | // 然后依次往前检查,直到超出当前的显示范围 325 | let startIdx = midIdx 326 | while (startIdx > 0) { 327 | let idx = startIdx - 1 328 | let attr = this.allCellAttributes[idx] 329 | if (rect.intersects(attr.frame) == false) { 330 | break 331 | } 332 | result.push(attr) 333 | startIdx = idx 334 | } 335 | 336 | // 依次往后检查,直到超出当前的显示范围 337 | let endIdx = midIdx 338 | while (endIdx < this.allCellAttributes.length - 1) { 339 | let idx = endIdx + 1 340 | let attr = this.allCellAttributes[idx] 341 | if (rect.intersects(attr.frame) == false) { 342 | break 343 | } 344 | result.push(attr) 345 | endIdx = idx 346 | } 347 | 348 | return result 349 | } 350 | } 351 | 352 | 353 | -------------------------------------------------------------------------------- /list-3x/assets/lib/yx-table-layout.ts.meta: -------------------------------------------------------------------------------- 1 | { 2 | "ver": "4.0.23", 3 | "importer": "typescript", 4 | "imported": true, 5 | "uuid": "0187d4ab-fbb6-44aa-8c36-b5709f3dbff4", 6 | "files": [], 7 | "subMetas": {}, 8 | "userData": {} 9 | } 10 | -------------------------------------------------------------------------------- /list-3x/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "list-3x", 3 | "uuid": "d21c5c56-bc81-481d-b241-7ce394501071", 4 | "creator": { 5 | "version": "3.8.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /list-3x/settings/v2/packages/builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "__version__": "1.3.6" 3 | } 4 | -------------------------------------------------------------------------------- /list-3x/settings/v2/packages/cocos-service.json: -------------------------------------------------------------------------------- 1 | { 2 | "__version__": "3.0.7", 3 | "game": { 4 | "name": "未知游戏", 5 | "app_id": "UNKNOW", 6 | "c_id": "0" 7 | }, 8 | "appConfigMaps": [ 9 | { 10 | "app_id": "UNKNOW", 11 | "config_id": "abc178" 12 | } 13 | ], 14 | "configs": [ 15 | { 16 | "app_id": "UNKNOW", 17 | "config_id": "abc178", 18 | "config_name": "Default", 19 | "config_remarks": "", 20 | "services": [] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /list-3x/settings/v2/packages/device.json: -------------------------------------------------------------------------------- 1 | { 2 | "__version__": "1.0.1" 3 | } 4 | -------------------------------------------------------------------------------- /list-3x/settings/v2/packages/engine.json: -------------------------------------------------------------------------------- 1 | { 2 | "__version__": "1.0.7", 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": true 19 | }, 20 | "skeletal-animation": { 21 | "_value": false 22 | }, 23 | "3d": { 24 | "_value": false 25 | }, 26 | "2d": { 27 | "_value": true 28 | }, 29 | "xr": { 30 | "_value": false 31 | }, 32 | "ui": { 33 | "_value": true 34 | }, 35 | "particle": { 36 | "_value": false 37 | }, 38 | "physics": { 39 | "_value": false, 40 | "_option": "physics-ammo" 41 | }, 42 | "physics-ammo": { 43 | "_value": false 44 | }, 45 | "physics-cannon": { 46 | "_value": false 47 | }, 48 | "physics-physx": { 49 | "_value": false 50 | }, 51 | "physics-builtin": { 52 | "_value": false 53 | }, 54 | "physics-2d": { 55 | "_value": true, 56 | "_option": "physics-2d-box2d" 57 | }, 58 | "physics-2d-box2d": { 59 | "_value": false 60 | }, 61 | "physics-2d-builtin": { 62 | "_value": false 63 | }, 64 | "intersection-2d": { 65 | "_value": true 66 | }, 67 | "primitive": { 68 | "_value": false 69 | }, 70 | "profiler": { 71 | "_value": true 72 | }, 73 | "occlusion-query": { 74 | "_value": false 75 | }, 76 | "geometry-renderer": { 77 | "_value": false 78 | }, 79 | "debug-renderer": { 80 | "_value": false 81 | }, 82 | "particle-2d": { 83 | "_value": true 84 | }, 85 | "audio": { 86 | "_value": true 87 | }, 88 | "video": { 89 | "_value": true 90 | }, 91 | "webview": { 92 | "_value": true 93 | }, 94 | "tween": { 95 | "_value": true 96 | }, 97 | "websocket": { 98 | "_value": false 99 | }, 100 | "websocket-server": { 101 | "_value": false 102 | }, 103 | "terrain": { 104 | "_value": false 105 | }, 106 | "light-probe": { 107 | "_value": false 108 | }, 109 | "tiled-map": { 110 | "_value": true 111 | }, 112 | "spine": { 113 | "_value": true 114 | }, 115 | "dragon-bones": { 116 | "_value": true 117 | }, 118 | "marionette": { 119 | "_value": false 120 | }, 121 | "custom-pipeline": { 122 | "_value": false 123 | } 124 | }, 125 | "includeModules": [ 126 | "2d", 127 | "animation", 128 | "audio", 129 | "base", 130 | "dragon-bones", 131 | "gfx-webgl", 132 | "gfx-webgl2", 133 | "intersection-2d", 134 | "particle-2d", 135 | "physics-2d-box2d", 136 | "profiler", 137 | "spine", 138 | "tiled-map", 139 | "tween", 140 | "ui", 141 | "video", 142 | "webview" 143 | ], 144 | "noDeprecatedFeatures": { 145 | "value": false, 146 | "version": "" 147 | }, 148 | "flags": {} 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /list-3x/settings/v2/packages/information.json: -------------------------------------------------------------------------------- 1 | { 2 | "__version__": "1.0.0", 3 | "information": { 4 | "customSplash": { 5 | "id": "customSplash", 6 | "label": "customSplash", 7 | "enable": true, 8 | "customSplash": { 9 | "complete": false, 10 | "form": "https://creator-api.cocos.com/api/form/show?sid=9fcf74f056ee5fdb06fa23517bdcc30a" 11 | } 12 | }, 13 | "removeSplash": { 14 | "id": "removeSplash", 15 | "label": "removeSplash", 16 | "enable": true, 17 | "removeSplash": { 18 | "complete": false, 19 | "form": "https://creator-api.cocos.com/api/form/show?sid=9fcf74f056ee5fdb06fa23517bdcc30a" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /list-3x/settings/v2/packages/program.json: -------------------------------------------------------------------------------- 1 | { 2 | "__version__": "1.0.3" 3 | } 4 | -------------------------------------------------------------------------------- /list-3x/settings/v2/packages/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "__version__": "1.0.5" 3 | } 4 | -------------------------------------------------------------------------------- /list-3x/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 | --------------------------------------------------------------------------------