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

2023-10-07 07:04

本文主要是介绍Cocos Creator3.8 项目实战(七)Listview 控件的实现和使用,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!


滚动列表在游戏中也很常见,比如排行榜 、充值记录等,在这些场景中,都有共同的特点, 那就是:数据量大 , 结构相同


在cocoscreator 中,没有现成的 Listview 控件, 无奈之下, 只能自己动手 用ScrollView 来实现一个。这样,有类似需求的朋友,能专注业务功能的开发,就不用重复造轮了。


⚠️ 文末附 ListView.ts 完整源码, 可直接拿去使用。

下面以排行榜Listview 实现为例,进行详细说明。


ListView 实现效果:

在这里插入图片描述


ListView 实现原理:

ListView 实现方式,类似 Android的 ListView 。

采用了AbsAdapter 适配器,用于设置数据,更新视图页面,获取数据数量,计算 item 显示位置等。

采用了 ScrollView 配合 item 预制体Prefab 来实现,动态生成列表项, 支持调整 item 项的间距,支持横向和竖向滚动 。

ListView 还设计了简单的上/下拉通知, 只需要初始化时设置相应回调方法即可。


使用步骤:

step 1 ,在creator层级管理器中,新建 ScrollView 节点,并做如下配置:

这里命名为 sore_rank_listview


在这里插入图片描述


step 2 ,独立新建一个item 预制体文件

这里命名为:score_rank_item ,添加了以下属性和布局

请添加图片描述


在这里插入图片描述


step 3 ,在层级管理器中,选择score_rank_item 节点,然后在creator属性检查器中,挂载ScoreRankItem.ts 脚本,并做如下属性配置:

请添加图片描述


step 4 ,在层级管理器中,选择Listview 节点,然后在creator属性检查器中,挂载Listview.ts 脚本,并做如下配置:

在这里插入图片描述

参数解释:

  • Spacing :用来约定item 之间的间距
  • SpawnCount: 用来约定超过可见区域的额外显示项数,可以调整滚动时的平滑性。
  • Item Template :独立的item 预制体
  • scroollview : 滚动条控件,在这里和 listview 控件是同一个节点

step 5 ,根据排行榜显示内容,我们准备了一个数据结构

export class RankItemData {/** 用户ID */userid:number;/** 用户昵称 */nickName:string;/** 排行名次 */topLevel:number;/** 自定义头像id */faceid:number;/** VIP */vipLevel:number;/** 金币 */score:number;reset(){this.userid = 0;this.nickName = '';this.topLevel = 0;this.faceid = 0;this.vipLevel = 0;this.score = 0;}
}

step 6 ,我们需要准备数据列表或者是数组

 // 离线测试代码let datas:Array<RankItemData>= new Array<RankItemData>;for(let i=0;i<100;i++){let itemData:RankItemData = new RankItemData();itemData.userid = 1000+i;itemData.faceid= 1;itemData.nickName="userName"+i;itemData.topLevel = i+1;itemData.vipLevel = i % 7 + 1;itemData.score = (101 - i)*10000;datas[i] = itemData;  }

step 7 ,我们需要一个数据到Item的适配层, ListView 组件类中提供了一个基类AbsAdapter ,我们实现它。

只需要继承此类,重写updateView()函数,对相应索引的itemComponent进行数据设置即可:

class ScoreRankListAdapter extends AbsAdapter {​    updateView(item:Node, posIndex: number) {
​        let comp = item.getComponent(ScoreRankItemComp);
​        if (comp) {
​            let data = this.getItem(posIndex);
​            comp.setData(this.getItem(posIndex));
​        }
​    }
}

step 8,数据显示和更新

@property(ListView)
private scoreRankListView:ListView;private _scoreRankListAdapter: ScoreRankListAdapter | null = null;
get scoreRankListAdapter(): ScoreRankListAdapter {if (!this._scoreRankListAdapter) {this._scoreRankListAdapter = new ScoreRankListAdapter();}return this._scoreRankListAdapter;
}    this.scoreRankListAdapter.setDataSet(args);
this.scoreRankListView.setAdapter(this.scoreRankListAdapter);

step 9、ScoreRankItem.ts 源码

