@cc-component/cc-ex-component 1.1.6 → 1.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/assets/core/BaseReference.ts +40 -0
  2. package/assets/{video/VideoComponent.ts.meta → core/BaseReference.ts.meta} +1 -1
  3. package/assets/core/BaseViewModelData.ts +12 -0
  4. package/assets/{video/Interface.ts.meta → core/BaseViewModelData.ts.meta} +1 -1
  5. package/assets/core/ReferenceComponent.ts +317 -0
  6. package/assets/core/ViewModel.ts +542 -0
  7. package/assets/{video/IVideo.ts.meta → core/ViewModel.ts.meta} +9 -9
  8. package/assets/ex/EXButton.ts +191 -0
  9. package/assets/ex/EXButton.ts.meta +9 -0
  10. package/assets/ex/ExCommon.ts +6 -4
  11. package/assets/ex/ExTool.ts +116 -0
  12. package/assets/ex/ExTool.ts.meta +9 -0
  13. package/assets/lib/collectView/lib-ext/custom-grid-flow-layout.ts +105 -0
  14. package/assets/lib/collectView/lib-ext/custom-grid-flow-layout.ts.meta +9 -0
  15. package/assets/lib/collectView/lib-ext/horizontal-center-layout.ts +84 -0
  16. package/assets/lib/collectView/lib-ext/horizontal-center-layout.ts.meta +9 -0
  17. package/assets/lib/collectView/lib-ext/yx-card-page-layout.ts +132 -0
  18. package/assets/lib/collectView/lib-ext/yx-card-page-layout.ts.meta +9 -0
  19. package/assets/lib/collectView/lib-ext/yx-carousel-layout.ts +156 -0
  20. package/assets/lib/collectView/lib-ext/yx-carousel-layout.ts.meta +9 -0
  21. package/assets/lib/collectView/lib-ext/yx-cover-layout.ts +405 -0
  22. package/assets/lib/collectView/lib-ext/yx-cover-layout.ts.meta +9 -0
  23. package/assets/lib/collectView/lib-ext/yx-masonry-flow-layout.ts +194 -0
  24. package/assets/lib/collectView/lib-ext/yx-masonry-flow-layout.ts.meta +9 -0
  25. package/assets/lib/collectView/lib-ext/yx-page-view.ts +232 -0
  26. package/assets/lib/collectView/lib-ext/yx-page-view.ts.meta +9 -0
  27. package/assets/lib/collectView/lib-ext/yx-table-view.ts +159 -0
  28. package/assets/lib/collectView/lib-ext/yx-table-view.ts.meta +9 -0
  29. package/assets/lib/collectView/lib-ext.meta +9 -0
  30. package/assets/lib/collectView/lib_collect/yx-collection-view.ts +1549 -0
  31. package/assets/lib/collectView/lib_collect/yx-collection-view.ts.meta +9 -0
  32. package/assets/lib/collectView/lib_collect/yx-compact-flow-layout.ts +364 -0
  33. package/assets/lib/collectView/lib_collect/yx-compact-flow-layout.ts.meta +9 -0
  34. package/assets/lib/collectView/lib_collect/yx-flow-layout.ts +909 -0
  35. package/assets/lib/collectView/lib_collect/yx-flow-layout.ts.meta +9 -0
  36. package/assets/lib/collectView/lib_collect/yx-table-layout.ts +352 -0
  37. package/assets/lib/collectView/lib_collect/yx-table-layout.ts.meta +9 -0
  38. package/assets/lib/collectView/lib_collect.meta +9 -0
  39. package/assets/{video/list.meta → lib/collectView.meta} +9 -9
  40. package/assets/lib/tableView/IListView.ts +17 -0
  41. package/assets/lib/tableView/IListView.ts.meta +9 -0
  42. package/assets/lib/tableView/ListView.ts +197 -0
  43. package/assets/lib/tableView/ListView.ts.meta +9 -0
  44. package/assets/lib/tableView/ListViewPage.ts +1048 -0
  45. package/assets/lib/tableView/ListViewPage.ts.meta +9 -0
  46. package/assets/lib/tableView/ListViewPageLoop.ts +922 -0
  47. package/assets/lib/tableView/ListViewPageLoop.ts.meta +1 -0
  48. package/assets/lib/tableView/TableView.ts +82 -0
  49. package/assets/lib/tableView/TableView.ts.meta +9 -0
  50. package/assets/lib/tableView.meta +9 -0
  51. package/assets/{video.meta → lib.meta} +1 -1
  52. package/assets/platform/Interface.ts +15 -10
  53. package/assets/platform/android/AndroidModule.ts +12 -0
  54. package/assets/platform/android/AndroidModule.ts.meta +9 -0
  55. package/assets/platform/android/AndroidSDK.ts +1 -2
  56. package/assets/platform/base/PlatfprmModule.ts +44 -29
  57. package/assets/platform/base/SDKBase.ts +2 -2
  58. package/assets/platform/base/TTSDK.ts +21 -10
  59. package/assets/platform/base/WXSDK.ts +15 -16
  60. package/assets/platform/wx/MiniSDK.ts +41 -3
  61. package/assets/platform/wx/wxmini.d.ts +2 -2
  62. package/index.ts +0 -1
  63. package/package.json +1 -1
  64. package/assets/core/ReferenceCollector.ts +0 -172
  65. package/assets/video/IVideo.ts +0 -73
  66. package/assets/video/Interface.ts +0 -25
  67. package/assets/video/VideoComponent.prefab +0 -614
  68. package/assets/video/VideoComponent.prefab.meta +0 -13
  69. package/assets/video/VideoComponent.ts +0 -33
  70. package/assets/video/VideoManager.ts +0 -399
  71. package/assets/video/VideoManager.ts.meta +0 -9
  72. package/assets/video/VideoModule.ts +0 -137
  73. package/assets/video/VideoModule.ts.meta +0 -9
  74. package/assets/video/VideoPlayTT.ts +0 -338
  75. package/assets/video/VideoPlayTT.ts.meta +0 -9
  76. package/assets/video/VideoPlayWX.ts +0 -274
  77. package/assets/video/VideoPlayWX.ts.meta +0 -9
  78. package/assets/video/VideoPlayWeb.ts +0 -228
  79. package/assets/video/VideoPlayWeb.ts.meta +0 -9
  80. /package/assets/core/{ReferenceCollector.ts.meta → ReferenceComponent.ts.meta} +0 -0
