creator-collection-view/list-3x/assets/lib/yx-collection-view.ts

1495 lines
58 KiB
TypeScript
Raw Normal View History

2025-01-08 23:03:52 +08:00
import { _decorator, Component, Enum, Event, EventMouse, EventTouch, instantiate, Mask, math, Node, NodeEventType, NodePool, Prefab, ScrollView, UIOpacity, UITransform } from 'cc';
2024-12-14 15:32:38 +08:00
const { ccclass, property, executionOrder, disallowMultiple, help } = _decorator;
const _vec3Out = new math.Vec3()
const _scroll_view_visible_rect = new math.Rect()
const _recycleInvisibleNodes_realFrame = new math.Rect()
/**
*
*/
enum _yx_collection_view_scroll_direction {
/**
*
*/
HORIZONTAL,
/**
*
*/
VERTICAL,
}
Enum(_yx_collection_view_scroll_direction)
/**
*
*/
enum _yx_collection_view_list_mode {
/**
*
* 优点: 控制总节点数量
* 缺点: 因为有复用逻辑cell Label (NONE)
*/
RECYCLE,
/**
*
* 优点: 避免 cell Label (NONE)
* 缺点: 会实例化所有节点
*/
PRELOAD,
}
Enum(_yx_collection_view_list_mode)
/**
2025-01-08 23:03:52 +08:00
*
2024-12-14 15:32:38 +08:00
*/
2025-01-08 23:03:52 +08:00
@ccclass(`_yx_editor_register_element_info`)
class _yx_editor_register_element_info {
2024-12-14 15:32:38 +08:00
@property({ type: Prefab, tooltip: `cell 节点预制体,必须配置` })
prefab: Prefab = null
@property({ tooltip: `节点重用标识符,必须配置` })
identifier: string = ``
@property({ tooltip: `节点挂载的自定义组件\n如果需要监听 NodePool 的重用/回收事件,确保你的自定义组件已经实现了 YXCollectionViewCell 接口并配置此属性为你的自定义组件名\n如果不需要可以忽略此配置` })
comp: string = ``
}
/**
*
*/
2025-01-08 23:03:52 +08:00
export class YXIndexPath {
private _item: number = 0
private _section: number = 0
2024-12-14 15:32:38 +08:00
public static ZERO: Readonly<YXIndexPath> = new YXIndexPath(0, 0)
/**
*
*/
2025-01-08 23:03:52 +08:00
get section(): number { return this._section }
2024-12-14 15:32:38 +08:00
/**
*
*/
2025-01-08 23:03:52 +08:00
get item(): number { return this._item }
/**
* item
*/
2024-12-14 15:32:38 +08:00
get row(): number { return this.item }
2025-01-08 23:03:52 +08:00
constructor(section: number, item: number) { this._section = section; this._item = item; }
clone(): YXIndexPath { return new YXIndexPath(this.section, this.item) }
equals(other: YXIndexPath): boolean { return (this.section == other.section && this.item == other.item) }
toString(): string { return `${this.section} - ${this.item}` }
2024-12-14 15:32:38 +08:00
}
/**
*
*/
2025-01-08 23:03:52 +08:00
export class YXEdgeInsets {
2024-12-14 15:32:38 +08:00
public static ZERO: Readonly<YXEdgeInsets> = new YXEdgeInsets(0, 0, 0, 0)
2025-01-08 23:03:52 +08:00
top: number;
left: number;
bottom: number;
right: number;
constructor(top: number, left: number, bottom: number, right: number) { this.top = top; this.left = left; this.bottom = bottom; this.right = right; }
clone(): YXEdgeInsets { return new YXEdgeInsets(this.top, this.left, this.bottom, this.right) }
equals(other: YXEdgeInsets): boolean { return (this.top == other.top && this.left == other.left && this.bottom == other.bottom && this.right == other.right) }
set(other: YXEdgeInsets): void { this.top = other.top; this.left = other.left; this.bottom = other.bottom; this.right = other.right; }
toString(): string { return `[ ${this.top}, ${this.left}, ${this.bottom}, ${this.right} ]` }
2024-12-14 15:32:38 +08:00
}
/**
*
2025-01-08 23:03:52 +08:00
* YXCollectionView
2024-12-14 15:32:38 +08:00
*/
2025-01-08 23:03:52 +08:00
class _yx_node_element_comp extends Component {
2024-12-14 15:32:38 +08:00
/**
*
*/
identifier: string
/**
*
*/
attributes: YXLayoutAttributes
}
/**
*
*
* https://github.com/cocos/cocos-engine/blob/v3.8.0/cocos/ui/scroll-view.ts
*/
class _scroll_view extends ScrollView {
protected _yx_scroll_offset_on_touch_start: math.Vec2 = null
_yx_startAttenuatingAutoScrollTargetOffset: (touchMoveVelocity: math.Vec3, startOffset: math.Vec2, originTargetOffset: math.Vec2, originScrollTime: number) => { offset: math.Vec2; time?: number; attenuated?: boolean; } = null
/**
*
*/
protected _onMouseWheel(event: EventMouse, captureListeners?: Node[]): void {
const comp = this.node.getComponent(YXCollectionView)
if (comp == null) { return }
if (comp.scrollEnabled == false) { return }
if (comp.wheelScrollEnabled == false) { return }
super._onMouseWheel(event, captureListeners)
}
/**
*
* @param initialVelocity
*/
protected _startAttenuatingAutoScroll(deltaMove: math.Vec3, initialVelocity: math.Vec3) {
const targetDelta = deltaMove.clone();
targetDelta.normalize();
if (this._content && this.view) {
const contentSize = this._content._uiProps.uiTransformComp!.contentSize;
const scrollViewSize = this.view.contentSize;
const totalMoveWidth = (contentSize.width - scrollViewSize.width);
const totalMoveHeight = (contentSize.height - scrollViewSize.height);
const attenuatedFactorX = this._calculateAttenuatedFactor(totalMoveWidth);
const attenuatedFactorY = this._calculateAttenuatedFactor(totalMoveHeight);
targetDelta.x = targetDelta.x * totalMoveWidth * (1 - this.brake) * attenuatedFactorX;
targetDelta.y = targetDelta.y * totalMoveHeight * attenuatedFactorY * (1 - this.brake);
targetDelta.z = 0;
}
const originalMoveLength = deltaMove.length();
let factor = targetDelta.length() / originalMoveLength;
targetDelta.add(deltaMove);
if (this.brake > 0 && factor > 7) {
factor = Math.sqrt(factor);
const clonedDeltaMove = deltaMove.clone();
clonedDeltaMove.multiplyScalar(factor);
targetDelta.set(clonedDeltaMove);
targetDelta.add(deltaMove);
}
let time = this._calculateAutoScrollTimeByInitialSpeed(initialVelocity.length());
if (this.brake > 0 && factor > 3) {
factor = 3;
time *= factor;
}
if (this.brake === 0 && factor > 1) {
time *= factor;
}
// 当自定义了滚动停留位置时,以自定义的停留位置为准
if (this._yx_startAttenuatingAutoScrollTargetOffset) {
const originTargetOffset = this.getScrollOffset()
originTargetOffset.x += targetDelta.x
originTargetOffset.y += targetDelta.y
let hookValue = this._yx_startAttenuatingAutoScrollTargetOffset(initialVelocity, this._yx_scroll_offset_on_touch_start, originTargetOffset, time)
if (hookValue) {
const hookOffset = hookValue.offset
const hookTime = hookValue.time || time
const hookAttenuated = hookValue.attenuated || true
if (hookOffset) {
this.scrollToOffset(hookOffset, hookTime, hookAttenuated)
return
}
}
}
// 走默认行为
this._startAutoScroll(targetDelta, time, true);
}
protected _onTouchBegan(event: EventTouch, captureListeners?: Node[]): void {
if (this.node.getComponent(YXCollectionView).scrollEnabled == false) { return }
// 记录开始滚动时的偏移量
let offset = this.getScrollOffset()
offset.x = - offset.x
this._yx_scroll_offset_on_touch_start = offset
let nodes: Node[] = [event.target]
if (captureListeners) { nodes = nodes.concat(captureListeners) }
for (let index = 0; index < nodes.length; index++) {
const element = nodes[index];
// 清空滚动节点标记
element[`_yx_scroll_target`] = null
}
super._onTouchBegan(event, captureListeners)
}
protected _onTouchMoved(event: EventTouch, captureListeners?: Node[]): void {
if (this.node.getComponent(YXCollectionView).scrollEnabled == false) { return }
// 处理嵌套冲突,每次只滚动需要滚动的列表
let scrollTarget = this._yxGetScrollTarget(event, captureListeners)
if (this.node === scrollTarget) {
super._onTouchMoved(event, captureListeners)
}
}
protected _hasNestedViewGroup(event: Event, captureListeners?: Node[]): boolean {
// 直接把所有的列表都标记为可滑动,具体滑动哪一个,去 _onTouchMoved 判断
return false
}
protected _stopPropagationIfTargetIsMe(event: Event): void {
if (this._touchMoved) {
event.propagationStopped = true;
return
}
super._stopPropagationIfTargetIsMe(event)
}
/**
*
*/
private _yxGetScrollTarget(event: EventTouch, captureListeners?: Node[]): Node {
// 尝试获取本次已经确定了的滚动节点
let cache = event.target[`_yx_scroll_target`]
if (cache) {
return cache
}
let nodes: Node[] = [event.target]
if (captureListeners) {
nodes = nodes.concat(captureListeners)
}
if (nodes.length == 1) { return nodes[0] } // 无需处理冲突
let touch = event.touch;
let deltaMove = touch.getLocation().subtract(touch.getStartLocation());
let x = Math.abs(deltaMove.x)
let y = Math.abs(deltaMove.y)
let distance = Math.abs(x - y)
if (distance < 5) {
return null // 不足以计算出方向
}
/** @todo 边界检测,滑动到边缘时滑动事件交给其他可滑动列表 */
let result = null
for (let index = 0; index < nodes.length; index++) {
const element = nodes[index];
let scrollComp = element.getComponent(_scroll_view)
if (scrollComp) {
let collectionView = element.getComponent(YXCollectionView)
if (collectionView && collectionView.scrollEnabled == false) { continue } // 不支持滚动
if (result == null) { result = element } // 取第一个滚动组件作为默认响应者
if (scrollComp.horizontal && scrollComp.vertical) { continue } // 全方向滚动暂时不处理
if (!scrollComp.horizontal && !scrollComp.vertical) { continue } // 不支持滚动的也不处理
if (scrollComp.horizontal && x > y) {
result = element
break
}
if (scrollComp.vertical && y > x) {
result = element
break
}
}
}
// 给所有捕获到的节点都保存一份,方便任意一个节点都可以读到
if (result) {
for (let index = 0; index < nodes.length; index++) {
const element = nodes[index];
element[`_yx_scroll_target`] = result
}
}
return result
}
}
/**
*
*/
export class YXLayoutAttributes {
2025-01-08 23:03:52 +08:00
/**
* cell
*/
static layoutAttributesForCell(indexPath: YXIndexPath): YXLayoutAttributes {
let result = new YXLayoutAttributes()
result._indexPath = indexPath
result._elementCategory = 'Cell'
return result
}
/**
* supplementary
* @param kinds supplementaryKinds
*/
static layoutAttributesForSupplementary(indexPath: YXIndexPath, kinds: string): YXLayoutAttributes {
let result = new YXLayoutAttributes()
result._indexPath = indexPath
result._elementCategory = 'Supplementary'
result._supplementaryKinds = kinds
return result
}
/**
* 访
*/
protected constructor() { }
2024-12-14 15:32:38 +08:00
/**
*
*/
get indexPath(): YXIndexPath { return this._indexPath }
private _indexPath: YXIndexPath = null
2025-01-08 23:03:52 +08:00
/**
*
*/
get elementCategory() { return this._elementCategory }
private _elementCategory: 'Cell' | 'Supplementary' = 'Cell'
/**
* Supplementary
*/
get supplementaryKinds() { return this._supplementaryKinds }
private _supplementaryKinds: string = ''
2024-12-14 15:32:38 +08:00
/**
*
* origin size
*/
2025-01-08 23:03:52 +08:00
get frame(): math.Rect { return this._frame }
private _frame: math.Rect = new math.Rect()
2024-12-14 15:32:38 +08:00
/**
*
*
* https://docs.cocos.com/creator/manual/zh/ui-system/components/editor/ui-transform.html?h=uitrans
* 备注: 内部暂时是通过节点的 siblingIndex layout layout @shouldUpdateAttributesZIndex
*/
zIndex: number = 0
/**
*
* 备注: 内部通过 UIOpacity UIOpacity opacity layout layout @shouldUpdateAttributesOpacity
*/
opacity: number = null
/**
* -
*/
scale: math.Vec3 = null
/**
2025-01-08 23:03:52 +08:00
* -
2024-12-14 15:32:38 +08:00
*/
offset: math.Vec3 = null
/**
* -
* 备注: 3D ???
*/
eulerAngles: math.Vec3 = null
}
/**
*
*
*/
export abstract class YXLayout {
constructor() { }
/**
* @required
2025-01-08 23:03:52 +08:00
*
* prepare
2024-12-14 15:32:38 +08:00
*/
contentSize: math.Size = math.Size.ZERO
/**
* @required
2025-01-08 23:03:52 +08:00
*
* prepare
* @todo 使
2024-12-14 15:32:38 +08:00
*/
attributes: YXLayoutAttributes[] = []
/**
* @required
2025-01-08 23:03:52 +08:00
*
* 注意: 必须初始化滚动区域大小并赋值给 contentSize
* 注意: 必须初始化所有的元素布局属性 attributes
2024-12-14 15:32:38 +08:00
* 可选: 根据 collectionView scrollDirection
*/
abstract prepare(collectionView: YXCollectionView): void
/**
* @optional
2025-01-08 23:03:52 +08:00
*
*
*
* @example
* // 比如一个垂直列表希望初始化时从最顶部开始展示数据,那么可以在这个方法里通过 scrollToTop 实现
* initOffset(collectionView: YXCollectionView): void {
* collectionView.scrollView.scrollToTop()
* }
2024-12-14 15:32:38 +08:00
*/
initOffset(collectionView: YXCollectionView) { }
/**
* @optional
*
*
* @param collectionView
* @param touchMoveVelocity
* @param startOffset
* @param originTargetOffset
* @param originScrollDuration
* @returns null null
*
*
* @param offset
* @param time originScrollDuration
* @param attenuated true
*/
targetOffset(collectionView: YXCollectionView, touchMoveVelocity: math.Vec3, startOffset: math.Vec2, originTargetOffset: math.Vec2, originScrollDuration: number): { offset: math.Vec2; time?: number; attenuated?: boolean; } | null { return null }
/**
* @optional
2025-01-08 23:03:52 +08:00
*
2024-12-14 15:32:38 +08:00
*/
onScrollEnded(collectionView: YXCollectionView) { }
/**
2025-01-08 23:03:52 +08:00
* @optional
*
* ( shouldUpdateAttributesForBoundsChange )
*
* @param rect
*
* @returns
*
* O(attributes.length)
2024-12-14 15:32:38 +08:00
*/
layoutAttributesForElementsInRect(rect: math.Rect, collectionView: YXCollectionView): YXLayoutAttributes[] {
2025-01-08 23:03:52 +08:00
return this.attributes
2024-12-14 15:32:38 +08:00
}
layoutAttributesForItemAtIndexPath(indexPath: YXIndexPath, collectionView: YXCollectionView): YXLayoutAttributes {
2025-01-08 23:03:52 +08:00
return this.attributes.find((a) => a.indexPath.equals(indexPath) && a.elementCategory === 'Cell')
}
layoutAttributesForSupplementaryAtIndexPath(indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string): YXLayoutAttributes {
return this.attributes.find((a) => a.indexPath.equals(indexPath) && a.elementCategory === 'Supplementary' && a.supplementaryKinds === kinds)
2024-12-14 15:32:38 +08:00
}
/**
* @optional
2025-01-08 23:03:52 +08:00
* scrollTo
2024-12-14 15:32:38 +08:00
*/
scrollTo(indexPath: YXIndexPath, collectionView: YXCollectionView): math.Vec2 { return null }
/**
* @optional
2025-01-08 23:03:52 +08:00
* @see YXLayoutAttributes.zIndex
2024-12-14 15:32:38 +08:00
*/
shouldUpdateAttributesZIndex(): boolean { return false }
/**
* @optional
2025-01-08 23:03:52 +08:00
* @see YXLayoutAttributes.opacity
2024-12-14 15:32:38 +08:00
*/
shouldUpdateAttributesOpacity(): boolean { return false }
/**
* @optional
*
* @returns true YXCollectionView frameInterval
*/
shouldUpdateAttributesForBoundsChange(): boolean { return false }
/**
2025-01-08 23:03:52 +08:00
* @optional
*
2024-12-14 15:32:38 +08:00
*/
2025-01-08 23:03:52 +08:00
onDestroy() { }
2024-12-14 15:32:38 +08:00
}
/**
* @see NodePool.poolHandlerComp
* NodePool
*/
export interface YXCollectionViewCell extends Component {
unuse(): void;
reuse(args: any): void;
}
/**
*
*/
@ccclass('YXCollectionView')
@disallowMultiple(true)
@executionOrder(-1)
2024-12-14 17:56:37 +08:00
@help(`https://github.com/568071718/creator-collection-view`)
2024-12-14 15:32:38 +08:00
export class YXCollectionView extends Component {
/**
* 访
*/
static ScrollDirection = _yx_collection_view_scroll_direction
static Mode = _yx_collection_view_list_mode
/**
*
*/
get scrollView(): ScrollView {
let result = this.node.getComponent(_scroll_view)
if (result == null) {
result = this.node.addComponent(_scroll_view)
// 配置 scroll view 默认参数
}
if (result.content == null) {
let content = new Node(`com.yx.scroll.content`)
content.parent = result.node
content.layer = content.parent.layer
let transform = content.getComponent(UITransform) || content.addComponent(UITransform)
transform.contentSize = this.node.getComponent(UITransform).contentSize
result.content = content
}
if (this.mask) {
let mask = result.node.getComponent(Mask)
if (mask == null) {
mask = result.node.addComponent(Mask)
mask.type = Mask.Type.GRAPHICS_RECT
}
}
return result
}
private get _scrollView(): _scroll_view { return this.scrollView as _scroll_view }
/**
* mask
*/
@property({ tooltip: `自动给挂载节点添加 mask 组件`, visible: true })
private mask: boolean = true
/**
*
*/
@property({ tooltip: `允许手势滚动` })
scrollEnabled: boolean = true
/**
*
*/
@property({ tooltip: `允许鼠标滑轮滚动` })
wheelScrollEnabled: boolean = false
/**
*
* YXLayout
2025-01-08 23:03:52 +08:00
* 备注: 如果使用的 YXLayout YXLayout 便
2024-12-14 15:32:38 +08:00
*/
@property({ type: _yx_collection_view_scroll_direction, tooltip: `列表滚动方向` })
scrollDirection: YXCollectionView.ScrollDirection = YXCollectionView.ScrollDirection.VERTICAL
/**
*
*/
@property({ type: _yx_collection_view_list_mode, tooltip: `列表单元节点加载模式 (详细区别查看枚举注释)\nRECYCLE: 根据列表显示范围加载需要的节点,同类型的节点会进行复用\nPRELOAD: 会实例化所有节点,并非真正的虚拟列表,仅仅是把显示范围外的节点透明了,如果列表数据量很大仍然会卡` })
mode: YXCollectionView.Mode = YXCollectionView.Mode.RECYCLE
/**
*
*/
@property({
tooltip: `预加载模式下每帧加载多少个节点`,
visible: function (this) {
return (this.mode == _yx_collection_view_list_mode.PRELOAD)
}
})
preloadNodesLimitPerFrame: number = 2
/**
*
*/
preloadProgress: (current: number, total: number) => void = null
/**
* 1
*/
@property({ tooltip: `每多少帧刷新一次可见节点1 表示每帧都刷` })
frameInterval: number = 1
/**
* 10
* @bug scroll view cancel scroll view ()
* @fix 0 `touch-up` `scroll-ended`
*/
@property({ tooltip: `滚动过程中每多少帧回收一次不可见节点1表示每帧都回收0表示不在滚动过程中回收不可见节点` })
recycleInterval: number = 1
/**
*
*/
2025-01-08 23:03:52 +08:00
@property({ type: [_yx_editor_register_element_info], visible: true, displayName: `Register Cells`, tooltip: `配置此列表内需要用到的 cell 节点类型` })
private registerCellForEditor: _yx_editor_register_element_info[] = []
@property({ type: [_yx_editor_register_element_info], visible: true, displayName: `Register Supplementarys`, tooltip: `配置此列表内需要用到的 Supplementary 节点类型` })
private registerSupplementaryForEditor: _yx_editor_register_element_info[] = []
2024-12-14 15:32:38 +08:00
/**
* cell
2025-01-08 23:03:52 +08:00
* cell identifier
* @param identifier cell dequeueReusableCell cell
2024-12-14 15:32:38 +08:00
* @param maker
2025-01-08 23:03:52 +08:00
* @param poolComp () NodePool
*/
registerCell(identifier: string, maker: () => Node, poolComp: (new (...args: any[]) => YXCollectionViewCell) | string | null = null) {
let elementCategory: typeof YXLayoutAttributes.prototype.elementCategory = 'Cell'
identifier = elementCategory + identifier
let pool = new NodePool(poolComp)
this.pools.set(identifier, pool)
this.makers.set(identifier, maker)
}
/**
* supplementary registerCell
2024-12-14 15:32:38 +08:00
*/
2025-01-08 23:03:52 +08:00
registerSupplementary(identifier: string, maker: () => Node, poolComp: (new (...args: any[]) => YXCollectionViewCell) | string | null = null) {
let elementCategory: typeof YXLayoutAttributes.prototype.elementCategory = 'Supplementary'
identifier = elementCategory + identifier
2024-12-14 15:32:38 +08:00
let pool = new NodePool(poolComp)
this.pools.set(identifier, pool)
this.makers.set(identifier, maker)
}
/**
*
*/
private pools: Map<string, NodePool> = new Map()
/**
*
*/
private makers: Map<string, () => Node> = new Map()
/**
* cell
* @param identifier
*/
dequeueReusableCell(identifier: string): Node {
2025-01-08 23:03:52 +08:00
return this._dequeueReusableElement(identifier, 'Cell')
}
/**
* supplementary
* @param identifier
*/
dequeueReusableSupplementary(identifier: string): Node {
return this._dequeueReusableElement(identifier, 'Supplementary')
}
private _dequeueReusableElement(identifier: string, elementCategory: typeof YXLayoutAttributes.prototype.elementCategory) {
identifier = elementCategory + identifier
2024-12-14 15:32:38 +08:00
let pool = this.pools.get(identifier)
if (pool == null) {
2025-01-08 23:03:52 +08:00
throw new Error(`YXCollectionView: dequeueReusable${elementCategory} 错误,未注册的 identifier`);
2024-12-14 15:32:38 +08:00
}
let result: Node = null
// 尝试从重用池获取
if (result == null) {
result = pool.get()
}
// 重新生成一个
if (result == null) {
const maker = this.makers.get(identifier)
result = maker()
2025-01-08 23:03:52 +08:00
let cell = result.getComponent(_yx_node_element_comp) || result.addComponent(_yx_node_element_comp)
2024-12-14 15:32:38 +08:00
cell.identifier = identifier
2025-01-08 23:03:52 +08:00
result.on(NodeEventType.TOUCH_END, this.onTouchElement, this)
2024-12-14 15:32:38 +08:00
}
return result
}
/**
* 1
*
*/
numberOfSections: number | ((collectionView: YXCollectionView) => number) = 1
getNumberOfSections(): number {
if (this.numberOfSections instanceof Function) { return this.numberOfSections(this) }
return this.numberOfSections
}
/**
*
*/
numberOfItems: number | ((section: number, collectionView: YXCollectionView) => number) = 0
getNumberOfItems(section: number): number {
if (this.numberOfItems instanceof Function) { return this.numberOfItems(section, this) }
return this.numberOfItems
}
/**
* UI
* indexPath Node dequeueReusableCell Node
*
* @example
* yourList.cellForItemAt = (indexPath ,collectionView) => {
* let cell = collectionView.dequeueReusableCell(`your identifier`)
* let comp = cell.getComponent(YourCellComp)
* comp.label.string = `${indexPath}`
* return cell
* }
*
* @returns 注意: 不要在这个方法里创建新的节点对象 Node dequeueReusableCell Node
*/
cellForItemAt: (indexPath: YXIndexPath, collectionView: YXCollectionView) => Node = null
/**
2025-01-08 23:03:52 +08:00
* cellForItemAt dequeueReusableSupplementary Node
* @param kinds 使 layout
*/
supplementaryForItemAt: (indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string) => Node = null
/**
* cell
2024-12-14 15:32:38 +08:00
*
*/
2025-01-08 23:03:52 +08:00
onCellDisplay: (node: Node, indexPath: YXIndexPath, collectionView: YXCollectionView) => void = null
onCellEndDisplay: (node: Node, indexPath: YXIndexPath, collectionView: YXCollectionView) => void = null
/**
* supplementary
*/
onSupplementaryDisplay: (node: Node, indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string) => void = null
onSupplementaryEndDisplay: (node: Node, indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string) => void = null
/**
* cell
*/
onTouchCellAt: (indexPath: YXIndexPath, collectionView: YXCollectionView) => void = null
2024-12-14 15:32:38 +08:00
/**
2025-01-08 23:03:52 +08:00
* supplementary
2024-12-14 15:32:38 +08:00
*/
2025-01-08 23:03:52 +08:00
onTouchSupplementaryAt: (indexPath: YXIndexPath, collectionView: YXCollectionView, kinds: string) => void = null
2024-12-14 15:32:38 +08:00
/**
2025-01-08 23:03:52 +08:00
*
2024-12-14 15:32:38 +08:00
*/
2025-01-08 23:03:52 +08:00
private onTouchElement(ev: EventTouch) {
const node = ev.target
if (node instanceof Node == false) { return }
const cell = node.getComponent(_yx_node_element_comp)
if (cell == null) { return }
const attr = cell.attributes
if (attr == null) { return }
if (attr.elementCategory === 'Cell') {
if (this.onTouchCellAt) {
this.onTouchCellAt(attr.indexPath, this)
return
}
return
}
if (attr.elementCategory === 'Supplementary') {
if (this.onTouchSupplementaryAt) {
this.onTouchSupplementaryAt(attr.indexPath, this, attr.supplementaryKinds)
}
return
2024-12-14 15:32:38 +08:00
}
}
/**
*
*/
layout: YXLayout = null
/**
*
* Map key = indexpath.string value =
*/
private visibleNodesMap: Map<string, Node> = new Map()
/**
*
* preload
*/
private preloadNodesMap: Map<string, Node> = new Map()
/**
2025-01-08 23:03:52 +08:00
* key
2024-12-14 15:32:38 +08:00
*/
2025-01-08 23:03:52 +08:00
private _getLayoutAttributesCacheKey(element: YXLayoutAttributes): string {
return this._getVisibleCacheKey(element.indexPath, element.elementCategory, element.supplementaryKinds)
}
private _getVisibleCacheKey(indexPath: YXIndexPath, elementCategory: typeof YXLayoutAttributes.prototype.elementCategory, kinds: string = '') {
return `${indexPath}${elementCategory}${kinds}`
}
/**
*
*/
getVisibleRect(): math.Rect {
2024-12-14 15:32:38 +08:00
const visibleRect = _scroll_view_visible_rect
visibleRect.origin = this.scrollView.getScrollOffset()
visibleRect.x = - visibleRect.x
visibleRect.size = this.scrollView.view.contentSize
return visibleRect
}
/**
2025-01-08 23:03:52 +08:00
* cell
2024-12-14 15:32:38 +08:00
*/
2025-01-08 23:03:52 +08:00
getVisibleCellNode(indexPath: YXIndexPath): Node {
const cacheKey = this._getVisibleCacheKey(indexPath, 'Cell')
return this.visibleNodesMap.get(cacheKey)
}
/**
* supplementary
*/
getVisibleSupplementaryNode(indexPath: YXIndexPath, kinds: string): Node {
const cacheKey = this._getVisibleCacheKey(indexPath, 'Supplementary', kinds)
return this.visibleNodesMap.get(cacheKey)
2024-12-14 15:32:38 +08:00
}
2025-01-08 23:03:52 +08:00
/**
* cell
*/
getVisibleCellNodes(): Node[] {
let result: Node[] = []
2024-12-14 15:32:38 +08:00
this.visibleNodesMap.forEach((value) => {
2025-01-08 23:03:52 +08:00
const comp = value.getComponent(_yx_node_element_comp)
if (comp.attributes.elementCategory === 'Cell') {
result.push(value)
}
2024-12-14 15:32:38 +08:00
})
return result
}
/**
2025-01-08 23:03:52 +08:00
* supplementary
* @param kinds
2024-12-14 15:32:38 +08:00
*/
2025-01-08 23:03:52 +08:00
getVisibleSupplementaryNodes(kinds: string = null): Node[] {
let result: Node[] = []
this.visibleNodesMap.forEach((value) => {
const comp = value.getComponent(_yx_node_element_comp)
if (comp.attributes.elementCategory === 'Supplementary') {
if (kinds === null || comp.attributes.supplementaryKinds === kinds) {
result.push(value)
}
}
})
return result
2024-12-14 15:32:38 +08:00
}
/**
2025-01-08 23:03:52 +08:00
*
2024-12-14 15:32:38 +08:00
*/
2025-01-08 23:03:52 +08:00
getElementAttributes(node: Node): YXLayoutAttributes {
const comp = node.getComponent(_yx_node_element_comp)
return comp ? comp.attributes : null
2024-12-14 15:32:38 +08:00
}
/**
*
*/
reloadData() {
if (this.node.activeInHierarchy && this.node.parent) {
this._reloadData()
2025-01-08 23:03:52 +08:00
return
2024-12-14 15:32:38 +08:00
}
2025-01-08 23:03:52 +08:00
this._late_reload_data = true
2024-12-14 15:32:38 +08:00
}
private update_reloadDataIfNeeds(dt: number) {
if (this._late_reload_data == false) { return }
this._reloadData()
}
private _reloadData() {
this._late_reload_data = false
// 校验 layout 参数
if (this.layout == null) {
throw new Error("YXCollectionView: 参数错误,请正确配置 layout 以确定布局方案");
}
// 立即停止当前滚动,准备刷新
this.scrollView.stopAutoScroll()
// 池子先清一下,可能会累积很多暂时用不到的节点
2025-01-08 23:03:52 +08:00
this.pools.forEach((element) => { element.clear() })
2024-12-14 15:32:38 +08:00
// 回收模式下,移除掉正在显示的节点并加到池子里 (不需要销毁)
if (this.mode == _yx_collection_view_list_mode.RECYCLE) {
this.visibleNodesMap.forEach((value, key) => {
2025-01-08 23:03:52 +08:00
const cell = value.getComponent(_yx_node_element_comp)
2024-12-14 15:32:38 +08:00
this.pools.get(cell.identifier).put(value)
this.visibleNodesMap.delete(key) // 从可见节点里删除
2025-01-08 23:03:52 +08:00
if (cell.attributes.elementCategory === 'Cell') {
if (this.onCellEndDisplay) {
this.onCellEndDisplay(cell.node, cell.attributes.indexPath, this)
}
}
if (cell.attributes.elementCategory === 'Supplementary') {
if (this.onSupplementaryEndDisplay) {
this.onSupplementaryEndDisplay(cell.node, cell.attributes.indexPath, this, cell.attributes.supplementaryKinds)
}
2024-12-14 15:32:38 +08:00
}
})
this.visibleNodesMap.clear()
}
// 预加载模式下,需要清空当前显示的所有节点以及已经预加载过的所有节点 (全部销毁)
if (this.mode == _yx_collection_view_list_mode.PRELOAD) {
// 销毁当前所有正在显示的节点
this.visibleNodesMap.forEach((value, key) => {
if (value) {
value.removeFromParent()
value.destroy()
}
})
this.visibleNodesMap.clear()
// 销毁所有预加载的节点
this.preloadNodesMap.forEach((value, key) => {
if (value) {
value.removeFromParent()
value.destroy()
}
})
this.preloadNodesMap.clear()
// 从第一个开始预加载节点
this.preloadIdx = 0
}
// 记录一下当前的偏移量,保证数据更新之后位置也不会太偏
let offset = this.scrollView.getScrollOffset()
offset.x = -offset.x
// 重新计算一遍布局属性
this.layout.prepare(this)
// 更新 content size
let contentTransform = this.scrollView.content.getComponent(UITransform) || this.scrollView.content.addComponent(UITransform)
contentTransform.contentSize = this.layout.contentSize
// 默认偏移量 或者 恢复偏移量
if (this.reloadDataCounter <= 0) {
this.layout.initOffset(this)
} else {
let maxOffset = this.scrollView.getMaxScrollOffset()
math.Vec2.min(offset, offset, maxOffset)
this.scrollView.scrollToOffset(offset)
}
// 更新可见 cell 节点
this.markForUpdateVisibleData(true)
this.reloadDataCounter++
}
/**
* @reloadData
*/
private reloadDataCounter: number = 0
/**
*
*/
2025-01-08 23:03:52 +08:00
private reloadVisibleElements(visibleRect: math.Rect = null) {
this._late_update_visible_data = false
if (visibleRect == null) { visibleRect = this.getVisibleRect() }
2024-12-14 15:32:38 +08:00
// 根据可见区域,找出对应的布局属性
2025-01-08 23:03:52 +08:00
let layoutAttributes = this.layout.layoutAttributesForElementsInRect(visibleRect, this)
2024-12-14 15:32:38 +08:00
// 按 zIndex 排序
let shouldUpdateAttributesZIndex = this.layout.shouldUpdateAttributesZIndex()
if (shouldUpdateAttributesZIndex) {
if (layoutAttributes == null || layoutAttributes == this.layout.attributes) {
layoutAttributes = this.layout.attributes.slice()
}
layoutAttributes.sort((a, b) => a.zIndex - b.zIndex)
}
2025-01-08 23:03:52 +08:00
let shouldUpdateAttributesForBoundsChange = this.layout.shouldUpdateAttributesForBoundsChange()
2024-12-14 15:32:38 +08:00
// 添加需要显示的节点
for (let index = 0; index < layoutAttributes.length; index++) {
const element = layoutAttributes[index];
2025-01-08 23:03:52 +08:00
if (visibleRect.intersects(element.frame) == false) { continue }
const cacheKey = this._getLayoutAttributesCacheKey(element)
let elementNode = null
2024-12-14 15:32:38 +08:00
// 检查是否已经预加载过了
2025-01-08 23:03:52 +08:00
if (elementNode == null) {
elementNode = this.preloadNodesMap.get(cacheKey)
2024-12-14 15:32:38 +08:00
}
// 检查节点是否正在显示了
2025-01-08 23:03:52 +08:00
if (elementNode == null) {
elementNode = this.visibleNodesMap.get(cacheKey)
2024-12-14 15:32:38 +08:00
}
// 尝试通过注册标识符从节点池获取节点
2025-01-08 23:03:52 +08:00
if (elementNode == null) {
if (element.elementCategory === 'Cell') {
elementNode = this.cellForItemAt(element.indexPath, this)
}
if (element.elementCategory === 'Supplementary') {
elementNode = this.supplementaryForItemAt(element.indexPath, this, element.supplementaryKinds)
}
2024-12-14 15:32:38 +08:00
}
// 无法正确获取节点,报错
2025-01-08 23:03:52 +08:00
if (elementNode == null) {
if (element.elementCategory === 'Cell') {
throw new Error("需要实现 cellForItemAt 方法并确保正确的返回了节点");
}
if (element.elementCategory === 'Supplementary') {
throw new Error("需要实现 supplementaryForItemAt 方法并确保正确的返回了节点");
}
2024-12-14 15:32:38 +08:00
}
// 恢复节点状态
2025-01-08 23:03:52 +08:00
const restoreStatus = this.restoreNodeIfNeeds(elementNode)
2024-12-14 15:32:38 +08:00
// 更新节点变化
2025-01-08 23:03:52 +08:00
if (restoreStatus == 1 || shouldUpdateAttributesForBoundsChange) {
this.applyLayoutAttributes(elementNode, element)
2024-12-14 15:32:38 +08:00
}
// 调整节点层级
if (shouldUpdateAttributesZIndex) {
2025-01-08 23:03:52 +08:00
elementNode.setSiblingIndex(-1)
2024-12-14 15:32:38 +08:00
}
// 标记此节点正在显示
2025-01-08 23:03:52 +08:00
this.visibleNodesMap.set(cacheKey, elementNode)
2024-12-14 15:32:38 +08:00
2025-01-08 23:03:52 +08:00
// 通知 display
2024-12-14 15:32:38 +08:00
if (restoreStatus == 1) {
2025-01-08 23:03:52 +08:00
if (element.elementCategory === 'Cell') {
if (this.onCellDisplay) {
this.onCellDisplay(elementNode, element.indexPath, this)
}
}
if (element.elementCategory === 'Supplementary') {
if (this.onSupplementaryDisplay) {
this.onSupplementaryDisplay(elementNode, element.indexPath, this, element.supplementaryKinds)
}
2024-12-14 15:32:38 +08:00
}
}
}
layoutAttributes = []
}
/**
* 使
*/
2025-01-08 23:03:52 +08:00
private restoreNodeIfNeeds(node: Node) {
2024-12-14 15:32:38 +08:00
// 是否触发了恢复行为0表示节点已经可见了 1表示触发了恢复行为节点从不可见变为了可见
let restoreStatus = 0
// 不管哪种模式,父节点检查都是必须的,只有正确的添加了才能确保正常可见
if (node.parent != this.scrollView.content) {
node.parent = this.scrollView.content
restoreStatus = 1
}
// 如果启用了预加载模式,给节点挂上 UIOpacity 组件,未启用则不管
let opacityComp = node.getComponent(UIOpacity)
if (this.mode == _yx_collection_view_list_mode.PRELOAD) {
if (opacityComp == null) {
opacityComp = node.addComponent(UIOpacity)
}
}
if (opacityComp) {
if (opacityComp.opacity !== 255) {
opacityComp.opacity = 255
restoreStatus = 1
}
}
return restoreStatus
}
/**
*
*/
private recycleInvisibleNodes(visibleRect: math.Rect = null) {
2025-01-08 23:03:52 +08:00
this._late_recycle_invisible_node = false
if (visibleRect == null) { visibleRect = this.getVisibleRect() }
2024-12-14 15:32:38 +08:00
const _realFrame = _recycleInvisibleNodes_realFrame
const _contentSize = this.scrollView.content.getComponent(UITransform).contentSize
this.visibleNodesMap.forEach((value, key) => {
2025-01-08 23:03:52 +08:00
const cell = value.getComponent(_yx_node_element_comp)
2024-12-14 15:32:38 +08:00
/**
* @version 1.0.2
*
* boundingBox
* position origin
*/
let boundingBox = value.getComponent(UITransform).getBoundingBox()
_realFrame.size = boundingBox.size
_realFrame.x = (_contentSize.width - _realFrame.width) * 0.5 + value.position.x
_realFrame.y = (_contentSize.height - _realFrame.height) * 0.5 - value.position.y
if (visibleRect.intersects(_realFrame) == false) {
if (this.mode == _yx_collection_view_list_mode.PRELOAD) {
value.getComponent(UIOpacity).opacity = 0
2025-01-08 23:03:52 +08:00
this.preloadNodesMap.set(key, value)
2024-12-14 15:32:38 +08:00
} else {
this.pools.get(cell.identifier).put(value)
}
this.visibleNodesMap.delete(key) // 从可见节点里删除
2025-01-08 23:03:52 +08:00
if (cell.attributes.elementCategory === 'Cell') {
if (this.onCellEndDisplay) {
this.onCellEndDisplay(cell.node, cell.attributes.indexPath, this)
}
}
if (cell.attributes.elementCategory === 'Supplementary') {
if (this.onSupplementaryEndDisplay) {
this.onSupplementaryEndDisplay(cell.node, cell.attributes.indexPath, this, cell.attributes.supplementaryKinds)
}
2024-12-14 15:32:38 +08:00
}
}
})
}
/**
* /
*/
private applyLayoutAttributes(node: Node, attributes: YXLayoutAttributes) {
2025-01-08 23:03:52 +08:00
let cell = node.getComponent(_yx_node_element_comp)
2024-12-14 15:32:38 +08:00
cell.attributes = attributes
let transform = node.getComponent(UITransform) || node.addComponent(UITransform)
transform.setContentSize(attributes.frame.size)
_vec3Out.x = - (this.layout.contentSize.width - attributes.frame.width) * 0.5 + attributes.frame.x
_vec3Out.y = + (this.layout.contentSize.height - attributes.frame.height) * 0.5 - attributes.frame.y
_vec3Out.z = node.position.z
if (attributes.offset) {
math.Vec3.add(_vec3Out, _vec3Out, attributes.offset)
}
node.position = _vec3Out
if (attributes.scale) {
node.scale = attributes.scale
}
if (attributes.eulerAngles) {
node.eulerAngles = attributes.eulerAngles
}
if (this.layout.shouldUpdateAttributesOpacity() && attributes.opacity) {
let opacity = node.getComponent(UIOpacity) || node.addComponent(UIOpacity)
opacity.opacity = attributes.opacity
}
}
/**
2025-01-08 23:03:52 +08:00
*
* @param force true: ; false:
*/
markForUpdateVisibleData(force: boolean = false) {
if (force) {
const visibleRect = this.getVisibleRect()
this.reloadVisibleElements(visibleRect)
this.recycleInvisibleNodes(visibleRect)
return
}
this._late_update_visible_data = true
this._late_recycle_invisible_node = true
}
/**
*
* @todo .scrollView.scrollToOffset()
2024-12-14 15:32:38 +08:00
*/
scrollTo(indexPath: YXIndexPath, timeInSecond: number = 0, attenuated: boolean = true) {
let toOffSet: math.Vec2 = this.layout.scrollTo(indexPath, this)
if (toOffSet == null) {
toOffSet = this.layout.layoutAttributesForItemAtIndexPath(indexPath, this)?.frame.origin
}
if (toOffSet) {
this.scrollView.stopAutoScroll()
this.scrollView.scrollToOffset(toOffSet, timeInSecond, attenuated)
this.markForUpdateVisibleData()
}
}
/**
*
*/
protected onLoad(): void {
for (let index = 0; index < this.registerCellForEditor.length; index++) {
const element = this.registerCellForEditor[index];
2025-01-08 23:03:52 +08:00
this.registerCell(element.identifier, () => instantiate(element.prefab), element.comp)
}
for (let index = 0; index < this.registerSupplementaryForEditor.length; index++) {
const element = this.registerSupplementaryForEditor[index];
this.registerSupplementary(element.identifier, () => instantiate(element.prefab), element.comp)
2024-12-14 15:32:38 +08:00
}
this.node.on(ScrollView.EventType.SCROLL_BEGAN, this.onScrollBegan, this)
this.node.on(ScrollView.EventType.SCROLLING, this.onScrolling, this)
this.node.on(ScrollView.EventType.TOUCH_UP, this.onScrollTouchUp, this)
this.node.on(ScrollView.EventType.SCROLL_ENDED, this.onScrollEnded, this)
this._scrollView._yx_startAttenuatingAutoScrollTargetOffset = (touchMoveVelocity, startOffset, originTargetOffset, originScrollTime) => {
return this.layout.targetOffset(this, touchMoveVelocity, startOffset, originTargetOffset, originScrollTime)
}
}
protected onDestroy(): void {
this.node.off(ScrollView.EventType.SCROLL_BEGAN, this.onScrollBegan, this)
this.node.off(ScrollView.EventType.SCROLLING, this.onScrolling, this)
this.node.off(ScrollView.EventType.TOUCH_UP, this.onScrollTouchUp, this)
this.node.off(ScrollView.EventType.SCROLL_ENDED, this.onScrollEnded, this)
this._scrollView._yx_startAttenuatingAutoScrollTargetOffset = null
// 销毁当前所有正在显示的节点
this.visibleNodesMap.forEach((value, key) => {
if (value) {
value.removeFromParent()
value.destroy()
}
})
this.visibleNodesMap.clear()
this.visibleNodesMap = null
// 销毁所有预加载的节点
this.preloadNodesMap.forEach((value, key) => {
if (value) {
value.removeFromParent()
value.destroy()
}
})
this.preloadNodesMap.clear()
this.preloadNodesMap = null
// 清空池子
this.pools.forEach((element) => {
element.clear()
})
this.pools.clear()
this.pools = null
this.makers.clear()
this.makers = null
if (this.layout) {
2025-01-08 23:03:52 +08:00
this.layout.onDestroy()
2024-12-14 15:32:38 +08:00
}
}
2025-01-08 23:03:52 +08:00
private _frameIdx = 0 // 帧计数
private _late_update_visible_data: boolean = false // 当前帧是否需要更新可见节点
private _late_recycle_invisible_node = false // 当前帧是否需要回收不可见节点
private _late_reload_data: boolean = false // 当前帧是否需要更新列表数据
2024-12-14 15:32:38 +08:00
protected update(dt: number): void {
this._frameIdx++
2025-01-08 23:03:52 +08:00
this.update_reloadVisibleNodesIfNeeds(dt)
2024-12-14 15:32:38 +08:00
this.update_recycleInvisibleNodesIfNeeds(dt)
this.update_reloadDataIfNeeds(dt)
this.update_preloadNodeIfNeeds(dt)
}
/**
*
*/
2025-01-08 23:03:52 +08:00
private update_reloadVisibleNodesIfNeeds(dt: number) {
if (this._late_update_visible_data == false) { return }
2024-12-14 15:32:38 +08:00
if ((this.frameInterval <= 1) || (this._frameIdx % this.frameInterval == 0)) {
2025-01-08 23:03:52 +08:00
this.reloadVisibleElements()
2024-12-14 15:32:38 +08:00
return
}
}
/**
*
*/
private update_recycleInvisibleNodesIfNeeds(dt: number) {
2025-01-08 23:03:52 +08:00
if (this._late_recycle_invisible_node == false) { return }
2024-12-14 15:32:38 +08:00
if ((this.recycleInterval >= 1) && (this._frameIdx % this.recycleInterval == 0)) {
2025-01-08 23:03:52 +08:00
this.recycleInvisibleNodes()
2024-12-14 15:32:38 +08:00
return
}
}
/**
*
*/
private preloadIdx: number = null
private update_preloadNodeIfNeeds(dt: number) {
2025-01-08 23:03:52 +08:00
if (this.mode !== _yx_collection_view_list_mode.PRELOAD) { return }
if (this.preloadIdx == null) { return }
if (this.preloadIdx >= this.layout.attributes.length) { return }
if (this.preloadNodesLimitPerFrame <= 0) { return }
2024-12-14 15:32:38 +08:00
let index = 0
let stop = false
while (!stop && index < this.preloadNodesLimitPerFrame) {
const attr = this.layout.attributes[this.preloadIdx]
2025-01-08 23:03:52 +08:00
const cacheKey = this._getLayoutAttributesCacheKey(attr)
2024-12-14 15:32:38 +08:00
let node: Node = null
// 检查节点是否正在显示
if (node == null) {
2025-01-08 23:03:52 +08:00
node = this.visibleNodesMap.get(cacheKey)
2024-12-14 15:32:38 +08:00
}
// 检查节点是否加载过了
if (node == null) {
2025-01-08 23:03:52 +08:00
node = this.preloadNodesMap.get(cacheKey)
2024-12-14 15:32:38 +08:00
}
// 预加载节点
if (node == null) {
2025-01-08 23:03:52 +08:00
if (attr.elementCategory === 'Cell') {
node = this.cellForItemAt(attr.indexPath, this)
}
if (attr.elementCategory === 'Supplementary') {
node = this.supplementaryForItemAt(attr.indexPath, this, attr.supplementaryKinds)
}
this.restoreNodeIfNeeds(node)
2024-12-14 15:32:38 +08:00
this.applyLayoutAttributes(node, attr)
2025-01-08 23:03:52 +08:00
this.visibleNodesMap.set(cacheKey, node)
2024-12-14 15:32:38 +08:00
this._late_recycle_invisible_node = true
}
// 保存节点
2025-01-08 23:03:52 +08:00
this.preloadNodesMap.set(cacheKey, node)
2024-12-14 15:32:38 +08:00
// 更新预加载索引
this.preloadIdx++
index++
if (this.preloadProgress) {
this.preloadProgress(this.preloadIdx, this.layout.attributes.length)
}
stop = (this.preloadIdx >= this.layout.attributes.length)
}
}
private onScrollBegan() {
}
private onScrolling() {
2025-01-08 23:03:52 +08:00
// 在滚动过程中仅仅是标记更新状态,具体更新业务统一到 update 里面处理,但是 layout 设置了实时更新的情况时例外
2024-12-14 15:32:38 +08:00
if (this.layout.shouldUpdateAttributesForBoundsChange()) {
2025-01-08 23:03:52 +08:00
this.reloadVisibleElements()
} else {
this._late_update_visible_data = true
}
if (this.recycleInterval > 0) {
this._late_recycle_invisible_node = true
2024-12-14 15:32:38 +08:00
}
}
private onScrollTouchUp() {
this.recycleInvisibleNodes()
}
private onScrollEnded() {
this.recycleInvisibleNodes()
this.layout.onScrollEnded(this)
}
}
export namespace YXCollectionView {
/**
*
*/
export type ScrollDirection = _yx_collection_view_scroll_direction
export type Mode = _yx_collection_view_list_mode
}
2025-01-08 23:03:52 +08:00
/**
* *****************************************************************************************
* *****************************************************************************************
*
* 使
* *****************************************************************************************
* *****************************************************************************************
*
* @deprecated 1.4.0 flow-layout 使
*/
export abstract class YXBinaryLayout extends YXLayout {
/**
* @bug
* @fix
*
*
*/
extraVisibleCount: number = 0
layoutAttributesForElementsInRect(rect: math.Rect, collectionView: YXCollectionView): YXLayoutAttributes[] {
if (this.attributes.length <= 100) { return this.attributes } // 少量数据就不查了,直接返回全部
if (this.extraVisibleCount < 0) { return this.attributes }
// 二分先查出大概位置
let midIdx = -1
let left = 0
let right = this.attributes.length - 1
while (left <= right && right >= 0) {
let mid = left + (right - left) / 2
mid = Math.floor(mid)
let attr = this.attributes[mid]
if (rect.intersects(attr.frame)) {
midIdx = mid
break
}
if (rect.yMax < attr.frame.yMin || rect.xMax < attr.frame.xMin) {
right = mid - 1
} else {
left = mid + 1
}
}
if (midIdx < 0) {
return super.layoutAttributesForElementsInRect(rect, collectionView)
}
let result = []
result.push(this.attributes[midIdx])
// 往前检查
let startIdx = midIdx
while (startIdx > 0) {
let idx = startIdx - 1
let attr = this.attributes[idx]
if (rect.intersects(attr.frame) == false) {
break
}
result.push(attr)
startIdx = idx
}
// 追加检查
let extra_left = this.extraVisibleCount
while (extra_left > 0) {
let idx = startIdx - 1
if (idx < 0) { break }
let attr = this.attributes[idx]
if (rect.intersects(attr.frame)) { result.push(attr) }
startIdx = idx
extra_left--
}
// 往后检查
let endIdx = midIdx
while (endIdx < this.attributes.length - 1) {
let idx = endIdx + 1
let attr = this.attributes[idx]
if (rect.intersects(attr.frame) == false) {
break
}
result.push(attr)
endIdx = idx
}
// 追加检查
let extra_right = this.extraVisibleCount
while (extra_right > 0) {
let idx = endIdx + 1
if (idx >= this.attributes.length) { break }
let attr = this.attributes[idx]
if (rect.intersects(attr.frame)) { result.push(attr) }
endIdx = idx
extra_right--
}
return result
}
}