step 10、ListView.ts 源码
import { _decorator,Component,Prefab,NodePool,ScrollView,Node,instantiate,UITransform, Vec3,sys} from "cc"; const { ccclass, property } = _decorator; @ccclass export class ListView extends Component { @property(Prefab) protected itemTemplate: Prefab = null; /** * 滚动视图 */ @property(ScrollView) protected scrollView:ScrollView = null; /** * 用来约定item 之间的间距 */ @property protected spacing: number = 1; /** * 用来约定超过可见区域的额外显示项数,可以调整滚动时的平滑性. * 比可见元素多缓存3个, 缓存越多,快速滑动越流畅,但同时初始化越慢. */ @property protected spawnCount: number = 2; /** * 设置ScrollView组件的滚动方向,即可自动适配 竖向/横向滚动. */ protected horizontal: boolean = false; protected content: Node = null; protected adapter: AbsAdapter = null; protected readonly _items: NodePool = new NodePool(); // 记录当前填充在树上的索引. 用来快速查找哪些位置缺少item了. protected readonly _filledIds: { [key: number]: number } = {}; // 初始时即计算item的高度.因为布局时要用到. protected _itemHeight: number = 1; protected _itemWidth: number = 1; protected _itemsVisible: number = 1; protected lastStartIndex: number = -1; protected scrollTopNotifyed: boolean = false; protected scrollBottomNotifyed: boolean = false; protected pullDownCallback: () => void = null; protected pullUpCallback: () => void = null; private initialize:boolean = false; public onLoad() { this.init() } public start(): void { } public init() { if(!this.initialize) { this.initView(); this.addEvent(); this.initialize = true; } } private initView(){ if (this.scrollView) { this.content = this.scrollView.content; this.horizontal = this.scrollView.horizontal; const parentTransform = this.content.getParent().getComponent(UITransform); if (this.horizontal) { this.scrollView.vertical = false this.content.getComponent(UITransform).anchorX = 0; this.content.getComponent(UITransform).anchorY = parentTransform.anchorY; this.content.position = new Vec3(0-parentTransform.width *parentTransform.anchorX,0,0); } else { this.scrollView.vertical = true; this.content.getComponent(UITransform).anchorX = parentTransform.anchorX; this.content.getComponent(UITransform).anchorY = 1; this.content.position = new Vec3(0, parentTransform.height * parentTransform.anchorY,0); } } let itemOne = this._items.get() || instantiate(this.itemTemplate); this._items.put(itemOne); this._itemHeight = itemOne.getComponent(UITransform).height || 10; this._itemWidth = itemOne.getComponent(UITransform).width || 10; if (this.horizontal) { this._itemsVisible = Math.ceil(this.content.getParent().getComponent(UITransform).width / this._itemWidth); } else { this._itemsVisible = Math.ceil(this.content.getParent().getComponent(UITransform).height / this._itemHeight); } } public async setAdapter(adapter: AbsAdapter) { if (this.adapter === adapter) { this.notifyUpdate(); return; } this.adapter = adapter; if (this.adapter == null) { console.error("adapter 为空.") return } if (this.itemTemplate == null) { console.error("Listview 未设置待显示的Item模板."); return; } this.notifyUpdate(); } public getItemIndex(height: number): number { return Math.floor(Math.abs(height / ((this._itemHeight + this.spacing)))); } public getPositionInView(item:Node) { let worldPos = item.getParent().getComponent(UITransform).convertToWorldSpaceAR(item.position); let viewPos = this.scrollView.node.getComponent(UITransform).convertToNodeSpaceAR(worldPos); return viewPos; } // 数据变更了需要进行更新UI显示, 可只更新某一条. public notifyUpdate(updateIndex?: number[]) { if (this.adapter == null) { console.log("notifyUpdate","this.adapter is null"); return; } if(this.content ==null){ console.log("notifyUpdate","this.content is null"); return; } if (updateIndex && updateIndex.length > 0) { updateIndex.forEach(i => { if (this._filledIds.hasOwnProperty(i)) { delete this._filledIds[i]; } }) } else { Object.keys(this._filledIds).forEach(key => { delete this._filledIds[key]; }) } this.recycleAll(); this.lastStartIndex = -1; if (this.horizontal) { this.content.getComponent(UITransform).width = this.adapter.getCount() * (this._itemWidth + this.spacing) + this.spacing; } else { this.content.getComponent(UITransform).height = this.adapter.getCount() * (this._itemHeight + this.spacing) + this.spacing; // get total content height } this.scrollView.scrollToTop() } public scrollToTop(anim: boolean = false) { this.scrollView.scrollToTop(anim ? 1 : 0); } public scrollToBottom(anim: boolean = false) { this.scrollView.scrollToBottom(anim ? 1 : 0); } public scrollToLeft(anim: boolean = false) { this.scrollView.scrollToLeft(anim ? 1 : 0); } public scrollToRight(anim: boolean = false) { this.scrollView.scrollToRight(anim ? 1 : 0); } // 下拉事件. public pullDown(callback: () => void, this$: any) { this.pullDownCallback = callback.bind(this$); } // 上拉事件. public pullUp(callback: () => void, this$: any) { this.pullUpCallback = callback.bind(this$); } protected update(dt) { const startIndex = this.checkNeedUpdate(); if (startIndex >= 0) { this.updateView(startIndex); } } // 向某位置添加一个item. protected _layoutVertical(child: Node, posIndex: number) { this.content.addChild(child); // 增加一个tag 属性用来存储child的位置索引. child["_tag"] = posIndex; this._filledIds[posIndex] = posIndex; child.setPosition(0, -child.getComponent(UITransform).height * (0.5 + posIndex) - this.spacing * (posIndex + 1)); } // 向某位置添加一个item. protected _layoutHorizontal(child: Node, posIndex: number) { this.content.addChild(child); // 增加一个tag 属性用来存储child的位置索引. child["_tag"] = posIndex; this._filledIds[posIndex] = posIndex; child.setPosition(child.getComponent(UITransform).width * (child.getComponent(UITransform).anchorX + posIndex) + this.spacing * posIndex, 0); } // 获取可回收item protected getRecycleItems(beginIndex: number, endIndex: number): Node[] { const children = this.content.children; const recycles = [] children.forEach(item => { if (item["_tag"] < beginIndex || item["_tag"] > endIndex) { recycles.push(item); delete this._filledIds[item["_tag"]]; } }) return recycles; } protected recycleAll() { const children = this.content.children; if(children==undefined || children==null) { return; } this.content.removeAllChildren(); children.forEach(item => { this._items.put(item); }) } // 填充View. protected updateView(startIndex) { let itemStartIndex = startIndex; // 比实际元素多3个. let itemEndIndex = itemStartIndex + this._itemsVisible + (this.spawnCount || 2); const totalCount = this.adapter.getCount(); if (itemStartIndex >= totalCount) { return; } if (itemEndIndex > totalCount) { itemEndIndex = totalCount; if (itemStartIndex > 0 && (!this.scrollBottomNotifyed)) { this.notifyScrollToBottom() this.scrollBottomNotifyed = true; } } else { this.scrollBottomNotifyed = false; } // 回收需要回收的元素位置.向上少收一个.向下少收2个. const recyles = this.getRecycleItems(itemStartIndex - (this.spawnCount || 2), itemEndIndex); recyles.forEach(item => { this._items.put(item); }) // 查找需要更新的元素位置. const updates = this.findUpdateIndex(itemStartIndex, itemEndIndex) // 更新位置. for (let index of updates) { let child = this.adapter._getView(this._items.get() || instantiate(this.itemTemplate), index); this.horizontal ? this._layoutHorizontal(child, index) : this._layoutVertical(child, index); } } // 检测是否需要更新UI. protected checkNeedUpdate(): number { if (this.adapter == null) { return -1; } let scroll = this.horizontal ? (-this.content.position.x - this.content.getParent().getComponent(UITransform).width * this.content.getParent().getComponent(UITransform).anchorX) : (this.content.position.y - this.content.getParent().getComponent(UITransform).height * this.content.getParent().getComponent(UITransform).anchorY); let itemStartIndex = Math.floor(scroll / ((this.horizontal ? this._itemWidth : this._itemHeight) + this.spacing)); if (itemStartIndex < 0 && !this.scrollTopNotifyed) { this.notifyScrollToTop(); this.scrollTopNotifyed = true; return itemStartIndex; } // 防止重复触发topNotify.仅当首item不可见后才能再次触发 if (itemStartIndex > 0) { this.scrollTopNotifyed = false; } if (this.lastStartIndex != itemStartIndex) { this.lastStartIndex = itemStartIndex; return itemStartIndex; } return -1; } // 查找需要补充的元素索引. protected findUpdateIndex(itemStartIndex: number, itemEndIndex: number): number[] { const d = []; for (let i = itemStartIndex; i < itemEndIndex; i++) { if (this._filledIds.hasOwnProperty(i)) { continue; } d.push(i); } return d; } protected notifyScrollToTop() { if (!this.adapter || this.adapter.getCount() <= 0) { return; } if (this.pullDownCallback) { this.pullDownCallback(); } } protected notifyScrollToBottom() { if (!this.adapter || this.adapter.getCount() <= 0) { return; } if (this.pullUpCallback) { this.pullUpCallback(); } } protected addEvent() { this.content.on(this.isMobile() ? Node.EventType.TOUCH_END : Node.EventType.MOUSE_UP, () => { this.scrollTopNotifyed = false; this.scrollBottomNotifyed = false; }, this) this.content.on(this.isMobile() ? Node.EventType.TOUCH_CANCEL : Node.EventType.MOUSE_LEAVE, () => { this.scrollTopNotifyed = false; this.scrollBottomNotifyed = false; }, this); } protected isMobile(): boolean { return (sys.isMobile) } } // 数据绑定的辅助适配器 export abstract class AbsAdapter { private dataSet: any[] = []; public setDataSet(data: any[]) { this.dataSet = data; } public getCount(): number { return this.dataSet.length; } public getItem(posIndex: number): any { return this.dataSet[posIndex]; } public _getView(item: Node, posIndex: number): Node { this.updateView(item, posIndex); return item; } public abstract updateView(item: Node, posIndex: number); }