@@ -0,0 +1,1549 @@
1
+ import { _decorator, Component, Enum, Event, EventMouse, EventTouch, instantiate, Mask, math, Node, NodeEventType, NodePool, Prefab, ScrollView, UIOpacity, UITransform } from 'cc';
2
+ const { ccclass, property, executionOrder, disallowMultiple, help } = _decorator;
3
+
4
+ const _vec3Out = new math.Vec3()
5
+ const _scroll_view_visible_rect = new math.Rect()
6
+ const _recycleInvisibleNodes_realFrame = new math.Rect()
7
+
8
+ /**
9
+ * 定义列表的滚动方向
10
+ */
11
+ enum _yx_collection_view_scroll_direction {
12
+ /**
13
+ * 水平滚动
14
+ */
15
+ HORIZONTAL,
16
+
17
+ /**
18
+ * 垂直滚动
19
+ */
20
+ VERTICAL,
21
+ }
22
+ Enum(_yx_collection_view_scroll_direction)
23
+
24
+ /**
25
+ * 列表节点加载模式
26
+ */
27
+ enum _yx_collection_view_list_mode {
28
+ /**
29
+ * 根据列表显示范围加载需要的节点,同类型的节点会进行复用
30
+ * 优点: 控制总节点数量,不会创建大量节点
31
+ * 缺点: 因为有复用逻辑,节点内容会频繁更新,cell 更新业务比较重的话列表会抖动,例如 Label (NONE) 很多的节点
32
+ */
33
+ RECYCLE,
34
+
35
+ /**
36
+ * 直接预加载所有的节点,处于列表显示范围外的节点透明化处理
37
+ * 优点: 避免 cell 频繁更新,优化大量 Label (NONE) 场景下的卡顿问题
38
+ * 缺点: 会实例化所有节点,并非真正的虚拟列表,仅仅是把显示范围外的节点透明了,如果列表数据量很大仍然会卡
39
+ */
40
+ PRELOAD,
41
+ }
42
+ Enum(_yx_collection_view_list_mode)
43
+
44
+ /**
45
+ * 定义通过编辑器注册节点时的数据结构
46
+ */
47
+ @ccclass(`_yx_editor_register_element_info_2`)
48
+ class _yx_editor_register_element_info_2 {
49
+ @property({ type: Prefab, tooltip: `cell 节点预制体,必须配置` })
50
+ prefab: Prefab = null
51
+ @property({ tooltip: `节点重用标识符,必须配置` })
52
+ identifier: string = ``
53
+ @property({ tooltip: `节点挂载的自定义组件\n如果需要监听 NodePool 的重用/回收事件,确保你的自定义组件已经实现了 YXCollectionViewCell 接口并配置此属性为你的自定义组件名\n如果不需要,可以忽略此配置` })
54
+ comp: string = ``
55
+
56
+ }
57
+
58
+ /**
59
+ * 表示索引的对象
60
+ */
61
+ export class YXIndexPath {
62
+ private _item: number = 0
63
+ private _section: number = 0
64
+ public static ZERO: Readonly<YXIndexPath> = new YXIndexPath(0, 0)
65
+ /**
66
+ * 区索引
67
+ */
68
+ get section(): number { return this._section }
69
+
70
+ /**
71
+ * 单元格在区内的位置
72
+ */
73
+ get item(): number { return this._item }
74
+ /**
75
+ * item 别名
76
+ */
77
+ get row(): number { return this.item }
78
+ constructor(section: number, item: number) { this._section = section; this._item = item; }
79
+ clone(): YXIndexPath { return new YXIndexPath(this.section, this.item) }
80
+ equals(other: YXIndexPath): boolean { return (this.section == other.section && this.item == other.item) }
81
+ toString(): string { return `${this.section} - ${this.item}` }
82
+ }
83
+
84
+ /**
85
+ * 表示边距的对象
86
+ */
87
+ export class YXEdgeInsets {
88
+ public static ZERO: Readonly<YXEdgeInsets> = new YXEdgeInsets(0, 0, 0, 0)
89
+ top: number;
90
+ left: number;
91
+ bottom: number;
92
+ right: number;
93
+ constructor(top: number, left: number, bottom: number, right: number) { this.top = top; this.left = left; this.bottom = bottom; this.right = right; }
94
+ clone(): YXEdgeInsets { return new YXEdgeInsets(this.top, this.left, this.bottom, this.right) }
95
+ equals(other: YXEdgeInsets): boolean { return (this.top == other.top && this.left == other.left && this.bottom == other.bottom && this.right == other.right) }
96
+ set(other: YXEdgeInsets): void { this.top = other.top; this.left = other.left; this.bottom = other.bottom; this.right = other.right; }
97
+ toString(): string { return `[ ${this.top}, ${this.left}, ${this.bottom}, ${this.right} ]` }
98
+ }
99
+
100
+ /**
101
+ * 私有组件
102
+ * 节点添加到 YXCollectionView 上时,自动挂载此组件,用来记录一些实时参数
103
+ */
104
+ class _yx_node_element_comp extends Component {
105
+ /**
106
+ * 此节点是通过哪个标识符创建的
107
+ */
108
+ identifier: string
109
+
110
+ /**
111
+ * 此节点目前绑定的布局属性
112
+ */
113
+ attributes: YXLayoutAttributes
114
+ }
115
+
116
+ /**
117
+ * 私有组件
118
+ * 内部滚动视图组件
119
+ * https://github.com/cocos/cocos-engine/blob/v3.8.0/cocos/ui/scroll-view.ts
120
+ */
121
+ class _scroll_view extends ScrollView {
122
+
123
+ protected _yx_scroll_offset_on_touch_start: math.Vec2 = null
124
+ _yx_startAttenuatingAutoScrollTargetOffset: (touchMoveVelocity: math.Vec3, startOffset: math.Vec2, originTargetOffset: math.Vec2, originScrollTime: number) => { offset: math.Vec2; time?: number; attenuated?: boolean; } = null
125
+
126
+ /**
127
+ * 鼠标滚轮
128
+ */
129
+ protected _onMouseWheel(event: EventMouse, captureListeners?: Node[]): void {
130
+ const comp = this.node.getComponent(YXCollectionView)
131
+ if (comp == null) { return }
132
+ if (comp.scrollEnabled == false) { return }
133
+ if (comp.wheelScrollEnabled == false) { return }
134
+ super._onMouseWheel(event, captureListeners)
135
+ }
136
+
137
+ /**
138
+ * 准备开始惯性滚动
139
+ * @param initialVelocity 手势速度
140
+ */
141
+ protected _startAttenuatingAutoScroll(deltaMove: math.Vec3, initialVelocity: math.Vec3) {
142
+ const targetDelta = deltaMove.clone();
143
+ targetDelta.normalize();
144
+ if (this._content && this.view) {
145
+ const contentSize = this._content._uiProps.uiTransformComp!.contentSize;
146
+ const scrollViewSize = this.view.contentSize;
147
+
148
+ const totalMoveWidth = (contentSize.width - scrollViewSize.width);
149
+ const totalMoveHeight = (contentSize.height - scrollViewSize.height);
150
+
151
+ const attenuatedFactorX = this._calculateAttenuatedFactor(totalMoveWidth);
152
+ const attenuatedFactorY = this._calculateAttenuatedFactor(totalMoveHeight);
153
+
154
+ targetDelta.x = targetDelta.x * totalMoveWidth * (1 - this.brake) * attenuatedFactorX;
155
+ targetDelta.y = targetDelta.y * totalMoveHeight * attenuatedFactorY * (1 - this.brake);
156
+ targetDelta.z = 0;
157
+ }
158
+
159
+ const originalMoveLength = deltaMove.length();
160
+ let factor = targetDelta.length() / originalMoveLength;
161
+ targetDelta.add(deltaMove);
162
+
163
+ if (this.brake > 0 && factor > 7) {
164
+ factor = Math.sqrt(factor);
165
+ const clonedDeltaMove = deltaMove.clone();
166
+ clonedDeltaMove.multiplyScalar(factor);
167
+ targetDelta.set(clonedDeltaMove);
168
+ targetDelta.add(deltaMove);
169
+ }
170
+
171
+ let time = this._calculateAutoScrollTimeByInitialSpeed(initialVelocity.length());
172
+ if (this.brake > 0 && factor > 3) {
173
+ factor = 3;
174
+ time *= factor;
175
+ }
176
+
177
+ if (this.brake === 0 && factor > 1) {
178
+ time *= factor;
179
+ }
180
+
181
+ // 当自定义了滚动停留位置时,以自定义的停留位置为准
182
+ if (this._yx_startAttenuatingAutoScrollTargetOffset) {
183
+ const originTargetOffset = this.getScrollOffset()
184
+ originTargetOffset.x += targetDelta.x
185
+ originTargetOffset.y += targetDelta.y
186
+ let hookValue = this._yx_startAttenuatingAutoScrollTargetOffset(initialVelocity, this._yx_scroll_offset_on_touch_start, originTargetOffset, time)
187
+ if (hookValue) {
188
+ const hookOffset = hookValue.offset
189
+ const hookTime = hookValue.time || time
190
+ const hookAttenuated = hookValue.attenuated || true
191
+ if (hookOffset) {
192
+ this.scrollToOffset(hookOffset, hookTime, hookAttenuated)
193
+ return
194
+ }
195
+ }
196
+ }
197
+
198
+ // 走默认行为
199
+ this._startAutoScroll(targetDelta, time, true);
200
+ }
201
+
202
+ protected _onTouchBegan(event: EventTouch, captureListeners?: Node[]): void {
203
+ if (this.node.getComponent(YXCollectionView).scrollEnabled == false) { return }
204
+
205
+ // 记录开始滚动时的偏移量
206
+ let offset = this.getScrollOffset()
207
+ offset.x = - offset.x
208
+ this._yx_scroll_offset_on_touch_start = offset
209
+
210
+ let nodes: Node[] = [event.target]
211
+ if (captureListeners) { nodes = nodes.concat(captureListeners) }
212
+ for (let index = 0; index < nodes.length; index++) {
213
+ const element = nodes[index];
214
+ // 清空滚动节点标记
215
+ element[`_yx_scroll_target`] = null
216
+ }
217
+ super._onTouchBegan(event, captureListeners)
218
+ }
219
+
220
+ protected _onTouchMoved(event: EventTouch, captureListeners?: Node[]): void {
221
+ if (this.node.getComponent(YXCollectionView).scrollEnabled == false) { return }
222
+ // 处理嵌套冲突,每次只滚动需要滚动的列表
223
+ let scrollTarget = this._yxGetScrollTarget(event, captureListeners)
224
+ if (this.node === scrollTarget) {
225
+ super._onTouchMoved(event, captureListeners)
226
+ }
227
+ }
228
+
229
+ protected _hasNestedViewGroup(event: Event, captureListeners?: Node[]): boolean {
230
+ // 直接把所有的列表都标记为可滑动,具体滑动哪一个,去 _onTouchMoved 判断
231
+ return false
232
+ }
233
+
234
+ protected _stopPropagationIfTargetIsMe(event: Event): void {
235
+ if (this._touchMoved) {
236
+ event.propagationStopped = true;
237
+ return
238
+ }
239
+ super._stopPropagationIfTargetIsMe(event)
240
+ }
241
+
242
+ /**
243
+ * 获取本次滑动是要滑动哪个列表
244
+ */
245
+ private _yxGetScrollTarget(event: EventTouch, captureListeners?: Node[]): Node {
246
+ // 尝试获取本次已经确定了的滚动节点
247
+ let cache = event.target[`_yx_scroll_target`]
248
+ if (cache) {
249
+ return cache
250
+ }
251
+
252
+ let nodes: Node[] = [event.target]
253
+ if (captureListeners) {
254
+ nodes = nodes.concat(captureListeners)
255
+ }
256
+ if (nodes.length == 1) { return nodes[0] } // 无需处理冲突
257
+
258
+ let touch = event.touch;
259
+ let deltaMove = touch.getLocation().subtract(touch.getStartLocation());
260
+ let x = Math.abs(deltaMove.x)
261
+ let y = Math.abs(deltaMove.y)
262
+ let distance = Math.abs(x - y)
263
+ if (distance < 5) {
264
+ return null // 不足以计算出方向
265
+ }
266
+ /** @todo 边界检测,滑动到边缘时滑动事件交给其他可滑动列表 */
267
+
268
+ let result = null
269
+ for (let index = 0; index < nodes.length; index++) {
270
+ const element = nodes[index];
271
+ let scrollComp = element.getComponent(_scroll_view)
272
+ if (scrollComp) {
273
+ let collectionView = element.getComponent(YXCollectionView)
274
+ if (collectionView && collectionView.scrollEnabled == false) { continue } // 不支持滚动
275
+ if (result == null) { result = element } // 取第一个滚动组件作为默认响应者
276
+ if (scrollComp.horizontal && scrollComp.vertical) { continue } // 全方向滚动暂时不处理
277
+ if (!scrollComp.horizontal && !scrollComp.vertical) { continue } // 不支持滚动的也不处理
278
+ if (scrollComp.horizontal && x > y) {
279
+ result = element
280
+ break
281
+ }
282
+ if (scrollComp.vertical && y > x) {
283
+ result = element
284
+ break
285
+ }
286
+ }
287
+ }
288
+
289
+ // 给所有捕获到的节点都保存一份,方便任意一个节点都可以读到
290
+ if (result) {
291
+ for (let index = 0; index < nodes.length; index++) {
292
+ const element = nodes[index];
293
+ element[`_yx_scroll_target`] = result
294
+ }
295
+ }
296
+ return result
297
+ }
298
+ }
299
+
300
+ class _yx_node_pool extends NodePool {
301
+ getAtIdx(indexPath: YXIndexPath, ...args: any[]): Node | null {
302
+ const nodes: Node[] = this['_pool']
303
+ for (let index = 0; index < nodes.length; index++) {
304
+ const obj = nodes[index];
305
+ let comp = obj.getComponent(_yx_node_element_comp)
306
+ if (comp && comp.attributes.indexPath.equals(indexPath)) {
307
+ nodes.splice(index, 1)
308
+ // @ts-ignore
309
+ const handler = this.poolHandlerComp ? obj.getComponent(this.poolHandlerComp) : null;
310
+ if (handler && handler.reuse) { handler.reuse(arguments); }
311
+ return obj
312
+ }
313
+ }
314
+ return null
315
+ }
316
+ }
317
+
318
+ /**
319
+ * 节点的布局属性
320
+ */
321
+ export class YXLayoutAttributes {
322
+
323
+ /**
324
+ * 创建一个 cell 布局属性实例
325
+ */
326
+ static layoutAttributesForCell(indexPath: YXIndexPath): YXLayoutAttributes {
327
+ let result = new YXLayoutAttributes()
328
+ result._indexPath = indexPath
329
+ result._elementCategory = 'Cell'
330
+ return result
331
+ }
332
+
333
+ /**
334
+ * 创建一个 supplementary 布局属性实例
335
+ * @param kinds 自定义类别标识,更多说明查看 supplementaryKinds
336
+ */
337
+ static layoutAttributesForSupplementary(indexPath: YXIndexPath, kinds: string): YXLayoutAttributes {
338
+ let result = new YXLayoutAttributes()
339
+ result._indexPath = indexPath
340
+ result._elementCategory = 'Supplementary'
341
+ result._supplementaryKinds = kinds
342
+ return result
343
+ }
344
+
345
+ /**
346
+ * 构造方法,外部禁止直接访问,需要通过上面的静态方法创建对象
347
+ */
348
+ protected constructor() { }
349
+
350
+ /**
351
+ * 节点索引
352
+ */
353
+ get indexPath(): YXIndexPath { return this._indexPath }
354
+ private _indexPath: YXIndexPath = null
355
+
356
+ /**
357
+ * 节点种类
358
+ */
359
+ get elementCategory() { return this._elementCategory }
360
+ private _elementCategory: 'Cell' | 'Supplementary' = 'Cell'
361
+
362
+ /**
363
+ * Supplementary 种类,本身无实际意义,具体作用由自定义布局规则决定
364
+ */
365
+ get supplementaryKinds() { return this._supplementaryKinds }
366
+ private _supplementaryKinds: string = ''
367
+
368
+ /**
369
+ * 节点在滚动视图中的位置和大小属性
370
+ * origin 属性表示节点在父视图坐标系中的左上角的位置,size 属性表示节点的宽度和高度
371
+ */
372
+ get frame(): math.Rect { return this._frame }
373
+ private _frame: math.Rect = new math.Rect()
374
+
375
+ /**
376
+ * 节点层级
377
+ * 越小会越早的添加到滚动视图上
378
+ * https://docs.cocos.com/creator/manual/zh/ui-system/components/editor/ui-transform.html?h=uitrans
379
+ * 备注: 内部暂时是通过节点的 siblingIndex 实现的,如果自定义 layout 有修改这个值的需求,需要重写 layout 的 @shouldUpdateAttributesZIndex 方法,默认情况下会忽略这个配置
380
+ */
381
+ zIndex: number = 0
382
+
383
+ /**
384
+ * 节点透明度
385
+ * 备注: 内部通过 UIOpacity 组件实现,会修改节点 UIOpacity 组件的 opacity 值,如果自定义 layout 有修改这个值的需求,需要重写 layout 的 @shouldUpdateAttributesOpacity 方法,默认情况下会忽略这个配置
386
+ */
387
+ opacity: number = null
388
+
389
+ /**
390
+ * 节点变换 - 缩放
391
+ */
392
+ scale: math.Vec3 = null
393
+
394
+ /**
395
+ * 节点变换 - 平移
396
+ */
397
+ offset: math.Vec3 = null
398
+
399
+ /**
400
+ * 节点变换 - 旋转
401
+ * 备注: 3D 变换似乎需要透视相机???
402
+ */
403
+ eulerAngles: math.Vec3 = null
404
+ }
405
+
406
+ /**
407
+ * 布局规则
408
+ * 这里只是约定出了一套接口,内部只是一些基础实现,具体布局方案通过子类重载去实现
409
+ */
410
+ export abstract class YXLayout {
411
+ constructor() { }
412
+
413
+ /**
414
+ * @required
415
+ * 整个滚动区域大小
416
+ * 需要在 prepare 内初始化
417
+ */
418
+ contentSize: math.Size = math.Size.ZERO
419
+
420
+ /**
421
+ * @required
422
+ * 所有元素的布局属性
423
+ * 需要在 prepare 内初始化
424
+ * @todo 这个不应该限制为数组结构,准确来说是不应该限制开发者必须使用数组来保存所有布局属性,目前为了实现预加载模式暂时是必须要求数组结构,后续有好的方案的话应该考虑优化
425
+ */
426
+ attributes: YXLayoutAttributes[] = []
427
+
428
+ /**
429
+ * @required
430
+ * 子类重写实现布局方案
431
+ * 注意: 必须初始化滚动区域大小并赋值给 contentSize 属性
432
+ * 注意: 必须初始化所有的元素布局属性,并保存到 attributes 数组
433
+ * 可选: 根据 collectionView 的 scrollDirection 支持不同的滚动方向
434
+ */
435
+ abstract prepare(collectionView: YXCollectionView): void
436
+
437
+ /**
438
+ * @optional
439
+ * 列表在首次更新数据后会执行这个方法
440
+ * 在这个方法里设置滚动视图的初始偏移量
441
+ *
442
+ * @example
443
+ * // 比如一个垂直列表希望初始化时从最顶部开始展示数据,那么可以在这个方法里通过 scrollToTop 实现
444
+ * initOffset(collectionView: YXCollectionView): void {
445
+ * collectionView.scrollView.scrollToTop()
446
+ * }
447
+ */
448
+ initOffset(collectionView: YXCollectionView) { }
449
+
450
+ /**
451
+ * @optional
452
+ * 当一次手势拖动结束后会立即调用此方法,通过重写这个方法可以定制列表最终停留的位置
453
+ *
454
+ * @param collectionView 列表组件
455
+ * @param touchMoveVelocity 手势速度
456
+ * @param startOffset 此次手势开始时列表的偏移位置
457
+ * @param originTargetOffset 接下来将要自动滚动到的位置
458
+ * @param originScrollDuration 接下来的惯性滚动持续时间
459
+ * @returns 可以返回 null ,返回 null 执行默认的惯性滚动逻辑
460
+ *
461
+ * 另外关于返回值的字段说明
462
+ * @param offset 这个字段表示列表本次滚动结束时期望停留的位置,一旦返回了这个字段,列表最终将会停留至返回的这个位置
463
+ * @param time 可选,默认为 originScrollDuration,这个字段表示自动滚动至期望停留位置需要的时间
464
+ * @param attenuated 可选,默认为 true,这个字段表示惯性滚动速度是否衰减
465
+ */
466
+ targetOffset(collectionView: YXCollectionView, touchMoveVelocity: math.Vec3, startOffset: math.Vec2, originTargetOffset: math.Vec2, originScrollDuration: number): { offset: math.Vec2; time?: number; attenuated?: boolean; } | null { return null }
467
+
468
+ /**
469
+ * @optional
470
+ * 列表每次滚动结束后会调用此方法
471
+ */
472
+ onScrollEnded(collectionView: YXCollectionView) { }
473
+
474
+ /**
475
+ * @optional
476
+ * 当滚动视图的可见范围变化后执行,这个方法会在列表滚动过程中频繁的执行
477
+ * 在这个方法里可以调整节点属性以实现交互性的节点变换效果,(如果在这个方法里调整了节点变换属性,需要重写 shouldUpdateAttributesForBoundsChange 以支持实时变换)
478
+ *
479
+ * @param rect 当前滚动视图的可见区域
480
+ *
481
+ * @returns
482
+ * 关于这个方法的返回值,最优的情况应该是根据实际的布局情况计算出当前显示区域内需要显示的所有布局属性
483
+ * 列表在更新可见节点时会遍历这个方法返回的数组并依次检查节点是否需要添加到列表内,默认这个方法是直接返回所有的布局属性,也就是在更新可见节点时的时间复杂度默认为 O(attributes.length),除非有更优的算法,否则建议直接返回所有的布局属性
484
+ */
485
+ layoutAttributesForElementsInRect(rect: math.Rect, collectionView: YXCollectionView): YXLayoutAttributes[] {
486
+ return this.attributes
487
+ }
488
+ layoutAttributesForItemAtIndexPath(indexPath: YXIndexPath, collectionView: YXCollectionView): YXLayoutAttributes {
489
+ return this.attributes.find((a) => a.indexPath.equals(indexPath) && a.elementCategory === 'Cell')
490
+ }
491
+ layoutAttributesForSupplementaryAtIndexPath(indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string): YXLayoutAttributes {
492
+ return this.attributes.find((a) => a.indexPath.equals(indexPath) && a.elementCategory === 'Supplementary' && a.supplementaryKinds === kinds)
493
+ }
494
+
495
+ /**
496
+ * @optional
497
+ * 列表组件在调用 scrollTo 方法时会触发这个方法,如果实现了这个方法,最终的滚动停止位置以这个方法返回的为准
498
+ */
499
+ scrollTo(indexPath: YXIndexPath, collectionView: YXCollectionView): math.Vec2 { return null }
500
+
501
+ /**
502
+ * @optional
503
+ * @see YXLayoutAttributes.zIndex
504
+ */
505
+ shouldUpdateAttributesZIndex(): boolean { return false }
506
+
507
+ /**
508
+ * @optional
509
+ * @see YXLayoutAttributes.opacity
510
+ */
511
+ shouldUpdateAttributesOpacity(): boolean { return false }
512
+
513
+ /**
514
+ * @optional
515
+ * 此布局下的节点,是否需要实时更新变换效果
516
+ * @returns 返回 true 会忽略 YXCollectionView 的 frameInterval 设置,强制在滚动过程中实时更新节点
517
+ */
518
+ shouldUpdateAttributesForBoundsChange(): boolean { return false }
519
+
520
+ /**
521
+ * @optional
522
+ * 列表组件销毁时执行
523
+ */
524
+ onDestroy() { }
525
+ }
526
+
527
+ /**
528
+ * @see NodePool.poolHandlerComp
529
+ * 节点的自定义组件可以通过这个接口跟 NodePool 的重用业务关联起来
530
+ */
531
+ export interface YXCollectionViewCell extends Component {
532
+ unuse(): void;
533
+ reuse(args: any): void;
534
+ }
535
+
536
+ /**
537
+ * 列表组件
538
+ */
539
+ @ccclass('YXCollectionView')
540
+ @disallowMultiple(true)
541
+ @executionOrder(-1)
542
+ @help(`https://github.com/568071718/creator-collection-view`)
543
+ export class YXCollectionView extends Component {
544
+
545
+ /**
546
+ * 访问定义的私有枚举
547
+ */
548
+ static ScrollDirection = _yx_collection_view_scroll_direction
549
+ static Mode = _yx_collection_view_list_mode
550
+
551
+ /**
552
+ * 滚动视图组件
553
+ */
554
+ get scrollView(): ScrollView {
555
+ let result = this.node.getComponent(_scroll_view)
556
+ if (result == null) {
557
+ result = this.node.addComponent(_scroll_view)
558
+ // 配置 scroll view 默认参数
559
+ }
560
+ if (result.content == null) {
561
+ let content = new Node(`com.yx.scroll.content`)
562
+ content.parent = result.node
563
+ content.layer = content.parent.layer
564
+
565
+ let transform = content.getComponent(UITransform) || content.addComponent(UITransform)
566
+ transform.contentSize = this.node.getComponent(UITransform).contentSize
567
+
568
+ result.content = content
569
+ }
570
+
571
+ if (this.mask) {
572
+ let mask = result.node.getComponent(Mask)
573
+ if (mask == null) {
574
+ mask = result.node.addComponent(Mask)
575
+ mask.type = Mask.Type.GRAPHICS_RECT
576
+ }
577
+ }
578
+
579
+ return result
580
+ }
581
+ private get _scrollView(): _scroll_view { return this.scrollView as _scroll_view }
582
+
583
+ /**
584
+ * 自动给挂载节点添加 mask 组件
585
+ */
586
+ @property({ tooltip: `自动给挂载节点添加 mask 组件`, visible: true })
587
+ private mask: boolean = true
588
+
589
+ /**
590
+ * 允许手势滚动
591
+ */
592
+ @property({ tooltip: `允许手势滚动` })
593
+ scrollEnabled: boolean = true
594
+
595
+ /**
596
+ * 允许鼠标滑轮滚动
597
+ */
598
+ @property({ tooltip: `允许鼠标滑轮滚动` })
599
+ wheelScrollEnabled: boolean = false
600
+
601
+ /**
602
+ * 列表滚动方向,默认垂直方向滚动
603
+ * 自定义 YXLayout 应该尽量根据这个配置来实现不同方向的布局业务
604
+ * 备注: 如果使用的 YXLayout 未支持对应的滚动方向,则此配置不会生效,严格来说这个滚动方向本就应该是由 YXLayout 决定的,定义在这里是为了编辑器配置方便
605
+ */
606
+ @property({ type: _yx_collection_view_scroll_direction, tooltip: `列表滚动方向` })
607
+ scrollDirection: YXCollectionView.ScrollDirection = YXCollectionView.ScrollDirection.VERTICAL
608
+
609
+ /**
610
+ * 列表单元节点加载模式
611
+ */
612
+ @property({ type: _yx_collection_view_list_mode, tooltip: `列表单元节点加载模式 (详细区别查看枚举注释)\nRECYCLE: 根据列表显示范围加载需要的节点,同类型的节点会进行复用\nPRELOAD: 会实例化所有节点,并非真正的虚拟列表,仅仅是把显示范围外的节点透明了,如果列表数据量很大仍然会卡` })
613
+ mode: YXCollectionView.Mode = YXCollectionView.Mode.RECYCLE
614
+
615
+ /**
616
+ * 预加载模式下每帧加载多少个节点
617
+ */
618
+ @property({
619
+ tooltip: `预加载模式下每帧加载多少个节点`,
620
+ visible: function (this) {
621
+ return (this.mode == _yx_collection_view_list_mode.PRELOAD)
622
+ }
623
+ })
624
+ preloadNodesLimitPerFrame: number = 2
625
+
626
+ /**
627
+ * 预加载进度
628
+ */
629
+ preloadProgress: (current: number, total: number) => void = null
630
+
631
+ /**
632
+ * 每多少帧刷新一次可见节点,1 表示每帧都刷
633
+ */
634
+ @property({ tooltip: `每多少帧刷新一次可见节点,1 表示每帧都刷` })
635
+ frameInterval: number = 1
636
+
637
+ /**
638
+ * 滚动过程中,每多少帧回收一次不可见节点,1表示每帧都回收,0表示不在滚动过程中回收不可见节点
639
+ * @bug 滚动过程中如果实时的回收不可见节点,有时候会收不到 scroll view 的 cancel 事件,导致 scroll view 的滚动状态不会更新 (且收不到滚动结束事件)
640
+ * @fix 当这个属性设置为 0 时,只会在 `touch-up` 和 `scroll-ended` 里面回收不可见节点
641
+ */
642
+ @property({ tooltip: `滚动过程中,每多少帧回收一次不可见节点,1表示每帧都回收,0表示不在滚动过程中回收不可见节点` })
643
+ recycleInterval: number = 1
644
+
645
+ /**
646
+ * 通过编辑器注册节点类型
647
+ */
648
+ @property({ type: [_yx_editor_register_element_info_2], visible: true, displayName: `Register Cells`, tooltip: `配置此列表内需要用到的 cell 节点类型` })
649
+ private registerCellForEditor: _yx_editor_register_element_info_2[] = []
650
+ @property({ type: [_yx_editor_register_element_info_2], visible: true, displayName: `Register Supplementarys`, tooltip: `配置此列表内需要用到的 Supplementary 节点类型` })
651
+ private registerSupplementaryForEditor: _yx_editor_register_element_info_2[] = []
652
+
653
+ /**
654
+ * 注册 cell
655
+ * 可多次注册不同种类的 cell,只要确保 identifier 的唯一性就好
656
+ * @param identifier cell 标识符,通过 dequeueReusableCell 获取重用 cell 时,会根据这个值匹配
657
+ * @param maker 生成节点,当重用池里没有可用的节点时,会通过这个回调获取节点,需要在这个回调里面生成节点
658
+ * @param poolComp (可选) 节点自定义组件,可以通过这个组件跟 NodePool 的重用业务关联起来
659
+ */
660
+ registerCell(identifier: string, maker: () => Node, poolComp: (new (...args: any[]) => YXCollectionViewCell) | string | null = null) {
661
+ let elementCategory: typeof YXLayoutAttributes.prototype.elementCategory = 'Cell'
662
+ identifier = elementCategory + identifier
663
+ let pool = new _yx_node_pool(poolComp)
664
+ this.pools.set(identifier, pool)
665
+ this.makers.set(identifier, maker)
666
+ }
667
+
668
+ /**
669
+ * 注册 supplementary 追加视图,用法跟 registerCell 一样
670
+ */
671
+ registerSupplementary(identifier: string, maker: () => Node, poolComp: (new (...args: any[]) => YXCollectionViewCell) | string | null = null) {
672
+ let elementCategory: typeof YXLayoutAttributes.prototype.elementCategory = 'Supplementary'
673
+ identifier = elementCategory + identifier
674
+ let pool = new _yx_node_pool(poolComp)
675
+ this.pools.set(identifier, pool)
676
+ this.makers.set(identifier, maker)
677
+ }
678
+
679
+ /**
680
+ * 每个注册的标识符对应一个节点池
681
+ */
682
+ private pools: Map<string, NodePool> = new Map()
683
+
684
+ /**
685
+ * 每个注册的标识符对应一个生成节点回调
686
+ */
687
+ private makers: Map<string, () => Node> = new Map()
688
+
689
+ /**
690
+ * 通过标识符从重用池里取出一个可用的 cell 节点
691
+ * @param identifier 注册时候的标识符
692
+ * @param indexPath 可选,尝试通过 indexPath 获取刷新之前的节点 (尽可能的保证刷新前和刷新后这个位置仍然是同一个节点),避免节点复用导致的刷新闪烁问题
693
+ */
694
+ dequeueReusableCell(identifier: string, indexPath: YXIndexPath = null): Node {
695
+ return this._dequeueReusableElement(identifier, 'Cell', indexPath)
696
+ }
697
+
698
+ /**
699
+ * 通过标识符从重用池里取出一个可用的 supplementary 节点
700
+ * @param identifier 注册时候的标识符
701
+ * @param indexPath 可选,尝试通过 indexPath 获取刷新之前的节点 (尽可能的保证刷新前和刷新后这个位置仍然是同一个节点),避免节点复用导致的刷新闪烁问题
702
+ */
703
+ dequeueReusableSupplementary(identifier: string, indexPath: YXIndexPath = null): Node {
704
+ return this._dequeueReusableElement(identifier, 'Supplementary', indexPath)
705
+ }
706
+ private _dequeueReusableElement(identifier: string, elementCategory: typeof YXLayoutAttributes.prototype.elementCategory, indexPath: YXIndexPath = null) {
707
+ identifier = elementCategory + identifier
708
+ let pool = this.pools.get(identifier)
709
+ if (pool == null) {
710
+ throw new Error(`YXCollectionView: dequeueReusable${elementCategory} 错误,未注册的 identifier`);
711
+ }
712
+ let result: Node = null
713
+
714
+ // 尝试从重用池获取 (牺牲一点性能,尝试通过 indexPath 获取对应的节点,防止刷新闪烁的问题)
715
+ if (result == null && indexPath && pool instanceof _yx_node_pool) {
716
+ result = pool.getAtIdx(indexPath)
717
+ }
718
+
719
+ // 尝试从重用池获取
720
+ if (result == null) {
721
+ result = pool.get()
722
+ }
723
+
724
+ // 重新生成一个
725
+ if (result == null) {
726
+ const maker = this.makers.get(identifier)
727
+ result = maker()
728
+ let cell = result.getComponent(_yx_node_element_comp) || result.addComponent(_yx_node_element_comp)
729
+ cell.identifier = identifier
730
+
731
+ result.on(NodeEventType.TOUCH_END, this.onTouchElement, this)
732
+ }
733
+ return result
734
+ }
735
+
736
+ /**
737
+ * 内容要分几个区展示,默认 1
738
+ * 没有分区展示的需求可以不管这个配置
739
+ */
740
+ numberOfSections: number | ((collectionView: YXCollectionView) => number) = 1
741
+ getNumberOfSections(): number {
742
+ if (this.numberOfSections instanceof Function) { return this.numberOfSections(this) }
743
+ return this.numberOfSections
744
+ }
745
+
746
+ /**
747
+ * 每个区里要展示多少条内容
748
+ */
749
+ numberOfItems: number | ((section: number, collectionView: YXCollectionView) => number) = 0
750
+ getNumberOfItems(section: number): number {
751
+ if (this.numberOfItems instanceof Function) { return this.numberOfItems(section, this) }
752
+ return this.numberOfItems
753
+ }
754
+
755
+ /**
756
+ * 配置每块内容对应的 UI 节点
757
+ * 在这个方法里,需要确定 indexPath 这个位置对应的节点应该是用注册过的哪个类型的 Node 节点,然后通过 dequeueReusableCell 生成对应的 Node
758
+ *
759
+ * @example
760
+ * yourList.cellForItemAt = (indexPath ,collectionView) => {
761
+ * let cell = collectionView.dequeueReusableCell(`your identifier`)
762
+ * let comp = cell.getComponent(YourCellComp)
763
+ * comp.label.string = `${indexPath}`
764
+ * return cell
765
+ * }
766
+ *
767
+ * @returns 注意: 不要在这个方法里创建新的节点对象,这个方法返回的 Node,必须是通过 dequeueReusableCell 匹配到的 Node
768
+ */
769
+ cellForItemAt: (indexPath: YXIndexPath, collectionView: YXCollectionView) => Node = null
770
+
771
+ /**
772
+ * 用法跟 cellForItemAt 差不多,此方法内需要通过 dequeueReusableSupplementary 获取 Node 节点
773
+ * @param kinds 关于这个字段的具体含义应该根据使用的自定义 layout 决定
774
+ */
775
+ supplementaryForItemAt: (indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string) => Node = null
776
+
777
+ /**
778
+ * cell 节点可见状态回调
779
+ * 如果同类型的节点大小可能不一样,可以在这里调整子节点的位置
780
+ */
781
+ onCellDisplay: (node: Node, indexPath: YXIndexPath, collectionView: YXCollectionView) => void = null
782
+ onCellEndDisplay: (node: Node, indexPath: YXIndexPath, collectionView: YXCollectionView) => void = null
783
+
784
+ /**
785
+ * supplementary 节点可见状态回调
786
+ */
787
+ onSupplementaryDisplay: (node: Node, indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string) => void = null
788
+ onSupplementaryEndDisplay: (node: Node, indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string) => void = null
789
+
790
+ /**
791
+ * 点击到 cell 节点后执行
792
+ */
793
+ onTouchCellAt: (indexPath: YXIndexPath, collectionView: YXCollectionView) => void = null
794
+
795
+ /**
796
+ * 点击到 supplementary 节点后执行
797
+ */
798
+ onTouchSupplementaryAt: (indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string) => void = null
799
+
800
+ /**
801
+ * 节点点击事件
802
+ */
803
+ private onTouchElement(ev: EventTouch) {
804
+ const node = ev.target
805
+ if (node instanceof Node == false) { return }
806
+ const cell = node.getComponent(_yx_node_element_comp)
807
+ if (cell == null) { return }
808
+ const attr = cell.attributes
809
+ if (attr == null) { return }
810
+ if (attr.elementCategory === 'Cell') {
811
+ if (this.onTouchCellAt) {
812
+ this.onTouchCellAt(attr.indexPath, this)
813
+ return
814
+ }
815
+ return
816
+ }
817
+ if (attr.elementCategory === 'Supplementary') {
818
+ if (this.onTouchSupplementaryAt) {
819
+ this.onTouchSupplementaryAt(attr.indexPath, this, attr.supplementaryKinds)
820
+ }
821
+ return
822
+ }
823
+ }
824
+
825
+ /**
826
+ * 布局规则
827
+ */
828
+ layout: YXLayout = null
829
+
830
+ /**
831
+ * 记录当前正在显示的所有节点
832
+ * 通过 Map 结构实现,减少查找复杂度,key = indexpath.string value = 对应的节点
833
+ */
834
+ private visibleNodesMap: Map<string, Node> = new Map()
835
+
836
+ /**
837
+ * 记录预加载的所有节点
838
+ * 相当于是 preload 模式下的节点缓存池子
839
+ */
840
+ private preloadNodesMap: Map<string, Node> = new Map()
841
+
842
+ /**
843
+ * 获取节点缓存 key
844
+ */
845
+ private _getLayoutAttributesCacheKey(element: YXLayoutAttributes): string {
846
+ return this._getVisibleCacheKey(element.indexPath, element.elementCategory, element.supplementaryKinds)
847
+ }
848
+ private _getVisibleCacheKey(indexPath: YXIndexPath, elementCategory: typeof YXLayoutAttributes.prototype.elementCategory, kinds: string = '') {
849
+ return `${indexPath}${elementCategory}${kinds}`
850
+ }
851
+
852
+ /**
853
+ * 获取列表当前的可见范围
854
+ */
855
+ getVisibleRect(): math.Rect {
856
+ const visibleRect = _scroll_view_visible_rect
857
+ visibleRect.origin = this.scrollView.getScrollOffset()
858
+ visibleRect.x = - visibleRect.x
859
+ visibleRect.size = this.scrollView.view.contentSize
860
+ return visibleRect
861
+ }
862
+
863
+ /**
864
+ * 通过索引获取指定的可见的 cell 节点
865
+ */
866
+ getVisibleCellNode(indexPath: YXIndexPath): Node {
867
+ const cacheKey = this._getVisibleCacheKey(indexPath, 'Cell')
868
+ return this.visibleNodesMap.get(cacheKey)
869
+ }
870
+
871
+ /**
872
+ * 通过索引获取指定的可见的 supplementary 节点
873
+ */
874
+ getVisibleSupplementaryNode(indexPath: YXIndexPath, kinds: string): Node {
875
+ const cacheKey = this._getVisibleCacheKey(indexPath, 'Supplementary', kinds)
876
+ return this.visibleNodesMap.get(cacheKey)
877
+ }
878
+
879
+ /**
880
+ * 获取所有正在显示的 cell 节点
881
+ */
882
+ getVisibleCellNodes(): Node[] {
883
+ let result: Node[] = []
884
+ this.visibleNodesMap.forEach((value) => {
885
+ const comp = value.getComponent(_yx_node_element_comp)
886
+ if (comp.attributes.elementCategory === 'Cell') {
887
+ result.push(value)
888
+ }
889
+ })
890
+ return result
891
+ }
892
+
893
+ /**
894
+ * 获取所有正在显示的 supplementary 节点
895
+ * @param kinds 可选按种类筛选
896
+ */
897
+ getVisibleSupplementaryNodes(kinds: string = null): Node[] {
898
+ let result: Node[] = []
899
+ this.visibleNodesMap.forEach((value) => {
900
+ const comp = value.getComponent(_yx_node_element_comp)
901
+ if (comp.attributes.elementCategory === 'Supplementary') {
902
+ if (kinds === null || comp.attributes.supplementaryKinds === kinds) {
903
+ result.push(value)
904
+ }
905
+ }
906
+ })
907
+ return result
908
+ }
909
+
910
+ /**
911
+ * 获取指定节点绑定的布局属性对象
912
+ */
913
+ getElementAttributes(node: Node): YXLayoutAttributes {
914
+ const comp = node.getComponent(_yx_node_element_comp)
915
+ return comp ? comp.attributes : null
916
+ }
917
+
918
+ maxWidth: number = -1
919
+ /**数量小于最大宽就居中显示 */
920
+ isCenterShow: boolean = false
921
+ /**少于最大宽就居中显示 */
922
+ layoutCenter() {
923
+ if (!this.isCenterShow) {
924
+ return;
925
+ }
926
+ let count = 0
927
+ if (this.numberOfItems instanceof Function) {
928
+ count = this.numberOfItems(1, this)
929
+ } else {
930
+ count = this.numberOfItems
931
+ }
932
+ const layout = (this.layout as any)
933
+ const size: math.Size = layout.itemSize
934
+ const space = layout.horizontalSpacing
935
+ const width = size.width
936
+ const ut = this.node.getComponent(UITransform)
937
+ if (this.maxWidth == -1) {
938
+ this.maxWidth = ut.width
939
+ }
940
+ const value = count * width + (space) * (count - 1)
941
+ this.scrollEnabled = value >= this.maxWidth
942
+ // this.node.getComponent(YXCollectionView).scrollEnabled = this.scrollEnabled
943
+ ut.width = Math.min(this.maxWidth, value)
944
+ }
945
+ /**
946
+ * 刷新列表数据
947
+ */
948
+ reloadData() {
949
+ this.layoutCenter()
950
+ if (this.node.activeInHierarchy && this.node.parent) {
951
+ this._reloadData()
952
+ return
953
+ }
954
+ this._late_reload_data = true
955
+ }
956
+ private update_reloadDataIfNeeds(dt: number) {
957
+ if (this._late_reload_data == false) { return }
958
+ this._reloadData()
959
+ }
960
+ private _reloadData() {
961
+ this._late_reload_data = false
962
+ // 校验 layout 参数
963
+ if (this.layout == null) {
964
+ throw new Error("YXCollectionView: 参数错误,请正确配置 layout 以确定布局方案");
965
+ }
966
+ // 立即停止当前滚动,准备刷新
967
+ this.scrollView.stopAutoScroll()
968
+
969
+ // 池子先清一下,可能会累积很多暂时用不到的节点
970
+ this.pools.forEach((element) => { element.clear() })
971
+
972
+ // 回收模式下,移除掉正在显示的节点并加到池子里 (不需要销毁)
973
+ if (this.mode == _yx_collection_view_list_mode.RECYCLE) {
974
+ this.visibleNodesMap.forEach((value, key) => {
975
+ const cell = value.getComponent(_yx_node_element_comp)
976
+ this.pools.get(cell.identifier).put(value)
977
+ this.visibleNodesMap.delete(key) // 从可见节点里删除
978
+ if (cell.attributes.elementCategory === 'Cell') {
979
+ if (this.onCellEndDisplay) {
980
+ this.onCellEndDisplay(cell.node, cell.attributes.indexPath, this)
981
+ }
982
+ }
983
+ if (cell.attributes.elementCategory === 'Supplementary') {
984
+ if (this.onSupplementaryEndDisplay) {
985
+ this.onSupplementaryEndDisplay(cell.node, cell.attributes.indexPath, this, cell.attributes.supplementaryKinds)
986
+ }
987
+ }
988
+ })
989
+ this.visibleNodesMap.clear()
990
+ }
991
+
992
+ // 预加载模式下,需要清空当前显示的所有节点以及已经预加载过的所有节点 (全部销毁)
993
+ if (this.mode == _yx_collection_view_list_mode.PRELOAD) {
994
+ // 销毁当前所有正在显示的节点
995
+ this.visibleNodesMap.forEach((value, key) => {
996
+ if (value) {
997
+ value.removeFromParent()
998
+ value.destroy()
999
+ }
1000
+ })
1001
+ this.visibleNodesMap.clear()
1002
+
1003
+ // 销毁所有预加载的节点
1004
+ this.preloadNodesMap.forEach((value, key) => {
1005
+ if (value) {
1006
+ value.removeFromParent()
1007
+ value.destroy()
1008
+ }
1009
+ })
1010
+ this.preloadNodesMap.clear()
1011
+
1012
+ // 从第一个开始预加载节点
1013
+ this.preloadIdx = 0
1014
+ }
1015
+
1016
+ // 记录一下当前的偏移量,保证数据更新之后位置也不会太偏
1017
+ let offset = this.scrollView.getScrollOffset()
1018
+ offset.x = -offset.x
1019
+
1020
+ // 重新计算一遍布局属性
1021
+ this.layout.prepare(this)
1022
+
1023
+ // 更新 content size
1024
+ let contentTransform = this.scrollView.content.getComponent(UITransform) || this.scrollView.content.addComponent(UITransform)
1025
+ contentTransform.contentSize = this.layout.contentSize
1026
+
1027
+ // 默认偏移量 或者 恢复偏移量
1028
+ if (this.reloadDataCounter <= 0) {
1029
+ this.layout.initOffset(this)
1030
+ } else {
1031
+ let maxOffset = this.scrollView.getMaxScrollOffset()
1032
+ math.Vec2.min(offset, offset, maxOffset)
1033
+ this.scrollView.scrollToOffset(offset)
1034
+ }
1035
+
1036
+ // 更新可见 cell 节点
1037
+ this.markForUpdateVisibleData(true)
1038
+ this.reloadDataCounter++
1039
+ }
1040
+
1041
+ /**
1042
+ * 记录 @reloadData 执行了多少次了,用来区分刷新列表的时候是否是首次刷新列表
1043
+ */
1044
+ private reloadDataCounter: number = 0
1045
+
1046
+ /**
1047
+ * 根据当前的可见区域调整需要显示的节点
1048
+ */
1049
+ private reloadVisibleElements(visibleRect: math.Rect = null) {
1050
+ this._late_update_visible_data = false
1051
+ if (visibleRect == null) { visibleRect = this.getVisibleRect() }
1052
+
1053
+ // 根据可见区域,找出对应的布局属性
1054
+ let layoutAttributes = this.layout.layoutAttributesForElementsInRect(visibleRect, this)
1055
+
1056
+ // 按 zIndex 排序
1057
+ let shouldUpdateAttributesZIndex = this.layout.shouldUpdateAttributesZIndex()
1058
+ if (shouldUpdateAttributesZIndex) {
1059
+ if (layoutAttributes == null || layoutAttributes == this.layout.attributes) {
1060
+ layoutAttributes = this.layout.attributes.slice()
1061
+ }
1062
+ layoutAttributes.sort((a, b) => a.zIndex - b.zIndex)
1063
+ }
1064
+
1065
+ let shouldUpdateAttributesForBoundsChange = this.layout.shouldUpdateAttributesForBoundsChange()
1066
+
1067
+ // 添加需要显示的节点
1068
+ for (let index = 0; index < layoutAttributes.length; index++) {
1069
+ const element = layoutAttributes[index];
1070
+ if (visibleRect.intersects(element.frame) == false) { continue }
1071
+ const cacheKey = this._getLayoutAttributesCacheKey(element)
1072
+ let elementNode = null
1073
+ // 检查是否已经预加载过了
1074
+ if (elementNode == null) {
1075
+ elementNode = this.preloadNodesMap.get(cacheKey)
1076
+ }
1077
+ // 检查节点是否正在显示了
1078
+ if (elementNode == null) {
1079
+ elementNode = this.visibleNodesMap.get(cacheKey)
1080
+ }
1081
+ // 尝试通过注册标识符从节点池获取节点
1082
+ if (elementNode == null) {
1083
+ if (element.elementCategory === 'Cell') {
1084
+ elementNode = this.cellForItemAt(element.indexPath, this)
1085
+ }
1086
+ if (element.elementCategory === 'Supplementary') {
1087
+ elementNode = this.supplementaryForItemAt(element.indexPath, this, element.supplementaryKinds)
1088
+ }
1089
+ }
1090
+ // 无法正确获取节点,报错
1091
+ if (elementNode == null) {
1092
+ if (element.elementCategory === 'Cell') {
1093
+ throw new Error("需要实现 cellForItemAt 方法并确保正确的返回了节点");
1094
+ }
1095
+ if (element.elementCategory === 'Supplementary') {
1096
+ throw new Error("需要实现 supplementaryForItemAt 方法并确保正确的返回了节点");
1097
+ }
1098
+ }
1099
+
1100
+ // 恢复节点状态
1101
+ const restoreStatus = this.restoreNodeIfNeeds(elementNode)
1102
+
1103
+ // 更新节点变化
1104
+ if (restoreStatus == 1 || shouldUpdateAttributesForBoundsChange) {
1105
+ this.applyLayoutAttributes(elementNode, element)
1106
+ }
1107
+
1108
+ // 调整节点层级
1109
+ if (shouldUpdateAttributesZIndex) {
1110
+ elementNode.setSiblingIndex(-1)
1111
+ }
1112
+
1113
+ // 标记此节点正在显示
1114
+ this.visibleNodesMap.set(cacheKey, elementNode)
1115
+
1116
+ // 通知 display
1117
+ if (restoreStatus == 1) {
1118
+ if (element.elementCategory === 'Cell') {
1119
+ if (this.onCellDisplay) {
1120
+ this.onCellDisplay(elementNode, element.indexPath, this)
1121
+ }
1122
+ }
1123
+ if (element.elementCategory === 'Supplementary') {
1124
+ if (this.onSupplementaryDisplay) {
1125
+ this.onSupplementaryDisplay(elementNode, element.indexPath, this, element.supplementaryKinds)
1126
+ }
1127
+ }
1128
+ }
1129
+ }
1130
+
1131
+ layoutAttributes = []
1132
+ }
1133
+
1134
+ /**
1135
+ * 节点被回收后需要重新使用时,根据当前回收模式恢复节点的状态,保证节点可见
1136
+ */
1137
+ private restoreNodeIfNeeds(node: Node) {
1138
+ // 是否触发了恢复行为,0表示节点已经可见了 1表示触发了恢复行为,节点从不可见变为了可见
1139
+ let restoreStatus = 0
1140
+
1141
+ // 不管哪种模式,父节点检查都是必须的,只有正确的添加了才能确保正常可见
1142
+ if (node.parent != this.scrollView.content) {
1143
+ node.parent = this.scrollView.content
1144
+ restoreStatus = 1
1145
+ }
1146
+
1147
+ // 如果启用了预加载模式,给节点挂上 UIOpacity 组件,未启用则不管
1148
+ let opacityComp = node.getComponent(UIOpacity)
1149
+ if (this.mode == _yx_collection_view_list_mode.PRELOAD) {
1150
+ if (opacityComp == null) {
1151
+ opacityComp = node.addComponent(UIOpacity)
1152
+ }
1153
+ }
1154
+ if (opacityComp) {
1155
+ if (opacityComp.opacity !== 255) {
1156
+ opacityComp.opacity = 255
1157
+ restoreStatus = 1
1158
+ }
1159
+ }
1160
+
1161
+ return restoreStatus
1162
+ }
1163
+
1164
+ /**
1165
+ * 回收不可见节点
1166
+ */
1167
+ private recycleInvisibleNodes(visibleRect: math.Rect = null) {
1168
+ this._late_recycle_invisible_node = false
1169
+ if (visibleRect == null) { visibleRect = this.getVisibleRect() }
1170
+
1171
+ const _realFrame = _recycleInvisibleNodes_realFrame
1172
+ const _contentSize = this.scrollView.content.getComponent(UITransform).contentSize
1173
+
1174
+ this.visibleNodesMap.forEach((value, key) => {
1175
+ const cell = value.getComponent(_yx_node_element_comp)
1176
+ /**
1177
+ * @version 1.0.2
1178
+ * 检查节点是否可见应该是通过变换后的位置来检查
1179
+ * 通过 boundingBox 获取实际变换后的大小
1180
+ * 把实际的 position 转换为 origin
1181
+ */
1182
+ let boundingBox = value.getComponent(UITransform).getBoundingBox()
1183
+ _realFrame.size = boundingBox.size
1184
+ _realFrame.x = (_contentSize.width - _realFrame.width) * 0.5 + value.position.x
1185
+ _realFrame.y = (_contentSize.height - _realFrame.height) * 0.5 - value.position.y
1186
+ if (visibleRect.intersects(_realFrame) == false) {
1187
+ if (this.mode == _yx_collection_view_list_mode.PRELOAD) {
1188
+ value.getComponent(UIOpacity).opacity = 0
1189
+ this.preloadNodesMap.set(key, value)
1190
+ } else {
1191
+ this.pools.get(cell.identifier).put(value)
1192
+ }
1193
+ this.visibleNodesMap.delete(key) // 从可见节点里删除
1194
+ if (cell.attributes.elementCategory === 'Cell') {
1195
+ if (this.onCellEndDisplay) {
1196
+ this.onCellEndDisplay(cell.node, cell.attributes.indexPath, this)
1197
+ }
1198
+ }
1199
+ if (cell.attributes.elementCategory === 'Supplementary') {
1200
+ if (this.onSupplementaryEndDisplay) {
1201
+ this.onSupplementaryEndDisplay(cell.node, cell.attributes.indexPath, this, cell.attributes.supplementaryKinds)
1202
+ }
1203
+ }
1204
+ }
1205
+ })
1206
+ }
1207
+
1208
+ /**
1209
+ * 调整节点的位置/变换
1210
+ */
1211
+ private applyLayoutAttributes(node: Node, attributes: YXLayoutAttributes) {
1212
+ let cell = node.getComponent(_yx_node_element_comp)
1213
+ cell.attributes = attributes
1214
+
1215
+ let transform = node.getComponent(UITransform) || node.addComponent(UITransform)
1216
+ transform.setContentSize(attributes.frame.size)
1217
+
1218
+ _vec3Out.x = - (this.layout.contentSize.width - attributes.frame.width) * 0.5 + attributes.frame.x
1219
+ _vec3Out.y = + (this.layout.contentSize.height - attributes.frame.height) * 0.5 - attributes.frame.y
1220
+ _vec3Out.z = node.position.z
1221
+ if (attributes.offset) {
1222
+ math.Vec3.add(_vec3Out, _vec3Out, attributes.offset)
1223
+ }
1224
+ node.position = _vec3Out
1225
+
1226
+ if (attributes.scale) {
1227
+ node.scale = attributes.scale
1228
+ }
1229
+ if (attributes.eulerAngles) {
1230
+ node.eulerAngles = attributes.eulerAngles
1231
+ }
1232
+ if (this.layout.shouldUpdateAttributesOpacity() && attributes.opacity) {
1233
+ let opacity = node.getComponent(UIOpacity) || node.addComponent(UIOpacity)
1234
+ opacity.opacity = attributes.opacity
1235
+ }
1236
+ }
1237
+
1238
+ /**
1239
+ * 刷新当前可见节点
1240
+ * @param force true: 立即刷新; false: 根据设置的刷新帧间隔在合适的时候刷新
1241
+ */
1242
+ markForUpdateVisibleData(force: boolean = false) {
1243
+ if (force) {
1244
+ const visibleRect = this.getVisibleRect()
1245
+ this.reloadVisibleElements(visibleRect)
1246
+ this.recycleInvisibleNodes(visibleRect)
1247
+ return
1248
+ }
1249
+ this._late_update_visible_data = true
1250
+ this._late_recycle_invisible_node = true
1251
+ }
1252
+
1253
+ /**
1254
+ * 滚动到指定节点的位置
1255
+ * @todo 支持偏移方位,目前固定是按顶部的位置的,有特殊需求的建议直接通过 .scrollView.scrollToOffset() 实现
1256
+ */
1257
+ scrollTo(indexPath: YXIndexPath, timeInSecond: number = 0, attenuated: boolean = true) {
1258
+ let toOffSet: math.Vec2 = this.layout.scrollTo(indexPath, this)
1259
+ if (toOffSet == null) {
1260
+ toOffSet = this.layout.layoutAttributesForItemAtIndexPath(indexPath, this)?.frame.origin
1261
+ }
1262
+ if (toOffSet) {
1263
+ this.scrollView.stopAutoScroll()
1264
+ this.scrollView.scrollToOffset(toOffSet, timeInSecond, attenuated)
1265
+ this.markForUpdateVisibleData()
1266
+ }
1267
+ }
1268
+
1269
+ /**
1270
+ * 生命周期方法
1271
+ */
1272
+ protected onLoad(): void {
1273
+ for (let index = 0; index < this.registerCellForEditor.length; index++) {
1274
+ const element = this.registerCellForEditor[index];
1275
+ this.registerCell(element.identifier, () => instantiate(element.prefab), element.comp)
1276
+ }
1277
+ for (let index = 0; index < this.registerSupplementaryForEditor.length; index++) {
1278
+ const element = this.registerSupplementaryForEditor[index];
1279
+ this.registerSupplementary(element.identifier, () => instantiate(element.prefab), element.comp)
1280
+ }
1281
+ this.node.on(ScrollView.EventType.SCROLL_BEGAN, this.onScrollBegan, this)
1282
+ this.node.on(ScrollView.EventType.SCROLLING, this.onScrolling, this)
1283
+ this.node.on(ScrollView.EventType.TOUCH_UP, this.onScrollTouchUp, this)
1284
+ this.node.on(ScrollView.EventType.SCROLL_ENDED, this.onScrollEnded, this)
1285
+ this._scrollView._yx_startAttenuatingAutoScrollTargetOffset = (touchMoveVelocity, startOffset, originTargetOffset, originScrollTime) => {
1286
+ return this.layout.targetOffset(this, touchMoveVelocity, startOffset, originTargetOffset, originScrollTime)
1287
+ }
1288
+ }
1289
+
1290
+ protected onDestroy(): void {
1291
+ this.node.off(ScrollView.EventType.SCROLL_BEGAN, this.onScrollBegan, this)
1292
+ this.node.off(ScrollView.EventType.SCROLLING, this.onScrolling, this)
1293
+ this.node.off(ScrollView.EventType.TOUCH_UP, this.onScrollTouchUp, this)
1294
+ this.node.off(ScrollView.EventType.SCROLL_ENDED, this.onScrollEnded, this)
1295
+ this._scrollView._yx_startAttenuatingAutoScrollTargetOffset = null
1296
+
1297
+ // 销毁当前所有正在显示的节点
1298
+ this.visibleNodesMap.forEach((value, key) => {
1299
+ if (value && value.isValid) {
1300
+ value.removeFromParent()
1301
+ value.destroy()
1302
+ }
1303
+ })
1304
+ this.visibleNodesMap.clear()
1305
+ this.visibleNodesMap = null
1306
+
1307
+ // 销毁所有预加载的节点
1308
+ this.preloadNodesMap.forEach((value, key) => {
1309
+ if (value) {
1310
+ value.removeFromParent()
1311
+ value.destroy()
1312
+ }
1313
+ })
1314
+ this.preloadNodesMap.clear()
1315
+ this.preloadNodesMap = null
1316
+
1317
+ // 清空池子
1318
+ this.pools.forEach((element) => {
1319
+ element.clear()
1320
+ })
1321
+ this.pools.clear()
1322
+ this.pools = null
1323
+
1324
+ this.makers.clear()
1325
+ this.makers = null
1326
+
1327
+ if (this.layout) {
1328
+ this.layout.onDestroy()
1329
+ }
1330
+ }
1331
+
1332
+ private _frameIdx = 0 // 帧计数
1333
+ private _late_update_visible_data: boolean = false // 当前帧是否需要更新可见节点
1334
+ private _late_recycle_invisible_node = false // 当前帧是否需要回收不可见节点
1335
+ private _late_reload_data: boolean = false // 当前帧是否需要更新列表数据
1336
+ protected update(dt: number): void {
1337
+ this._frameIdx++
1338
+ this.update_reloadVisibleNodesIfNeeds(dt)
1339
+ this.update_recycleInvisibleNodesIfNeeds(dt)
1340
+ this.update_reloadDataIfNeeds(dt)
1341
+ this.update_preloadNodeIfNeeds(dt)
1342
+ }
1343
+
1344
+ /**
1345
+ * 更新可见区域节点逻辑
1346
+ */
1347
+ private update_reloadVisibleNodesIfNeeds(dt: number) {
1348
+ if (this._late_update_visible_data == false) { return }
1349
+ if ((this.frameInterval <= 1) || (this._frameIdx % this.frameInterval == 0)) {
1350
+ this.reloadVisibleElements()
1351
+ return
1352
+ }
1353
+ }
1354
+
1355
+ /**
1356
+ * 回收不可见节点逻辑
1357
+ */
1358
+ private update_recycleInvisibleNodesIfNeeds(dt: number) {
1359
+ if (this._late_recycle_invisible_node == false) { return }
1360
+ if ((this.recycleInterval >= 1) && (this._frameIdx % this.recycleInterval == 0)) {
1361
+ this.recycleInvisibleNodes()
1362
+ return
1363
+ }
1364
+ }
1365
+
1366
+ /**
1367
+ * 预加载节点逻辑
1368
+ */
1369
+ private preloadIdx: number = null
1370
+ private update_preloadNodeIfNeeds(dt: number) {
1371
+ if (this.mode !== _yx_collection_view_list_mode.PRELOAD) { return }
1372
+ if (this.preloadIdx == null) { return }
1373
+ if (this.preloadIdx >= this.layout.attributes.length) { return }
1374
+ if (this.preloadNodesLimitPerFrame <= 0) { return }
1375
+
1376
+ let index = 0
1377
+ let stop = false
1378
+ while (!stop && index < this.preloadNodesLimitPerFrame) {
1379
+
1380
+ const attr = this.layout.attributes[this.preloadIdx]
1381
+ const cacheKey = this._getLayoutAttributesCacheKey(attr)
1382
+ let node: Node = null
1383
+ // 检查节点是否正在显示
1384
+ if (node == null) {
1385
+ node = this.visibleNodesMap.get(cacheKey)
1386
+ }
1387
+ // 检查节点是否加载过了
1388
+ if (node == null) {
1389
+ node = this.preloadNodesMap.get(cacheKey)
1390
+ }
1391
+ // 预加载节点
1392
+ if (node == null) {
1393
+ if (attr.elementCategory === 'Cell') {
1394
+ node = this.cellForItemAt(attr.indexPath, this)
1395
+ }
1396
+ if (attr.elementCategory === 'Supplementary') {
1397
+ node = this.supplementaryForItemAt(attr.indexPath, this, attr.supplementaryKinds)
1398
+ }
1399
+ this.restoreNodeIfNeeds(node)
1400
+ this.applyLayoutAttributes(node, attr)
1401
+ this.visibleNodesMap.set(cacheKey, node)
1402
+ this._late_recycle_invisible_node = true
1403
+ }
1404
+ // 保存节点
1405
+ this.preloadNodesMap.set(cacheKey, node)
1406
+ // 更新预加载索引
1407
+ this.preloadIdx++
1408
+ index++
1409
+
1410
+ if (this.preloadProgress) {
1411
+ this.preloadProgress(this.preloadIdx, this.layout.attributes.length)
1412
+ }
1413
+
1414
+ stop = (this.preloadIdx >= this.layout.attributes.length)
1415
+ }
1416
+ }
1417
+
1418
+ private onScrollBegan() {
1419
+ }
1420
+
1421
+ private onScrolling() {
1422
+ // 在滚动过程中仅仅是标记更新状态,具体更新业务统一到 update 里面处理,但是 layout 设置了实时更新的情况时例外
1423
+ if (this.layout.shouldUpdateAttributesForBoundsChange()) {
1424
+ this.reloadVisibleElements()
1425
+ } else {
1426
+ this._late_update_visible_data = true
1427
+ }
1428
+ if (this.recycleInterval > 0) {
1429
+ this._late_recycle_invisible_node = true
1430
+ }
1431
+ }
1432
+
1433
+ private onScrollTouchUp() {
1434
+ this.recycleInvisibleNodes()
1435
+ }
1436
+
1437
+ private onScrollEnded() {
1438
+ this.recycleInvisibleNodes()
1439
+ this.layout.onScrollEnded(this)
1440
+ }
1441
+ }
1442
+
1443
+ export namespace YXCollectionView {
1444
+ /**
1445
+ * 重定义私有类型
1446
+ */
1447
+ export type ScrollDirection = _yx_collection_view_scroll_direction
1448
+ export type Mode = _yx_collection_view_list_mode
1449
+ }
1450
+
1451
+ /**
1452
+ * *****************************************************************************************
1453
+ * *****************************************************************************************
1454
+ * 把二分查找的规则抽出来封装一下,继承这个类的布局,默认通过二分查找实现查找业务
1455
+ * 这种查找规则对数据量很大的有序列表来说相对高效,具体是否使用还是要根据实际排列需求决定
1456
+ * *****************************************************************************************
1457
+ * *****************************************************************************************
1458
+ *
1459
+ * @deprecated 1.4.0 版本开始,在自定义布局规则的时候暂时不建议继承这个规则了,如何优化查找算法应该全靠开发者根据实际需求自行实现,目前保留这个是为了 flow-layout 使用,后续有更优方案的话可能会删除这部分代码
1460
+ */
1461
+ export abstract class YXBinaryLayout extends YXLayout {
1462
+
1463
+ /**
1464
+ * @bug 如果节点大小差距很大,可能会导致计算屏幕内节点时不准确,出现节点不被正确添加到滚动视图上的问题
1465
+ * @fix 可以通过此属性,追加屏幕显示的节点数量
1466
+ * 设置这个值会在检查是否可见的节点时,尝试检查更多的可能处于屏幕外的节点,具体设置多少要根据实际情况调试,一般如果都是正常大小的节点,不需要考虑这个配置
1467
+ * 设置负值会检查所有的节点
1468
+ */
1469
+ extraVisibleCount: number = 0
1470
+
1471
+ layoutAttributesForElementsInRect(rect: math.Rect, collectionView: YXCollectionView): YXLayoutAttributes[] {
1472
+ if (this.attributes.length <= 100) { return this.attributes } // 少量数据就不查了,直接返回全部
1473
+ if (this.extraVisibleCount < 0) { return this.attributes }
1474
+
1475
+ // 二分先查出大概位置
1476
+ let midIdx = -1
1477
+ let left = 0
1478
+ let right = this.attributes.length - 1
1479
+
1480
+ while (left <= right && right >= 0) {
1481
+ let mid = left + (right - left) / 2
1482
+ mid = Math.floor(mid)
1483
+ let attr = this.attributes[mid]
1484
+ if (rect.intersects(attr.frame)) {
1485
+ midIdx = mid
1486
+ break
1487
+ }
1488
+ if (rect.yMax < attr.frame.yMin || rect.xMax < attr.frame.xMin) {
1489
+ right = mid - 1
1490
+ } else {
1491
+ left = mid + 1
1492
+ }
1493
+ }
1494
+ if (midIdx < 0) {
1495
+ return super.layoutAttributesForElementsInRect(rect, collectionView)
1496
+ }
1497
+
1498
+ let result = []
1499
+ result.push(this.attributes[midIdx])
1500
+
1501
+ // 往前检查
1502
+ let startIdx = midIdx
1503
+ while (startIdx > 0) {
1504
+ let idx = startIdx - 1
1505
+ let attr = this.attributes[idx]
1506
+ if (rect.intersects(attr.frame) == false) {
1507
+ break
1508
+ }
1509
+ result.push(attr)
1510
+ startIdx = idx
1511
+ }
1512
+
1513
+ // 追加检查
1514
+ let extra_left = this.extraVisibleCount
1515
+ while (extra_left > 0) {
1516
+ let idx = startIdx - 1
1517
+ if (idx < 0) { break }
1518
+ let attr = this.attributes[idx]
1519
+ if (rect.intersects(attr.frame)) { result.push(attr) }
1520
+ startIdx = idx
1521
+ extra_left--
1522
+ }
1523
+
1524
+ // 往后检查
1525
+ let endIdx = midIdx
1526
+ while (endIdx < this.attributes.length - 1) {
1527
+ let idx = endIdx + 1
1528
+ let attr = this.attributes[idx]
1529
+ if (rect.intersects(attr.frame) == false) {
1530
+ break
1531
+ }
1532
+ result.push(attr)
1533
+ endIdx = idx
1534
+ }
1535
+
1536
+ // 追加检查
1537
+ let extra_right = this.extraVisibleCount
1538
+ while (extra_right > 0) {
1539
+ let idx = endIdx + 1
1540
+ if (idx >= this.attributes.length) { break }
1541
+ let attr = this.attributes[idx]
1542
+ if (rect.intersects(attr.frame)) { result.push(attr) }
1543
+ endIdx = idx
1544
+ extra_right--
1545
+ }
1546
+
1547
+ return result
1548
+ }
1549
+ }