const { ccclass, property } = cc._decorator; enum Direction { vertical = 1, horizontal, } /** 动态加载的滚动列表 * - 滑动列表动态加载 * - 支持多种类型预制体混合滚动 * * - item脚本需要有函数 setItemData 用于接收数据 * * - 多个prefab混合时,要为每条item指定 pfbType * * - item、content锚点y需要为1其他为0.5 * @author 薛鸿潇 */ @ccclass export default class TableView extends cc.Component { @property({ displayName: '预制条目', tooltip: '脚本必须实现 setItemData 方法用于接收数据!', type: [cc.Prefab] }) private pre_item: cc.Prefab[] = []; @property({ displayName: '脚本名', tooltip: '脚本必须实现 setItemData 方法用于接收数据!', type: [cc.String] }) private scr_item: string[] = []; /** 间距 */ @property({ displayName: '间距' }) private itemInterval: number = 0; @property({ displayName: '显示隐藏节点', tooltip: '一般是顶部标识可向下滑动的小图标', type: cc.Node }) private node_xiabiao: cc.Node = null; @property({ displayName: '上面的节点是否根据指定数据的长度来控制显示,优先级最高' }) private isShowByLimitLength: boolean = false; @property({ displayName: '长度值', visible() { return this.isShowByLimitLength } }) private limitLength: number = 0; @property({ displayName: '滚动方向', tooltip: '', type: cc.Enum(Direction) }) private direction = Direction.vertical; @property({ displayName: "初始化时所有子节点隐藏" }) private isHideAllChild: boolean = false; @property({ displayName: '数据长度限制', tooltip: '总数据小于限制长度调大间距' }) private limitDataLen: number = -1; @property({ displayName: '间距增加数值', visible() { return this.limitDataLen != -1 } }) private addOffset: number = 10; /** 数据 */ private itemData: any = null; /** 滚动列表组件 */ private scrollView: cc.ScrollView = null; /** item预制体组 */ private item: Array = null; /** 滑动列表的内容节点 */ private content: cc.Node = null; /** 列表高 */ private layerHeight: any = null; /** 列表宽 */ private layerWith: any = null; /** ? */ private firstItemIndex: number = 0; private lastItemIndex: number = 0; private firstItemData: any = {}; private lastItemData: any = {}; private itemArr: Array = []; private itemNode: Array = []; private itemPosMap: any = new Map(); private initItemData: boolean = true; private count: number = 0; private itemCanMoveDown: boolean = null; private itemCanMoveUp: boolean = null; onLoad() { this.initPro(); if (this.direction == Direction.vertical) { this.scrollView.vertical = true; this.scrollView.horizontal = false; } else { this.scrollView.vertical = false; this.scrollView.horizontal = true; } if (this.node_xiabiao) this.node_xiabiao.active = false; // 测试 // let itemData = []; // for (let i = 0; i < 20; i++) { // let test_data = { // pfbType: 0, // } // itemData.push(test_data); // } // this.init(itemData); } private initPro() { this.scrollView = this.node.getComponent(cc.ScrollView); this.content = this.scrollView.content; this.item = this.pre_item; this.layerHeight = this.node.height; this.layerWith = this.node.width; for (let i = 0; i < this.item.length; i++) { this.itemNode[i] = cc.instantiate(this.item[i]); } this.node.on('srollview-init', this.init, this); this.node.on('srollview-update', this.updateList, this); } /** * @param itemData 列表数据 * @param isSkipInit 跳过初始化 * @returns */ public init(itemData: Array, isSkipInit: boolean = false) { if (!itemData[0]) { return false; } this.clearItem(); this.itemData = itemData; //item数据 this.firstItemIndex = 0; this.lastItemIndex = 0; this.firstItemData = {}; this.lastItemData = {}; this.itemArr = []; this.itemPosMap = new Map(); this.initItemData = true; this.count = 0; if (isSkipInit) return; if (this.limitDataLen != -1) { if (this.itemData.length <= this.limitDataLen) { this.itemInterval += this.addOffset; } } // this.itemNode = []; // for (let i = 0; i < this.item.length; i++) { // this.itemNode[i] = cc.instantiate(this.item[i]); // } // cc.log(this.testTime(0));// 消耗计时 this.initItem(); this.initItemPos(0); if (this.isHideAllChild) this.isHideAllChild = false; this.scrollView.node.on('scrolling', this.callback, this); // cc.log('tableView结束:' + this.testTime()); //水平居中 if (this.direction == Direction.horizontal) { if (this.content.width < this.layerWith) { this.node.x = (this.layerWith - this.content.width) * 0.5 + this.itemInterval / 2; } } } /** * 刷新列表 * - 仅支持使用1种预制体时,更新data数据 * @param itemData */ public updateList(itemData: Array) { this.itemData = itemData; const old_count = this.itemArr.length; for (let i = 0; i < old_count; i++) { if (this.itemData[i]) { if (this.direction == Direction.vertical) { if (!this.itemArr[i]) { let y = 0; if (i > 0) { y = this.itemArr[i - 1].y - this.itemArr[i - 1].height - this.itemInterval;// 下一条目纵坐标 } let item_node = this.addItemNode(i, y); this.updateContentHeigh(this.itemArr[i].height - item_node.y); } } else { if (!this.itemArr[i]) { let x = 0; if (i > 0) { x = this.itemArr[i - 1].x + this.itemArr[i - 1].width + this.itemInterval;// 下一条目纵坐标 } let item_node = this.addItemNode(i, x); this.updateContentHeigh(this.itemArr[i].width + item_node.x); } } let comp_script = this.itemArr[i].getComponent(this.scr_item[this.itemData[i].pfbType || 0]); comp_script && comp_script.setItemData(this.itemData[i], this); } else { this.itemArr[i].destroy(); this.itemArr[i] = null; } } } update(dt) { if (!this.node_xiabiao) return; if (this.isShowByLimitLength) { if (this.itemData && this.itemData.length <= this.limitLength) { return; } } if (this.content.height > this.layerHeight) { if (this.scrollView.getScrollOffset().y >= this.scrollView.getMaxScrollOffset().y) { if (this.node_xiabiao.active == true) this.node_xiabiao.active = false; } else { if (this.node_xiabiao.active == false) this.node_xiabiao.active = true; } } else { if (this.node_xiabiao.active == true) this.node_xiabiao.active = false; } } private initItemPos(index: number) { let item_data_count = this.itemData.length; if (this.direction == Direction.vertical) { for (let i = index; i < item_data_count; i++) { let obj: any = {} if (i === 0) { obj.startPos = 0; } else { obj.startPos = this.itemPosMap.get(i - 1).endPos; } let j = this.itemData[i].pfbType || 0; obj.endPos = obj.startPos + this.itemNode[j].height + this.itemInterval; this.itemPosMap.set(i, obj); } } else { for (let i = index; i < item_data_count; i++) { let obj: any = {} if (i === 0) { obj.startPos = 0; } else { obj.startPos = this.itemPosMap.get(i - 1).endPos; } let j = this.itemData[i].pfbType || 0; obj.endPos = obj.startPos + this.itemNode[j].width + this.itemInterval; this.itemPosMap.set(i, obj); } } if (item_data_count > 0) { if (this.direction == Direction.vertical) { this.updateContentHeigh(this.itemPosMap.get(item_data_count - 1).endPos); } else { this.updateContentWidth(this.itemPosMap.get(item_data_count - 1).endPos); } } } /** * 实例化所有用到的item,控制实例化item的数目,暂定超出两个 */ private initItem() { let j = 0; if (this.direction == Direction.vertical) { for (let i = 0; i < this.itemData.length; i++) { if (this.content.height > this.layerHeight) { j++ if (j > 2) { break; } } let y = 0; if (i > 0) { y = this.itemArr[i - 1].y - this.itemArr[i - 1].height - this.itemInterval;// 下一条目纵坐标 } let item_node = this.addItemNode(i, y); this.updateContentHeigh(this.itemArr[i].height - item_node.y); } } else { for (let i = 0; i < this.itemData.length; i++) { if (this.content.width > this.layerWith) { j++ if (j > 2) { break; } } let x = 0; if (i > 0) { x = this.itemArr[i - 1].x + this.itemArr[i - 1].width + this.itemInterval;// 下一条目纵坐标 } let item_node = this.addItemNode(i, x); this.updateContentWidth(this.itemArr[i].width + item_node.x); } } } /** * 添加条目节点 * @param i 编号 * @param y 坐标y */ private addItemNode(i: number, xOrY: number) { let pfbType = this.itemData[i].pfbType || 0; let item = cc.instantiate(this.itemNode[pfbType]); item.parent = this.content; item.pfbType = pfbType; item.index = i; if (this.isHideAllChild) item.active = false; if (this.direction == Direction.vertical) { if (i === 0) { item.y = 0; } else { item.y = xOrY; } item.x = 0; } else { if (i === 0) { item.x = 0; } else { item.x = xOrY; } item.y = 0; } //对item赋值 let comp_script = item.getComponent(this.scr_item[pfbType]); comp_script && comp_script.setItemData(this.itemData[i], this); this.itemArr.push(item); return item; // cc.log('生成itemNode' + i); } /** * 更新centent高 * @param num */ private updateContentHeigh(num: number) { // this.content.height = num > this.layerHeight ? num : this.layerHeight; this.content.height = num; if (this.content.height <= this.layerHeight) { this.scrollView.vertical = false; } else { this.scrollView.vertical = true; } // cc.log('滚动条高度:', this.content.height); } /** * 更新centent宽 * @param num */ private updateContentWidth(num: number) { // this.content.width = num > this.layerWith ? num : this.layerWith; this.content.width = num; if (this.content.width <= this.layerWith) { this.scrollView.horizontal = false; } else { this.scrollView.horizontal = true; } // cc.log('滚动条高度:', this.content.height); } /** * 触摸滚动条的函数回调 * @param event * @param eventType * @returns */ private callback(event, eventType) { // cc.log(event && event.type || eventType) if (this.direction == Direction.vertical) { if (this.content.height > this.layerHeight) { let firstItemPos = this.scrollView.getScrollOffset().y; let lastItemPos = firstItemPos + this.layerHeight; if (firstItemPos < 0) return; if (this.initItemData) { // cc.log('111:%o', this.itemPosMap) this.initItemData = false; this.updateFirstItemIndex(); this.itemCanMoveDown = true; this.updateLastItemIndex(); this.itemCanMoveDown = false; } //超出边界直接返回. //滚动条向上滑动可能会触发的函数 if (firstItemPos > this.firstItemData.endPos) { if (this.lastItemIndex + 1 < this.itemData.length) { this.updateFirstItemIndex(); } this.count++; } if (lastItemPos > this.lastItemData.endPos) { if (this.lastItemIndex + 1 < this.itemData.length) { this.itemCanMoveDown = true; this.updateLastItemIndex(); this.itemCanMoveDown = false; } } //滚动条向下滑动可能会触发的函数 if (lastItemPos < this.lastItemData.startPos) { this.updateLastItemIndex(); this.count--; } if (firstItemPos < this.firstItemData.startPos) { this.itemCanMoveUp = true; this.updateFirstItemIndex(); this.itemCanMoveUp = false; } } } else { if (this.content.width > this.layerWith) { let firstItemPos = -this.scrollView.getScrollOffset().x; let lastItemPos = firstItemPos + this.layerWith; if (firstItemPos < 0) return; if (this.initItemData) { // cc.log('111:%o', this.itemPosMap) this.initItemData = false; this.updateFirstItemIndex(); this.itemCanMoveDown = true; this.updateLastItemIndex(); this.itemCanMoveDown = false; } //超出边界直接返回. //滚动条向上滑动可能会触发的函数 if (firstItemPos > this.firstItemData.endPos) { if (this.lastItemIndex + 1 < this.itemData.length) { this.updateFirstItemIndex(); } this.count++; } if (lastItemPos > this.lastItemData.endPos) { if (this.lastItemIndex + 1 < this.itemData.length) { this.itemCanMoveDown = true; this.updateLastItemIndex(); this.itemCanMoveDown = false; } } //滚动条向下滑动可能会触发的函数 if (lastItemPos < this.lastItemData.startPos) { this.updateLastItemIndex(); this.count--; } if (firstItemPos < this.firstItemData.startPos) { this.itemCanMoveUp = true; this.updateFirstItemIndex(); this.itemCanMoveUp = false; } } } } private updateFirstItemIndex() { let num = this.firstItemIndex; if (this.itemCanMoveUp && num > this.getItemIndex()[0] && num > 0) { this.itemMoveUp(this.firstItemIndex - 1); } this.updateItemIndex(); } private updateLastItemIndex() { let num = this.lastItemIndex; if (this.itemCanMoveDown && num < this.getItemIndex()[1] && num + 1 < this.itemData.length) { this.itemMoveDown(this.lastItemIndex + 1); } this.updateItemIndex(); } private updateItemIndex() { //cc.log(this.firstItemIndex, this.lastItemIndex, this.itemArr, this.itemData) } /** * 得到滚动条此时状态下应有的itemNode元素,包括滚动条上方一个,滚动条下方一个 * @returns */ private getItemIndex() { let arr = []; if (this.direction == Direction.vertical) { let firstItemPos = this.scrollView.getScrollOffset().y; let lastItemPos = firstItemPos + this.layerHeight; // console.log('firstItemPos 22 ', firstItemPos) // console.log('lastItemPos 22 ', lastItemPos) this.itemPosMap.forEach((value, key) => { let status1 = value.startPos <= firstItemPos && value.endPos > firstItemPos; let status2 = value.startPos >= firstItemPos && value.endPos < lastItemPos; let status3 = value.startPos <= lastItemPos && value.endPos > lastItemPos; if (status1) { this.firstItemData.startPos = value.startPos; this.firstItemData.endPos = value.endPos; this.firstItemIndex = key; arr.push(key); } if (status3) { this.lastItemData.startPos = value.startPos; this.lastItemData.endPos = value.endPos; this.lastItemIndex = key; arr.push(key); } }) } else { let firstItemPos = -this.scrollView.getScrollOffset().x; let lastItemPos = firstItemPos + this.layerWith; // console.log('firstItemPos ', firstItemPos) // console.log('lastItemPos ', lastItemPos) this.itemPosMap.forEach((value, key) => { let status1 = value.startPos <= firstItemPos && value.endPos > firstItemPos; let status2 = value.startPos >= firstItemPos && value.endPos < lastItemPos; let status3 = value.startPos <= lastItemPos && value.endPos > lastItemPos; if (status1) { this.firstItemData.startPos = value.startPos; this.firstItemData.endPos = value.endPos; this.firstItemIndex = key; arr.push(key); } if (status3) { this.lastItemData.startPos = value.startPos; this.lastItemData.endPos = value.endPos; this.lastItemIndex = key; arr.push(key); } }) } // console.log('arr >>>>> ', arr) return arr; } /** * 滚动到顶部【滚动条顺序是从上到下开始遍历】 * @param num * @returns */ private itemMoveUp(num: number) { if (num < 0 || this.lastItemIndex + 1 < num || num + 1 > this.itemData.length) { return; } if (!this.hasItem(num)) { if (this.direction == Direction.vertical) { this.itemMove(num, -this.itemPosMap.get(num).startPos); } else { this.itemMove(num, this.itemPosMap.get(num).startPos); } } num++; return this.itemMoveUp(num); } /** 滚动到底部 */ public itemMoveDown(num: number) { if (num < 0 || this.firstItemIndex - 1 > num || num + 1 > this.itemData.length) { return; } if (!this.hasItem(num)) { if (this.direction == Direction.vertical) { this.itemMove(num, -this.itemPosMap.get(num).startPos); } else { this.itemMove(num, this.itemPosMap.get(num).startPos); } } num--; return this.itemMoveDown(num); } /** * 判断指定index位置是否存在itemNode * @param index 节点下标 * @returns */ private hasItem(index: number) { for (let i = 0; i < this.itemArr.length; i++) { if (this.itemArr[i].index === index) { return true; } } return false; } /** * 移动条目 * 逻辑判断,第一种情况,修改itemArr数组的某个对象,第二种情况实例化一个新itemNode * @param index 条目节点标记 * @param y * @returns */ private itemMove(index: number, xOrY: number) { for (let i = 0; i < this.itemArr.length; i++) { //index存在-1的情况,类似于在缓存池里的item. let status1 = this.itemArr[i].index < this.firstItemIndex - 1 ? true : false; let status2 = this.itemArr[i].index > this.lastItemIndex + 1 ? true : false; let status3 = this.itemArr[i].pfbType === (this.itemData[index].pfbType || 0); //cc.log('item的索引', this.firstItemIndex, this.lastItemIndex) if (status1 && status3 || status2 && status3) { //cc.log(i, index, this.itemArr, this.content.height); //给item赋值还有设置位置 this.itemArr[i].index = index; if (this.direction == Direction.vertical) { this.itemArr[i].y = xOrY; } else { this.itemArr[i].x = xOrY; } let comp_script = this.itemArr[i].getComponent(this.scr_item[this.itemArr[i].pfbType]); comp_script && comp_script.setItemData(this.itemData[index], this); return; } } this.addItemNode(index, xOrY); } /** * 内容滚动到指定下标位置 * @param index 条目index */ public contentMoveByIndex(index: number, duration: number = 0.5) { let pos_xOrY = this.itemPosMap.get(index).startPos; if (this.direction == Direction.vertical) { this.scrollView.scrollToOffset(new cc.Vec2(this.content.x, pos_xOrY), duration); } else { this.scrollView.scrollToOffset(new cc.Vec2(pos_xOrY, this.content.y), duration); } } public getItemByIndexAndCount(index: number, count: number) { const itemArray: Array = []; for (let j = 0; j != this.itemArr.length; ++j) { let isCheckSuccess = false; for (let i = index; i != (index + count); ++i) { if (this.itemArr[j].index == i) { itemArray.push(this.itemArr[j]); isCheckSuccess = true; break; } } // if (!isCheckSuccess) { // this.itemArr[j].active = true; // } } return itemArray; } public getFrontPositionItemByCount(count: number) { const itemArray: Array = []; for (let i = 0; i != this.itemArr.length; ++i) { if (i < count) { itemArray.push(this.itemArr[i]); } else { //this.itemArr[i].active = true; } } return itemArray; } /** * 得到相关位置的排序index * 方法待验证,可能有问题 * @param pos 坐标 * @returns */ public getPosIndex(pos: cc.Vec2) { for (let [key, value] of this.itemPosMap.entries()) { if (value.endPos > pos && value.startPos <= pos) { return key; } } } /** * 添加条目 * @param obj 数据对象 * @returns */ public addItem(obj: any) { this.itemData.push(obj); this.initItemPos(this.itemData.length - 1); let endPos = this.itemPosMap.get(this.itemData.length - 1).endPos; let layerHOrW = 0; if (this.direction == Direction.vertical) { layerHOrW = this.layerHeight; } else { layerHOrW = this.layerWith; } if (endPos - layerHOrW > 0) { let startPos = endPos - layerHOrW; //得到当前的firstItemIndex; for (let i = this.itemData.length - 1; i >= 0; i--) { if (this.itemPosMap.get(i).endPos > startPos && this.itemPosMap.get(i).startPos <= startPos) { this.firstItemIndex = i; } } if (this.direction == Direction.vertical) { this.scrollView.scrollToBottom(); } else { this.scrollView.scrollToRight(); } this.lastItemIndex = this.itemData.length - 1; let num = this.firstItemIndex - 1 > 0 ? (this.firstItemIndex - 1) : 0; this.itemMoveUp(num); return true; } else { this.firstItemIndex = 0; this.lastItemIndex = this.itemData.length - 1; this.itemMoveUp(this.firstItemIndex); return false; } } /** * 清理条目 */ public clearItem() { this.itemData = []; this.itemPosMap.clear(); this.scrollView.scrollToTop(); this.content.height = 0; for (let i in this.itemArr) { // this.itemArr[i].index = -1; // this.itemArr[i].y = 3000; this.itemArr[i].destroy(); } } /** * 删除指定条目 * @param i 节点.index * @returns */ public deleteItem(i: number) { this.itemData.splice(i, 1); const item_data_count = this.itemData.length; if (item_data_count <= 0) return; this.initItemPos(item_data_count - 1); //改变this.itemArr的内容 for (let j = 0; j < this.itemArr.length; j++) { if (this.direction == Direction.vertical) { if (this.itemArr[j].index === i) { this.itemArr[j].index = -1; this.itemArr[j].y = 3000; } if (this.itemArr[j].index > i) { let num = this.itemArr[j].index; this.itemArr[j].y = -this.itemPosMap.get(num - 1).startPos; this.itemArr[j].index = num - 1; } } else { if (this.itemArr[j].index === i) { this.itemArr[j].index = -1; this.itemArr[j].x = 3000; } if (this.itemArr[j].index > i) { let num = this.itemArr[j].index; this.itemArr[j].x = this.itemPosMap.get(num - 1).startPos; this.itemArr[j].index = num - 1; } } } this.updateContentHeigh(this.itemPosMap.get(this.itemData.length - 1).endPos); if (!this.lastItemIndex) this.getItemIndex();// 初始化时没有初始化此值,会导致为0.不能创建新的item,此处校验 this.itemMoveUp(this.firstItemIndex);// } /** * 重置指定item * @param index 节点下标 * @param item_data 新的数据 */ public resetItemData(index: number, item_data?: any) { for (let i = 0; i < this.itemArr.length; i++) { if (this.itemArr[i].index === index) { if (item_data) this.itemData[i] = item_data; let comp_script = this.itemArr[i].getComponent(this.scr_item[this.itemArr[i].pfbType]); comp_script && comp_script.setItemData(this.itemData[index], this); break; } } } private lastResetItemInfoHeight: any = null; private lastResetItemIndex: any = null; /** * 重置条目大小 * @param index 节点下标 * @param infoHeight 新的高度 */ public resetItemSize(index: number, infoHeight: number) { let func = (function (index, infoHeight) { for (let i = 0; i < this.itemArr.length; i++) { if (this.itemArr[i].index > index) { if (this.direction == Direction.vertical) { this.itemArr[i].y -= infoHeight; } else { this.itemArr[i].x += infoHeight; } } } for (let [key, value] of this.itemPosMap.entries()) { if (key === index) { value.endPos += infoHeight; } if (key > index) { value.endPos += infoHeight; value.startPos += infoHeight; } } this.lastResetItemInfoHeight = infoHeight; this.lastResetItemIndex = index; }).bind(this); if (this.lastResetItemIndex !== null && this.lastResetItemInfoHeight) { if (this.lastResetItemIndex === index) { func(this.lastResetItemIndex, -this.lastResetItemInfoHeight); this.lastResetItemIndex = null; this.lastResetItemInfoHeight = 0; } else { func(this.lastResetItemIndex, -this.lastResetItemInfoHeight); func(index, infoHeight); } } else { func(index, infoHeight); } this.itemMoveUp(this.firstItemIndex); this.updateContentHeigh(this.itemPosMap.get(this.itemData.length - 1).endPos); } }