cb/assets/career/script/CareerList.ts
2025-12-04 14:17:59 +08:00

670 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<any> = [];
/**应创建的实例数量 */
private spawnCount: number = 0;
/**存放列表项实例的数组 */
private itemList: Array<cc.Node> = [];
/**item的高度 */
private itemHeight: number = 0;
/**item的宽度 */
private itemWidth: number = 0;
/**存放不再使用中的列表项 */
private itemPool: Array<cc.Node> = [];
//======================= 计算参数 ==========================
/**距离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<any>, 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; //计算bufferZoneitem的显示范围
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") {
let useravatar = this.topData[i - 3].useravatar;
let useravatarTemp = "icon_" + useravatar;
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);
}
}