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);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/96252.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【JavaEE重点知识归纳】第5节:方法

目录 一&#xff1a;方法的概念和使用 1.什么是方法 2.方法的定义 3.方法的调用过程 4.实参和形参的关系&#xff08;重点&#xff09; 二:方法重载 1.方法重载概念 2.方法签名 三&#xff1a;递归 1.递归的概念 2.递归执行的过程分析 一&#xff1a;方法的概念和使…

[开源项目推荐]privateGPT使用体验和修改

文章目录 一.跑通简述二.解读ingest.py1.导入库和读取环境变量2.文档加载3.文档处理&#xff08;调用文件加载函数和文本分割函数&#xff09; 三.injest.py效果演示1.main函数解读2.测试 四.修改代码&#xff0c;使之适配多知识库需求1.修改配置文件&#xff1a;constants.py2…

Elasticsearch:ES|QL 查询语言简介

警告&#xff1a;此功能处于技术预览阶段&#xff0c;可能会在未来版本中更改或删除。 Elastic 将尽最大努力解决任何问题&#xff0c;但技术预览版中的功能不受官方 GA 功能的支持 SLA 的约束。在目前的 Elastic Stack 8.10 中此功能还没有提供。 Elasticsearch 查询语言 (ES|…

IntelliJ IDEA 常用快捷键

目录 一、IDEA 常用快捷键 1 通用型 2 提高编写速度 3 类结构、查找和查看源码 4 查找、替换与关闭 5 调整格式 二、Debug快捷键 三、查看快捷键 1、已知快捷键操作名&#xff0c;未知快捷键 2、已知快捷键&#xff0c;不知道对应的操作名 3、自定义快捷键 4、使用…

MyBatisPlus(十一)包含查询:in

说明 包含查询&#xff0c;对应SQL语句中的 in 语句&#xff0c;查询参数包含在入参列表之内的数据。 in Testvoid inNonEmptyList() {// 非空列表&#xff0c;作为参数List<Integer> ages Stream.of(18, 20, 22).collect(Collectors.toList());in(ages);}Testvoid in…

[强网杯 2022]factor有感

可直接私信&#xff0b;Q 3431550587 此题记录主要是他运用了几个新看见的攻击思路和拜读了一篇论文&#xff0c;所以写写。题目源码&#xff1a; #encoding:utf-8 from Crypto.Util.number import * from gmpy2 import * from random import randint from flag import flagd…

AGI之MFM:《多模态基础模型:从专家到通用助手》翻译与解读之视觉理解、视觉生成

​AGI之MFM&#xff1a;《Multimodal Foundation Models: From Specialists to General-Purpose Assistants多模态基础模型&#xff1a;从专家到通用助手》翻译与解读之视觉理解、视觉生成 目录 相关文章 AGI之MFM&#xff1a;《Multimodal Foundation Models: From Speciali…

【Docker内容大集合】Docker从认识到实践再到底层原理大汇总

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量博客汇总https://blog.csdn.net/yu_cblog/categ…

硬盘损坏不能用怎么办?

硬盘是电脑的核心&#xff0c;如果硬盘出现了问题&#xff0c;那么整台电脑都会受到影响&#xff0c;电脑中的数据也会丢失。那么面对硬盘损坏时我们该如何解决呢&#xff1f;本文今天用5种简单的方法帮您解决&#xff01; 1、硬盘连接是否松动 当电脑的硬盘突然表现出无法识别…

stm32之手动创建keil工程--HAL库

用CubeMx创建了好多stm32的工程&#xff0c;这里记录下手动创建keil工程的过程。 一、准备工作 1.1、下载对应的HAL库&#xff0c; 这里使用的是stm32f103c8t6, 下载地址stm32HAL库 在页面中输入对应型号点击进行二级页面进行下载 1.2、准备工程 各文件夹下具体操作如下&am…

破译滑块验证间距 破译sf顺丰滑块验证

废话不多说直接开干&#xff01; from selenium import webdriver # 导入配置 from selenium.webdriver.chrome.options import Options import time from PIL import Image # 导入动作链 from selenium.webdriver.common.action_chains import ActionChains import random, st…

雷达电子箔条干扰和雷达诱饵简介

一、JAMMING 利用箔条、角反射器和诱饵进行机械干扰。 1、箔条(Chaff) 箔条是一种雷达干扰,在这种干扰中,飞机或其他目标散布一团薄薄的铝、金属化玻璃纤维或塑料,它们要么作为一组主要目标出现在雷达屏幕上,要么以多次回击淹没屏幕。 图1 箔条 2、箔条的雷达散射截面…

linux系统中三个重要的结构体

第一​&#xff1a;struct inode结构体 struct inode { struct hlist_node i_hash; struct list_head i_list; /* backing dev IO list */ struct list_head i_sb_list;​ //主次设备号 dev_t i_rdev;​ struct list_head i_devices; //用联合体是因为该…

uniapp 实现地图头像上的水波纹效果

最近实现了uniapp 地图头像水波纹的效果&#xff0c;话不多说&#xff0c;先来看看视频效果吧&#xff1a;链接 在这里具体的代码就不放出来了&#xff0c;还是利用了uniapp的 uni.createAnimation 方法&#xff0c;因为cover-view 不支持一些css 的动画效果&#xff0c;所以这…

如何使用百度“云一朵”来分析PDF文件

PDF 文件是一种常见的文件格式&#xff0c;用于存储文档、图像和其他内容。在许多情况下&#xff0c;我们需要对 PDF 文件进行分析&#xff0c;以提取其中的信息。百度“云一朵”提供了一个 PDF 分析 API&#xff0c;可以帮助我们轻松地对 PDF 文件进行分析。 在本博客文章中&…

基于Java的企业人事管理系统设计与实现(源码+lw+ppt+部署文档+视频讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

进程互斥的软件实现方法,硬件实现方法以及互斥锁

1.进程互斥的软件实现方法 1.单标志法 1.算法思想: 两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。 也就是说每个进程进入临界区的权限只能被另一个进程赋予。 2.例子 因此&#xff0c;该算法可以实现“同一时刻最多只允许一个进程访问临界区”。 3.主要…

Polygon Mide状态模型:解决状态膨胀,而不牺牲隐私和去中心化

1. 引言 前序博客有&#xff1a; Polygon Miden&#xff1a;扩展以太坊功能集的ZK-optimized rollupPolygon Miden zkRollup中的UTXO账户混合状态模型Polygon Miden交易模型&#xff1a;Actor模式 ZKP &#xff1e; 并行 隐私 在Polygon Miden交易模型&#xff1a;Actor模…

定时器+按键控制LED流水灯模式+定时器时钟——“51单片机”

各位CSDN的uu们好呀&#xff0c;今天&#xff0c;小雅兰的内容是51单片机中的定时器以及按键控制LED流水灯模式&定时器时钟&#xff0c;下面&#xff0c;让我们进入51单片机的世界吧&#xff01;&#xff01;&#xff01; 定时器 按键控制LED流水灯模式 定时器时钟 源代…

微服务技术栈-Docker应用部署

文章目录 前言一、数据卷二、Docker 应用部署1、MySQL部署2、Tomcat部署3、Nginx部署4、Redis部署5、Kafka部署 总结 前言 之前文章讲到过&#xff0c;docker运行程序的过程就是去仓库把镜像拉到本地&#xff0c;然后用一条命令把镜像运行起来变成容器&#xff0c;接下来我们将…