// Learn TypeScript: // - https://docs.cocos.com/creator/manual/en/scripting/typescript.html // Learn Attribute: // - https://docs.cocos.com/creator/manual/en/scripting/reference/attributes.html // Learn life-cycle callbacks: // - https://docs.cocos.com/creator/manual/en/scripting/life-cycle-callbacks.html import RoundBox from "../../Script/module/Tool/RoundBox"; import CareerItem from "./CareerItem"; // import CareerItem from "./CareerItem" const { ccclass, property } = cc._decorator; /**列表排列方式 */ export enum ListType { /**水平排列 */ Horizontal = 1, /**垂直排列 */ Vertical = 2, /**网格排列 */ Grid = 3 } /**网格布局中的方向 */ export enum StartAxisType { /**水平排列 */ Horizontal = 1, /**垂直排列 */ Vertical = 2, } /** * 列表 * 根据cocos_example的listView改动而来 * @author chenkai 2020.7.8 * @example * 1.创建cocos的ScrollView组件,添加List,设置List属性即可 * */ @ccclass export default class CareerList extends cc.Component { //==================== 属性面板 ========================= /**列表选项 */ @property({ type: cc.Node, tooltip: "列表项" }) public itemRender: cc.Node = null; @property({ type: cc.Node, tooltip: "列表项" }) public firstRender: cc.Node = null; /**排列方式 */ @property({ type: cc.Enum(ListType), tooltip: "排列方式" }) public type: ListType = ListType.Vertical; /**网格布局中的方向 */ @property({ type: cc.Enum(StartAxisType), tooltip: "网格布局中的方向", visible() { return this.type == ListType.Grid } }) public startAxis: StartAxisType = StartAxisType.Horizontal; /**列表项之间X间隔 */ @property({ type: cc.Integer, tooltip: "列表项X间隔", visible() { return (this.type == ListType.Horizontal || this.type == ListType.Grid) } }) public spaceX: number = 0; /**列表项之间Y间隔 */ @property({ type: cc.Integer, tooltip: "列表项Y间隔", visible() { return this.type == ListType.Vertical || this.type == ListType.Grid } }) public spaceY: number = 0; /**上间距 */ @property({ type: cc.Integer, tooltip: "上间距", visible() { return (this.type == ListType.Vertical || this.type == ListType.Grid) } }) public padding_top: number = 0; /**下间距 */ @property({ type: cc.Integer, tooltip: "下间距", visible() { return (this.type == ListType.Vertical || this.type == ListType.Grid) } }) public padding_buttom: number = 0; /**左间距 */ @property({ type: cc.Integer, tooltip: "左间距", visible() { return (this.type == ListType.Horizontal || this.type == ListType.Grid || this.type == ListType.Vertical) } }) public padding_left: number = 0; @property(cc.Integer) public _padding: number = 0; /**右间距 */ @property({ type: cc.Integer, tooltip: "右间距", visible() { return (this.type == ListType.Horizontal || this.type == ListType.Grid) } }) public padding_right: number = 0; //====================== 滚动容器 =============================== /**列表滚动容器 */ public scrollView: cc.ScrollView = null; /**scrollView的内容容器 */ private content: cc.Node = null; //======================== 列表项 =========================== /**列表项数据 */ private itemDataList: Array = []; /**应创建的实例数量 */ private spawnCount: number = 0; /**存放列表项实例的数组 */ private itemList: Array = []; /**item的高度 */ private itemHeight: number = 0; /**item的宽度 */ private itemWidth: number = 0; /**存放不再使用中的列表项 */ private itemPool: Array = []; //======================= 计算参数 ========================== /**距离scrollView中心点的距离,超过这个距离的item会被重置,一般设置为 scrollVIew.height/2 + item.heigt/2 + space,因为这个距离item正好超出scrollView显示范围 */ private halfScrollView: number = 0; /**上一次content的X值,用于和现在content的X值比较,得出是向左还是向右滚动 */ private lastContentPosX: number = 0; /**上一次content的Y值,用于和现在content的Y值比较,得出是向上还是向下滚动 */ private lastContentPosY: number = 0; /**网格行数 */ private gridRow: number = 0; /**网格列数 */ private gridCol: number = 0; /**刷新时间,单位s */ private updateTimer: number = 0; /**刷新间隔,单位s */ private updateInterval: number = 0.1; /**是否滚动容器 */ private bScrolling: boolean = false; /**刷新的函数 */ private updateFun: Function = function () { }; randerChildren: any[]; topData: any; @property(cc.SpriteAtlas) UI: cc.SpriteAtlas = null; onLoad() { this.randerChildren = []; this.itemHeight = this.itemRender.height; this.itemWidth = this.itemRender.width; this.scrollView = this.node.getComponent(cc.ScrollView); this.content = this.scrollView.content; this.content.anchorX = 0; this.content.anchorY = 1; // this.content.removeAllChildren(); // 初始化firstRender if (this.firstRender) { this.firstRender.parent = this.content; // this.firstRender.active = true; // this.firstRenderInit(); // 设置firstRender的位置在顶部 if (this.type == ListType.Vertical) { this.firstRender.setPosition(535, 0); } else if (this.type == ListType.Horizontal) { this.firstRender.setPosition(this.firstRender.width / 2, 0); } } this.scrollView.node.on("scrolling", this.onScrolling, this); } /** * 列表数据 (列表数据复制使用,如果列表数据改变,则需要重新设置一遍数据) * @param itemDataList item数据列表 */ public setData(itemDataList: Array, topData: any) { // 检查 itemDataList 是否为有效数组,如果不是则初始化为空数组 this.itemDataList = itemDataList.slice(); this.topData = topData; this.firstRenderInit(); this.updateContent(); } /**计算列表的各项参数 */ private countListParam() { let dataLen = this.itemDataList.length; let firstRenderHeight = this.firstRender ? this.firstRender.height : 0; let firstRenderWidth = this.firstRender ? this.firstRender.width : 0; if (this.type == ListType.Vertical) { this.scrollView.horizontal = false; this.scrollView.vertical = true; this.content.width = this.content.parent.width; // 计算高度时考虑firstRender的高度和间距 this.content.height = firstRenderHeight + (firstRenderHeight > 0 ? this.spaceY : 0) + dataLen * this.itemHeight + (dataLen - 3) * this.spaceY + this.padding_top + this.padding_buttom + 500; this.spawnCount = Math.round(this.scrollView.node.height / (this.itemHeight + this.spaceY)) + 4; //计算创建的item实例数量,比当前scrollView容器能放下的item数量再加上2个 this.halfScrollView = this.scrollView.node.height / 2 + this.itemHeight / 2 + this.spaceY; //计算bufferZone,item的显示范围 this.updateFun = this.updateV; } } /** * 创建列表 * @param startIndex 起始显示的数据索引 0表示第一项 * @param offset scrollView偏移量 */ private createList(startIndex: number, offset: cc.Vec2) { //当需要显示的数据长度 > 虚拟列表长度, 删除最末尾几个数据时,列表需要重置位置到scrollView最底端 if (this.itemDataList.length > this.spawnCount && (startIndex + this.spawnCount - 1) >= this.itemDataList.length) { startIndex = this.itemDataList.length - this.spawnCount; offset = this.scrollView.getMaxScrollOffset(); //当需要显示的数据长度 <= 虚拟列表长度, 隐藏多余的虚拟列表项 } else if (this.itemDataList.length <= this.spawnCount) { startIndex = 0; } for (let i = 0; i < this.spawnCount; i++) { let item: cc.Node; //需要显示的数据索引在数据范围内,则item实例显示出来 if (i + startIndex < this.itemDataList.length) { if (this.itemList[i] == null) { item = this.getItem(); this.itemList.push(item); item.active = true; item.parent = this.content; } else { item = this.itemList[i]; } //需要显示的数据索引超过了数据范围,则item实例隐藏起来 } else { //item实例数量 > 需要显示的数据量 if (this.itemList.length > (this.itemDataList.length - startIndex)) { item = this.itemList.pop(); item.removeFromParent(); this.itemPool.push(item); } continue; } let itemRender: CareerItem = item.getComponent(CareerItem); itemRender.itemIndex = i + startIndex; itemRender.data = this.itemDataList[i + startIndex]; itemRender.dataChanged(); // 计算firstRender的高度偏移 let firstRenderHeight = this.firstRender ? this.firstRender.height : 0; let firstRenderOffsetY = firstRenderHeight > 0 ? firstRenderHeight + this.spaceY : 0; if (this.type == ListType.Vertical) { // item左边对齐padding_left item.setPosition(this.padding_left + item.width / 2, -item.height * (0.5 + i + startIndex) - this.spaceY * (i + startIndex) - this.padding_top - firstRenderOffsetY); } } this.scrollView.scrollToOffset(offset); } /**获取一个列表项 */ private getItem() { if (this.itemPool.length == 0) { return cc.instantiate(this.itemRender); } else { return this.itemPool.pop(); } } update(dt) { if (this.bScrolling == false) { return; } this.updateTimer += dt; if (this.updateTimer < this.updateInterval) { return; } this.updateTimer = 0; this.bScrolling = false; this.updateFun(); } onScrolling() { this.bScrolling = true; } /**垂直排列 */ private updateV() { let items = this.itemList; let item; let bufferZone = this.halfScrollView; let isUp = this.scrollView.content.y > this.lastContentPosY; let offset = (this.itemHeight + this.spaceY) * items.length; // 计算firstRender的高度偏移 let firstRenderHeight = this.firstRender ? this.firstRender.height : 0; let firstRenderOffsetY = firstRenderHeight > 0 ? firstRenderHeight + this.spaceY : 0; for (let i = 0; i < items.length; i++) { item = items[i]; let viewPos = this.getPositionInView(item); if (isUp) { if (viewPos) { //item上滑时,超出了scrollView上边界,将item移动到下方复用,item移动到下方的位置必须不超过content的下边界 if (viewPos.y > bufferZone && item.y - offset - this.padding_buttom > -this.content.height - firstRenderOffsetY) { let itemRender: CareerItem = item.getComponent(CareerItem); let itemIndex = itemRender.itemIndex + items.length; itemRender.itemIndex = itemIndex; itemRender.data = this.itemDataList[itemIndex]; itemRender.dataChanged(); item.y = item.y - offset; } } } else { if (viewPos) { //item下滑时,超出了scrollView下边界,将item移动到上方复用,item移动到上方的位置必须不超过content的上边界 if (viewPos.y < -bufferZone && item.y + offset + this.padding_top < -firstRenderOffsetY) { let itemRender: CareerItem = item.getComponent(CareerItem); let itemIndex = itemRender.itemIndex - items.length; itemRender.itemIndex = itemIndex; itemRender.data = this.itemDataList[itemIndex]; itemRender.dataChanged(); item.y = item.y + offset; } } } } this.lastContentPosY = this.scrollView.content.y; } /**获取item在scrollView的局部坐标 */ private getPositionInView(item) { // if (item.parent) { let worldPos = item.parent.convertToWorldSpaceAR(item.position); let viewPos = this.scrollView.node.convertToNodeSpaceAR(worldPos); return viewPos; // } } /**获取列表数据 */ public getListData() { return this.itemDataList; } /** * 增加一项数据到列表的末尾 * @param data 数据 */ public addItem(data: any) { this.itemDataList.push(data); this.updateContent(); } /** * 增加一项数据到列表指定位置 * @param index 位置,0表示第1项 * @param data 数据 */ public addItemAt(index: number, data: any) { if (this.itemDataList[index] != null || this.itemDataList.length == index) { this.itemDataList.splice(index, 1, data); this.updateContent(); } } /** * 删除一项数据 * @param index 删除项的位置 ,0表示第1项 */ public deleteItem(index: number) { if (this.itemDataList[index] != null) { this.itemDataList.splice(index, 1); this.updateContent(); } } /** * 改变一项数据 * @param index 位置,0表示第1项 * @param data 替换的数据 */ public changeItem(index: number, data: any) { if (this.itemDataList[index] != null) { this.itemDataList[index] = data; this.updateContent(); } } /**获取第一个Item的位置 */ private updateContent() { //显示列表实例为0个 if (this.itemList.length == 0) { this.countListParam(); this.createList(0, new cc.Vec2(0, 0)); //显示列表的实例不为0个,则需要重新排列item实例数组 } else { if (this.type == ListType.Vertical) { this.itemList.sort((a: any, b: any) => { return b.y - a.y; }); } else if (this.type == ListType.Horizontal) { this.itemList.sort((a: any, b: any) => { return a.x - b.x; }); } else if (this.type == ListType.Grid) { if (this.startAxis == StartAxisType.Vertical) { this.itemList.sort((a: any, b: any) => { return a.x - b.x; }); this.itemList.sort((a: any, b: any) => { return b.y - a.y; }); } else if (this.startAxis == StartAxisType.Horizontal) { this.itemList.sort((a: any, b: any) => { return b.y - a.y; }); this.itemList.sort((a: any, b: any) => { return a.x - b.x; }); } } this.countListParam(); //获取第一个item实例需要显示的数据索引 var startIndex = this.itemList[0].getComponent(CareerItem).itemIndex; if (this.type == ListType.Grid && this.startAxis == StartAxisType.Vertical) { startIndex += (startIndex + this.spawnCount) % this.gridCol; } else if (this.type == ListType.Grid && this.startAxis == StartAxisType.Horizontal) { startIndex += (startIndex + this.spawnCount) % this.gridRow; } //getScrollOffset()和scrollToOffset()的x值是相反的 var offset: cc.Vec2 = this.scrollView.getScrollOffset(); offset.x = - offset.x; this.createList(startIndex, offset); } } public firstRenderInit() { // let this.firstRender.active = true; if (this.randerChildren.length == 0) { this.randerChildren = []; let length = this.topData.length + 3; if (length > this.firstRender.children.length) length = this.firstRender.children.length; for (let i = 3; i < length; i++) { this.randerChildren.push(this.firstRender.children[i]); let username = cc.fx.GameTool.subName(this.topData[i - 3].username, 5); if (username == "user") username = "匿名玩家"; this.firstRender.children[i].getChildByName("name").getComponent(cc.Label).string = username + ""; this.firstRender.children[i].getChildByName("rank").getComponent(cc.Label).string = this.topData[i - 3].addLevel; if (this.topData[i - 3].useravatar == "" || this.topData[i - 3].useravatar == null || this.topData[i - 3].useravatar == undefined ) { // this.firstRender.children[i].getChildByName("icon").getComponent(cc.Sprite).spriteFrame = this.defaultsprite; } else if (this.topData[i - 3].useravatar == "0" || this.topData[i - 3].useravatar == "1" || this.topData[i - 3].useravatar == "2" || this.topData[i - 3].useravatar == "3" || this.topData[i - 3].useravatar == "4" || this.topData[i - 3].useravatar == "5" || this.topData[i - 3].useravatar == "6" || this.topData[i - 3].useravatar == "7" || this.topData[i - 3].useravatar == "8" || this.topData[i - 3].useravatar == "9" || this.topData[i - 3].useravatar == "10" || this.topData[i - 3].useravatar == "11" || this.topData[i - 3].useravatar == "12" || this.topData[i - 3].useravatar == "13" || this.topData[i - 3].useravatar == "14" || this.topData[i - 3].useravatar == "15" || this.topData[i - 3].useravatar == "16" || this.topData[i - 3].useravatar == "17" ) { let useravatar = this.topData[i - 3].useravatar; let useravatarTemp = "icon_" + useravatar; // console.log("222头像名称", useravatarTemp, "333用户名字:", username); this.firstRender.children[i].getChildByName("mask").getChildByName("icon").getComponent(cc.Sprite).spriteFrame = this.UI.getSpriteFrame(useravatarTemp); } else this.setPic(this.topData[i - 3].useravatar, this.firstRender.children[i].getChildByName("mask").getChildByName("icon")); } } } public setPic(url, node) { // this.node.getChildByName("pic").getChildByName("icon").active = false; // this.node.getChildByName("pic").active = false; var self = this; // let url = this.data.useravatar; cc.assetManager.loadRemote(url, { ext: '.png' }, (err, texture: cc.Texture2D) => { if (texture) { // node.getChildByName("pic").active = true; var sprite = node.getComponent(cc.Sprite); sprite.spriteFrame = new cc.SpriteFrame(texture); } else { } }) } /**销毁 */ public onDestroy() { //清理列表项 let len = this.itemList.length; for (let i = 0; i < len; i++) { if (cc.isValid(this.itemList[i], true)) { this.itemList[i].destroy(); } } this.itemList.length = 0; //清理对象池 len = this.itemPool.length; for (let i = 0; i < len; i++) { if (cc.isValid(this.itemPool[i], true)) { this.itemPool[i].destroy(); } } this.itemPool.length = 0; //清理列表数据 this.itemDataList.length = 0; } /** * 将滑动列表滑动回到最顶端 * @param duration 动画持续时间,单位秒,默认0.3秒 * @param easing 缓动函数,默认使用平滑缓动 */ public backTop(duration: number = 0.3, easing?: string) { if (!this.scrollView || !this.content) { console.warn('ScrollView或Content未初始化'); return; } // 计算回到顶部的位置 let targetPosition: cc.Vec2; if (this.type == ListType.Vertical) { // 垂直列表:将content的y坐标设置为0(最顶端) targetPosition = new cc.Vec2(this.content.x, 0); } else if (this.type == ListType.Horizontal) { // 水平列表:将content的x坐标设置为0(最左端) targetPosition = new cc.Vec2(0, this.content.y); } else { console.warn('不支持Grid类型的回到顶部操作'); return; } // 使用scrollTo方法平滑滚动到顶部 this.scrollView.scrollToOffset(targetPosition, duration, false); console.log('回到顶部操作执行完成'); } /** * 获取当前滚动位置 * @returns 返回当前滚动位置的百分比(0-1) */ public getScrollPosition(): number { if (!this.scrollView || !this.content) { return 0; } if (this.type == ListType.Vertical) { const maxScrollY = Math.max(0, this.content.height - this.scrollView.node.height); if (maxScrollY <= 0) return 0; return Math.abs(this.content.y) / maxScrollY; } else if (this.type == ListType.Horizontal) { const maxScrollX = Math.max(0, this.content.width - this.scrollView.node.width); if (maxScrollX <= 0) return 0; return Math.abs(this.content.x) / maxScrollX; } return 0; } /** * 查找并滚动到指定城市名的子节点 * @param cityName 要查找的城市名,如"北京" * @param duration 动画持续时间,单位秒,默认0.5秒 * @param position 目标位置,0表示屏幕顶部,0.5表示屏幕中间,1表示屏幕底部,默认0.5(屏幕中间) */ public scrollToCity(cityName: string, duration: number = 0.5, position: number = 0.5): boolean { if (!this.scrollView || !this.content) { console.warn('ScrollView或Content未初始化'); return false; } if (this.type !== ListType.Vertical) { console.warn('目前只支持垂直列表的滚动到城市功能'); return false; } // 循环遍历itemDataList查找目标城市 let targetIndex = -1; for (let i = 0; i < this.itemDataList.length; i++) { if (this.itemDataList[i] && this.itemDataList[i].name === cityName) { targetIndex = i; break; } } if (targetIndex === -1) { console.warn(`未找到城市名为"${cityName}"的列表项`); return false; } console.log(`找到城市"${cityName}",索引位置:${targetIndex}`); // 计算目标城市在列表中的位置 const itemHeight = this.itemHeight; const spaceY = this.spaceY; const paddingTop = this.padding_top; // 计算firstRender的高度偏移 let firstRenderHeight = this.firstRender ? this.firstRender.height : 0; let firstRenderOffsetY = firstRenderHeight > 0 ? firstRenderHeight + this.spaceY : 0; // 计算目标城市在content中的Y坐标(相对于content顶部) let targetYInContent = -itemHeight * (0.5 + targetIndex) - spaceY * targetIndex - paddingTop - firstRenderOffsetY; // 计算目标城市在屏幕中的期望位置 const scrollViewHeight = this.scrollView.node.height; const contentHeight = this.content.height; // 计算目标城市在屏幕中的期望Y坐标(相对于scrollView顶部) let targetYInView = -targetYInContent - (scrollViewHeight * position); // 限制滚动范围 const maxScrollY = Math.max(0, contentHeight - scrollViewHeight); targetYInView = Math.max(0, Math.min(maxScrollY, targetYInView)); // 获取当前滚动偏移量 const currentOffset = this.scrollView.getScrollOffset(); // 计算相对偏移量(从当前位置到目标位置的差值) const relativeOffsetY = targetYInView - currentOffset.y; // 计算目标偏移量(当前位置 + 相对偏移量) const targetOffset = new cc.Vec2(0, currentOffset.y + relativeOffsetY); // 根据滚动距离动态调整动画时间(距离越大,时间越长) const distance = Math.abs(relativeOffsetY); const baseDuration = 0.2; // 基础动画时间 const maxDuration = 1; // 最大动画时间 const adjustedDuration = Math.min(maxDuration, baseDuration + (distance / 1000) * 0.5); console.log(`当前偏移量: ${currentOffset.y}, 目标偏移量: ${targetOffset.y}, 相对偏移量: ${relativeOffsetY}, 距离: ${distance}`); // 执行滚动 this.scrollView.scrollToOffset(targetOffset, adjustedDuration, false); console.log(`滚动到城市"${cityName}",目标位置:${position},动画时间:${adjustedDuration}秒`); return true; } /** * 查找并滚动到北京的子节点(屏幕中间) * @param duration 动画持续时间,单位秒,默认0.5秒 */ public scrollToBeijing(duration: number = 0.5): boolean { return this.scrollToCity("北京", duration, 0.5); } /** * 查找并滚动到北京的子节点(屏幕顶部) * @param duration 动画持续时间,单位秒,默认0.5秒 */ public scrollToBeijingTop(duration: number = 0.5): boolean { return this.scrollToCity("北京", duration, 0); } /** * 查找并滚动到北京的子节点(屏幕底部) * @param duration 动画持续时间,单位秒,默认0.5秒 */ public scrollToBeijingBottom(duration: number = 0.5): boolean { return this.scrollToCity("北京", duration, 1); } }