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

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

使用SecondaryNameNode恢复NameNode的数据

1)需求: NameNode进程挂了并且存储的数据也丢失了,如何恢复NameNode 此种方式恢复的数据可能存在小部分数据的丢失。 2)故障模拟 (1)kill -9 NameNode进程 [lytfly@hadoop102 current]$ kill -9 19886 (2)删除NameNode存储的数据(/opt/module/hadoop-3.1.4/data/tmp/dfs/na

Hadoop数据压缩使用介绍

一、压缩原则 (1)运算密集型的Job,少用压缩 (2)IO密集型的Job,多用压缩 二、压缩算法比较 三、压缩位置选择 四、压缩参数配置 1)为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器 2)要在Hadoop中启用压缩,可以配置如下参数

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

如何用Docker运行Django项目

本章教程,介绍如何用Docker创建一个Django,并运行能够访问。 一、拉取镜像 这里我们使用python3.11版本的docker镜像 docker pull python:3.11 二、运行容器 这里我们将容器内部的8080端口,映射到宿主机的80端口上。 docker run -itd --name python311 -p

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传