Cocos Creator3.8 项目实战(七)Listview 控件的实现和使用(2)

简介: Cocos Creator3.8 项目实战(七)Listview 控件的实现和使用

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);
}
相关文章
Cocos Creator3.8 项目实战(五)背景无限滚屏效果如何实现
Cocos Creator3.8 项目实战(五)背景无限滚屏效果如何实现
656 0
|
XML 存储 JSON
CocosCreator 面试题(十五)Cocos Creator如何内置protobuf JS版本?
CocosCreator 面试题(十五)Cocos Creator如何内置protobuf JS版本?
397 0
|
前端开发
CocosCreator 面试题(九)什么是异步加载资源
CocosCreator 面试题(九)什么是异步加载资源
365 0
|
JavaScript 前端开发
CocosCreator 面试题(二)JavaScript中的prototype的理解
CocosCreator 面试题(二)JavaScript中的prototype的理解
394 0
|
JavaScript 安全 编译器
CocosCreator 面试题(六)什么是泛型,有什么作用?
CocosCreator 面试题(六)什么是泛型,有什么作用?
329 0
|
存储
CocosCreator3.8研究笔记(二十二)CocosCreator 动画系统-动画剪辑和动画组件介绍
CocosCreator3.8研究笔记(二十二)CocosCreator 动画系统-动画剪辑和动画组件介绍
578 0
|
安全 网络安全 数据安全/隐私保护
CocosCreator 面试题(十四)Cocos Creator WebSocket 、Socket.IO分别是什么?
CocosCreator 面试题(十四)Cocos Creator WebSocket 、Socket.IO分别是什么?
745 0
|
Android开发 索引
Cocos Creator3.8 项目实战(七)Listview 控件的实现和使用(1)
Cocos Creator3.8 项目实战(七)Listview 控件的实现和使用
935 0
|
编解码
CocosCreator 面试题(十七)Cocos creator 固定宽度与固定高度的底层原理是什么?Cocos creator是如何做适配的?
CocosCreator 面试题(十七)Cocos creator 固定宽度与固定高度的底层原理是什么?Cocos creator是如何做适配的?
584 0
|
缓存
CocosCreator 面试题(八)Cocos Creator 中如何做资源管理
CocosCreator 面试题(八)Cocos Creator 中如何做资源管理
1032 0