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.rhkb.cn/news/150235.html

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

相关文章

NEFU数字图像处理(1)绪论

一、简介 1.1什么是数字图像 图像是三维场景在二维平面上的影像。根据其存储方式和表现形式&#xff0c;可以将图像分为模拟图像和数字图像两大类 图像处理方法&#xff1a;光学方法、电子学方法 模拟图像&#xff1a;连续的图像数字图像&#xff1a;通过对时间上和数值上连续…

【拿完年终奖后】想要转行网络安全,一定不要错过这个时间段。

网络安全&#xff0c;作为当下互联网行业中较为热门的岗位&#xff0c;薪资可观、人才需求量大&#xff0c;作为转行必考虑。 在这里奉劝所有零基础想转行&#xff08;入门&#xff09; 网络安全的朋友们 在转行之前&#xff0c;一定要对网络安全行业做一个大概了解&#xf…

Java卷上天,可以转行干什么?

小刚是某名企里的一位有5年经验的高级Java开发工程师&#xff0c;每天沉重的的工作让他疲惫不堪&#xff0c;让他萌生出想换工作的心理&#xff0c;但是转行其他工作他又不清楚该找什么样的工作 因为JAVA 这几年的更新实在是太太太……快了&#xff0c;JAVA 8 都还没用多久&am…

【算法|动态规划No.13】leetcode LCR 166. 珠宝的最高价值

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】【LeetCode】 &#x1f354;本专栏旨在提高自己算法能力的同时&#xff0c;记录一下自己的学习过程&#xff0c;希望…

计算机视觉——飞桨深度学习实战-起始篇

后面我会直接跳到实战项目&#xff0c;将计算机视觉的主要任务和目标都实现一遍&#xff0c;但是需要大家下去自己多理解和学习一下。例如&#xff0c;什么是深度学习&#xff0c;什么是计算机视觉&#xff0c;什么是自然语言处理&#xff0c;计算机视觉的主要任务有哪些&#…

121-宏免杀

CS生成宏&上线 生成宏 1.cs生成宏&#xff0c;如下图操作 2.点击复制宏代码&#xff0c;保存下来 cs上线 注&#xff1a;如下操作使用的是word&#xff0c;同样的操作也适用于Excel 1.新建一个word文档&#xff0c;使用word打开。点击文件—— 2.更多——选项—— 3.自定义…

(三)行为模式:8、状态模式(State Pattern)(C++示例)

目录 1、状态模式&#xff08;State Pattern&#xff09;含义 2、状态模式的UML图学习 3、状态模式的应用场景 4、状态模式的优缺点 &#xff08;1&#xff09;优点 &#xff08;2&#xff09;缺点 5、C实现状态模式的实例 1、状态模式&#xff08;State Pattern&#x…

大数据软件项目的验收流程

大数据软件项目的验收流程是确保项目交付符合预期需求和质量标准的关键步骤。以下是一般的大数据软件项目验收流程&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.项目验收计划制定&#xff1a; 在…

工业路由器项目应用(4g+5g两种工业路由器项目介绍)

引言&#xff1a; 随着工业智能化的不断发展&#xff0c;工业路由器在各个领域的应用越来越广泛。本文将介绍两个工业路由器项目的应用案例&#xff0c;一个是使用SR500 4g工业路由器&#xff0c;另一个是使用SR800 5g工业路由器。 详情&#xff1a;https://www.key-iot.com/i…

STM32+USB3300复位枚举异常的问题

关键字&#xff1a;STM32F4&#xff0c;STM32H7&#xff0c;USB3300&#xff0c;USBHS&#xff0c;Reset复位 F4和H7用的都是DWC2的USBIP&#xff0c;我的板子上3300单片机工作的很好&#xff0c;插入枚举一切正常&#xff0c;但是设备收到上位机的复位命令后&#xff0c;单片…

新手选MT4还是MT5,anzo capital昂首资本建议选择MT4,一个原因

在交易中就订单执行策略而言&#xff0c;MT4和MT5哪个更好&#xff0c;相信很多交易者和&#xff0c;anzo capital昂首资本一样很难做出判断。在MT5中&#xff0c;虽然开发人员对发送订单的流程进行了额外的复杂化&#xff0c;同时MT5在订单执行政策方面的优势在于其能够调整全…

Langchain-Chatchat项目:1-整体介绍

基于Langchain与ChatGLM等语言模型的本地知识库问答应用实现。项目中默认LLM模型改为THUDM/chatglm2-6b[2]&#xff0c;默认Embedding模型改为moka-ai/m3e-base[3]。 一.项目介绍 1.实现原理   本项目实现原理如下图所示&#xff0c;过程包括加载文件->读取文本->文本…

Langchain-Chatchat项目:1.2-Baichuan2项目整体介绍

由百川智能推出的新一代开源大语言模型&#xff0c;采用2.6万亿Tokens的高质量语料训练&#xff0c;在多个权威的中文、英文和多语言的通用、领域benchmark上取得同尺寸最佳的效果&#xff0c;发布包含有7B、13B的Base和经过PPO训练的Chat版本&#xff0c;并提供了Chat版本的4b…

Windows 10 没有【休眠】选项的配置操作

目录 一、相关知识 1.1、名词解释 二、睡眠/休眠选项 三、睡眠/休眠配置 3.1 打开休眠配置 3.2 打开休眠功能 一、相关知识 1.1、名词解释 睡眠&#xff1a; 当Windows系统进入睡眠模式之后&#xff0c;将电脑当前的内存中的运行状态和数据存储在硬盘中&#xff0…

EQ 均衡器

EQ 的全称是 Equalizer&#xff0c;EQ 是 Equalizer 的前两个字母&#xff0c;中文名字叫做“均衡器”。最早是用来提升电话信号在长距离的传输中损失的高频&#xff0c;由此得到一个各频带相对平衡的结果&#xff0c;它让各个频带的声音得到了均衡。 EQ 的主要功能是&#xf…

从零开始学习线性回归:理论、实践与PyTorch实现

文章目录 &#x1f966;介绍&#x1f966;基本知识&#x1f966;代码实现&#x1f966;完整代码&#x1f966;总结 &#x1f966;介绍 线性回归是统计学和机器学习中最简单而强大的算法之一&#xff0c;用于建模和预测连续性数值输出与输入特征之间的关系。本博客将深入探讨线性…

Python爬虫(二十二)_selenium案例:模拟登陆豆瓣

本篇博客主要用于介绍如何使用seleniumphantomJS模拟登陆豆瓣&#xff0c;没有考虑验证码的问题&#xff0c;更多内容&#xff0c;请参考&#xff1a;Python学习指南 #-*- coding:utf-8 -*-from selenium import webdriver from selenium.webdriver.common.keys import Keysimp…

IDEA 使用

目录 Git.gitignore 不上传取消idea自动 add file to git撤销commit的内容本地已经有一个开发完成的项目&#xff0c;这个时候想要上传到仓库中 Git .gitignore 不上传 在项目根目录下创建 .gitignore 文件夹&#xff0c;并添加内容&#xff1a; .gitignore取消idea自动 add…

Leetcode901-股票价格跨度

一、前言 本题基于leetcode901股票价格趋势这道题&#xff0c;说一下通过java解决的一些方法。并且解释一下笔者写这道题之前的想法和一些自己遇到的错误。需要注意的是&#xff0c;该题最多调用 next 方法 10^4 次,一般出现该提示说明需要注意时间复杂度。 二、解决思路 ①…