import { _decorator,Component,Label, Sprite} from "cc";
const { ccclass, property } = _decorator;@ccclass
export  class ScoreRankItem extends Component {@property(Label)private labelLevel!:Label;@property(Sprite)private spriteAvatr!:Sprite;@property(Label)private lableNickName!:Label;@property(Label)private labelVip!:Label;@property(Label)private labelScore!:Label;@property(Sprite)private spriteLevel1!:Sprite;@property(Sprite)private spriteLevel2!:Sprite;@property(Sprite)private spriteLevel3!:Sprite;public setData(data: any) {const itemData = data as RankItemData;this.lableNickName.string = itemData.nickName;this.labelVip.string = "VIP " + String(itemData.vipLevel);this.labelScore.string =  String(itemData.score);...}
}

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 之间的间距*/@propertyprotected spacing: number = 1;/*** 用来约定超过可见区域的额外显示项数,可以调整滚动时的平滑性.* 比可见元素多缓存3个, 缓存越多,快速滑动越流畅,但同时初始化越慢.*/@propertyprotected 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 = falsethis.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);}// 获取可回收itemprotected 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 项目实战(七)Listview 控件的实现和使用的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/156834

相关文章

Spring IoC 容器的使用详解(最新整理)

《SpringIoC容器的使用详解(最新整理)》文章介绍了Spring框架中的应用分层思想与IoC容器原理,通过分层解耦业务逻辑、数据访问等模块,IoC容器利用@Component注解管理Bean... 目录1. 应用分层2. IoC 的介绍3. IoC 容器的使用3.1. bean 的存储3.2. 方法注

MySQL中查找重复值的实现

《MySQL中查找重复值的实现》查找重复值是一项常见需求,比如在数据清理、数据分析、数据质量检查等场景下,我们常常需要找出表中某列或多列的重复值,具有一定的参考价值,感兴趣的可以了解一下... 目录技术背景实现步骤方法一:使用GROUP BY和HAVING子句方法二:仅返回重复值方法三:返回完整记录方法四:

Python内置函数之classmethod函数使用详解

《Python内置函数之classmethod函数使用详解》:本文主要介绍Python内置函数之classmethod函数使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地... 目录1. 类方法定义与基本语法2. 类方法 vs 实例方法 vs 静态方法3. 核心特性与用法(1编程客

IDEA中新建/切换Git分支的实现步骤

《IDEA中新建/切换Git分支的实现步骤》本文主要介绍了IDEA中新建/切换Git分支的实现步骤,通过菜单创建新分支并选择是否切换,创建后在Git详情或右键Checkout中切换分支,感兴趣的可以了... 前提:项目已被Git托管1、点击上方栏Git->NewBrancjsh...2、输入新的分支的

Linux中压缩、网络传输与系统监控工具的使用完整指南

《Linux中压缩、网络传输与系统监控工具的使用完整指南》在Linux系统管理中,压缩与传输工具是数据备份和远程协作的桥梁,而系统监控工具则是保障服务器稳定运行的眼睛,下面小编就来和大家详细介绍一下它... 目录引言一、压缩与解压:数据存储与传输的优化核心1. zip/unzip:通用压缩格式的便捷操作2.

Python实现对阿里云OSS对象存储的操作详解

《Python实现对阿里云OSS对象存储的操作详解》这篇文章主要为大家详细介绍了Python实现对阿里云OSS对象存储的操作相关知识,包括连接,上传,下载,列举等功能,感兴趣的小伙伴可以了解下... 目录一、直接使用代码二、详细使用1. 环境准备2. 初始化配置3. bucket配置创建4. 文件上传到os

关于集合与数组转换实现方法

《关于集合与数组转换实现方法》:本文主要介绍关于集合与数组转换实现方法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、Arrays.asList()1.1、方法作用1.2、内部实现1.3、修改元素的影响1.4、注意事项2、list.toArray()2.1、方

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

深度解析Java项目中包和包之间的联系

《深度解析Java项目中包和包之间的联系》文章浏览阅读850次,点赞13次,收藏8次。本文详细介绍了Java分层架构中的几个关键包:DTO、Controller、Service和Mapper。_jav... 目录前言一、各大包1.DTO1.1、DTO的核心用途1.2. DTO与实体类(Entity)的区别1

使用Python实现可恢复式多线程下载器

《使用Python实现可恢复式多线程下载器》在数字时代,大文件下载已成为日常操作,本文将手把手教你用Python打造专业级下载器,实现断点续传,多线程加速,速度限制等功能,感兴趣的小伙伴可以了解下... 目录一、智能续传:从崩溃边缘抢救进度二、多线程加速:榨干网络带宽三、速度控制:做网络的好邻居四、终端交互