/** * @Author huangxin * @ctime 2020-06-10 * @Version * ScrollFinal1.0 2020-06-10 beta升级,使用挂载预制体 * ScrollFinal1.1 2020-08-11 新增对象池类大法 * ScrollFinal1.2 2020-08-17 1.新增延迟刷新item,防止在某一帧生成过量item导致卡顿的问题 * 2.初始的控件属性设置(init方法)改为由initScrollView触发(原本的onLoad触发在适配模式下会不准确) * ScrollFinal1.3 2021-05-26 现在的滚动节点不需要手动添加scrollView组件了 * ScrollFinal1.4 2022-04-14 现在可以在初始化或refresh时,立刻滚动到某个位置了 * ScrollFinal1.5 2023-06-22 新增 adapterItem , 可在index首次出现时设定它的高度(后续把动态修改补上) * @Tips * 复用滚动轴 用来减少drawcall * 与cc.ScrollView组件一同挂载在一个节点上 * item挂载的脚本必须添加setData方法,用来传递数据 * item锚点应该在中心 * 目前Grid类型只支持从左上到右下模式(垂直滚动),其他奇葩模式自己搞定 * 滚动轴的锚点必须放在滚动列表的起始位置(比如背包grid模式在左上角,成就列表在左上角) * * @adapterItem item如有不定高度时,编辑器中设定的item需要为最小高度,确保instance的个数最大值是正确的 * 支持不规则高度item 谨用,有缺陷: * 1.同一个item再次设置不同高度时会出现问题(若要修改,需要做一个链表来关联前后item) * 2.仅支持垂直和水平模式,背包模式不支持(水平模式未测试) */ import { gameMethod } from "../common/gameMethod"; import { CC_NODE_EVENT } from "../data/const/TypeConst"; import ScrollInner from "./ScrollInner"; import ScrollOut from "./ScrollOut"; // 对象池类型 let PoolEnum = cc.Enum({ /**通用道具 */ ITEM_BASE: 0, /**背包道具 */ ITEM_BAG: 1, // /**合成道具 */ // ITEM_COMPOSE: 2, }) // 滚动类型 let ScrollDirEnum = cc.Enum({ /**垂直*/ VERTICAL: 0, /**水平*/ HORIZONTAL: 1, /**背包*/ GRID: 2 }) // 滚动类型 let ScrollOutInner = cc.Enum({ /**外层scroll*/ DEFAULT: 0, /**外层scroll*/ OUT: 1, /**内层scroll*/ INNER: 2, }) interface AdapterList { x: number, y: number, width: number, height: number } type ItemList = { index: number, node: cc.Node } const { ccclass, property, menu } = cc._decorator; @ccclass @menu('Scroll/ScrollFinal') export default class ScrollFinal extends cc.Component { // @property(cc.ScrollView) // scroll: cc.ScrollView | null = null @property({ tooltip: "使用对象池" }) useNodePool: boolean = false @property({ type: PoolEnum, visible: function () { return this.useNodePool }, tooltip: "对象池类型" }) private poolType = PoolEnum.ITEM_BASE @property({ type: ScrollDirEnum, visible: () => { return true }, tooltip: "滚动类型" }) private scrollDir = ScrollDirEnum.VERTICAL // 滚动类型 @property({ type: ScrollOutInner, tooltip: "0=>常规scroll\n1=>外层scroll\n2=>内层scroll" }) private outInner = ScrollOutInner.DEFAULT @property({ tooltip: "与滚动层的边界-左" }) padingX: number = 10 @property({ tooltip: "与滚动层的边界-上" }) padingY: number = 10 @property({ tooltip: "与滚动层的边界-下" }) padingY2: number = 0 @property({ visible: function () { return this.scrollDir != ScrollDirEnum.VERTICAL }, tooltip: "item行间距" }) spacingX: number = 20 @property({ visible: function () { return this.scrollDir != ScrollDirEnum.HORIZONTAL }, tooltip: "item列间距" }) spacingY: number = 20 @property(cc.Prefab) itemPrefab: cc.Prefab = null // item资源加载地址 @property itemScript: string = "" // item挂在的脚本名 @property itemScale: number = 1 // item缩放比例 @property({ tooltip: "是否开启滚动惯性" }) inertia: boolean = true @property({ tooltip: "开启惯性后,在用户停止触摸后滚动多块停止,0表示永不停止,1表示立即停止", visible: function () { return this.inertia == true }, }) brake: number = 0.75 @property({ tooltip: "是否允许滚动内容超过边界,并在停止触摸后回弹" }) elastic: boolean = true @property({ tooltip: "滚动行为是否会取消子节点上注册的触摸事件", }) cancelInnerEvents: boolean = true @property({ tooltip: "不支持背包,默认不激活", // 激活后,无法在初始化时直接滚动到未展示过的index标签位置,即 scrollToIndexNow会受到限制 displayName: "不固定item尺寸" }) adapterItem: boolean = false adapterList: { [idx: string]: AdapterList } = {} // 记录适配的坐标列表 adapterContentLength: number = 0 // 激活适配后的content高度 // @property({ // tooltip: "启动widget模式(注意激活后不可与原生cc.widget同时使用)" // }) // useWidget: boolean = false // @property({ tooltip: "适配顶部", visible: function () { return this.useWidget == true } }) // useAlignTop: boolean = false // @property({ tooltip: "距离父节点顶部", visible: function () { return this.useAlignTop == true } }) // widgetTop: number = 0 // @property({ tooltip: "适配底部", visible: function () { return this.useWidget == true } }) // useAlignBottom: boolean = false // @property({ tooltip: "距离父节点底部", visible: function () { return this.useAlignBottom == true } }) // widgetBottom: number = 0 // @property({ tooltip: "适配左侧", visible: function () { return this.useWidget == true } }) // useAlignLeft: boolean = false // @property({ tooltip: "距离父节点左侧", visible: function () { return this.useAlignLeft == true } }) // widgetLeft: number = 0 // @property({ tooltip: "适配右侧", visible: function () { return this.useWidget == true } }) // useAlignRight: boolean = false // @property({ tooltip: "距离父节点右侧", visible: function () { return this.useAlignRight == true } }) // widgetRight: number = 0 @property({ tooltip: "展示生产动画\n0->不展示\n1->缩放动画\n2->x方向压扁拉伸\n3->y方向压扁拉伸\n4->慢缩放" }) showAnim: number = 0 @property({ tooltip: "单个动画播放速度\n最佳播放速度参考:\n缩放动画->0.1\nx方向压扁拉伸->0.1\ny方向压扁拉伸->0.25\n慢缩放->0.3" }) animSpeed: number = 0.15 @property({ tooltip: "创建item的延迟时间,设为0则为每帧生成。\n注意:在所有item刷新完之前,scroll组件的滚动功能将被关闭" }) ctime: number = 0 @property({ tooltip: "每帧生成item的个数" }) cnumber: number = 1 scrollView: cc.ScrollView | ScrollOut | ScrollInner mask: cc.Node content: cc.Node isScrollUp: boolean = false // 当前往哪个方向滚动 左和上是true private _itemDataList: any[] = [] // 当前显示阵营的所有数据 private extraParams: any[] = [] // 额外数据 private itemList: ItemList[] = [] // 实例化的item列表 private instantiateCount: number = 0 // item实例化数量 private hangCount: number = 0 // 行个数 private lieCount: number = 0 // 列个数 private itemDistanceX: number = 0 // item中心点之间的距离 private itemDistanceY: number = 0 // item中心点之间的距离 private scrollMaxOffsetX: number = 0 // 最大可滚动区域X private scrollMaxOffsetY: number = 0 // 最大可滚动区域Y private scrollIndex: number = -1 // scroll参数 private lastScrollPos: number = 0 //上一次的滚动位置 private curScrollPos: number = 0 // 当前滚动位置 private itemWidth: number = 10 // item宽度 private itemHeight: number = 10 // item高度 private tagLang: number = 0 private tagIndex: number = -999 // 999 表示清0状态,此时无【插入标签】,-1表示标签置顶,其他即当前标签的下方(右侧)显示 private canCreateItem: boolean = false // 可以生成item private createIndex: number = 0 // 生成item的数据标签 private life: number = 0 // 生成item的时间 private baseIndex: number = 0 // 基础标签位置 private hasInit: boolean = false onLoad() { // if (this.node.getComponent(cc.ScrollView) != null) { // console.error("滚动节点无需挂载scrollView组件了") // return // } this.init() } resetSize() { if (this.mask) { this.mask.setContentSize(this.node.getContentSize()) } if (this.content) { // this.content.setContentSize(this.node.getContentSize()) this.setScrollContentSize() } this.setInstantCount() } private init() { if (this.hasInit) { return } /////////////// 构建滚动轴 /////////////// this.scrollView = this.outInner == ScrollOutInner.DEFAULT ? this.addComponent(cc.ScrollView) : this.outInner == ScrollOutInner.OUT ? this.addComponent(ScrollOut) : this.addComponent(ScrollInner) // this.scrollView = this.addComponent(cc.ScrollView) this.scrollView.horizontal = this.scrollDir == ScrollDirEnum.HORIZONTAL this.scrollView.vertical = this.scrollDir != ScrollDirEnum.HORIZONTAL this.scrollView.inertia = this.inertia this.scrollView.brake = this.brake this.scrollView.elastic = this.elastic this.scrollView.cancelInnerEvents = this.cancelInnerEvents /////////////// 检测是否需要重新适配 /////////////// if (this.node.getComponent(cc.Widget)) { this.node.getComponent(cc.Widget).updateAlignment() } /////////////// 构建滚动遮罩 /////////////// this.mask = new cc.Node() this.mask.parent = this.node this.mask.name = "scrollMask" this.mask.setContentSize(this.node.getContentSize()) this.mask.addComponent(cc.Widget) this.mask.getComponent(cc.Widget).isAlignTop = true this.mask.getComponent(cc.Widget).isAlignBottom = true this.mask.getComponent(cc.Widget).top = 0 this.mask.getComponent(cc.Widget).bottom = 0 this.mask.addComponent(cc.Mask) this.mask.getComponent(cc.Mask).type = cc.Mask.Type.RECT this.mask.anchorX = this.node.anchorX this.mask.anchorY = this.node.anchorY this.mask.x = 0 this.mask.y = 0 // widget不需要加了 // let maskWidget = mask.addComponent(cc.Widget) // maskWidget.isAlignTop = true // maskWidget.isAlignBottom = true // maskWidget.isAlignLeft = true // maskWidget.isAlignRight = true // maskWidget.top = 0 // maskWidget.bottom = 0 // maskWidget.left = 0 // maskWidget.right = 0 /////////////// 构建滚动内容器 /////////////// this.content = new cc.Node() this.content.parent = this.mask this.content.name = "scrollContent" this.scrollView.content = this.content this.content.setContentSize(this.node.getContentSize()) this.content.anchorX = this.node.anchorX this.content.anchorY = this.node.anchorY this.content.x = 0 this.content.y = 0 this.hasInit = true this.itemWidth = this.itemPrefab.data.getContentSize().width this.itemHeight = this.itemPrefab.data.getContentSize().height // SCROLL_BOUNCE_BOTTOM = 'bounce-bottom',//滚动视图滚动到顶部边界并且开始回弹时发出的事件 // SCROLL_BOUNCE_LEFT = 'bounce-left',//滚动视图滚动到底部边界并且开始回弹时发出的事件 // SCROLL_BOUNCE_RIGHT = 'bounce-right',//滚动视图滚动到左边界并且开始回弹时发出的事件 // SCROLL_BOUNCE_TOP = 'bounce-top',//滚动视图滚动到右边界并且开始回弹时发出的事件 this.scrollView.node.on(CC_NODE_EVENT.SCROLLING, this.onScroll, this) this.scrollView.node.on(CC_NODE_EVENT.SCROLL_BEGAN, this.onScrollBegan, this) this.scrollView.node.on(CC_NODE_EVENT.SCROLL_BOUNCE_TOP, this.onBounceTop, this) this.scrollView.node.on(CC_NODE_EVENT.SCROLL_BOUNCE_BOTTOM, () => { }, this) this.scrollView.node.on(CC_NODE_EVENT.SCROLL_BOUNCE_LEFT, () => { }, this) this.scrollView.node.on(CC_NODE_EVENT.SCROLL_BOUNCE_RIGHT, () => { }, this) this.scrollView.node.on(CC_NODE_EVENT.SCROLL_BOUNCE_TOP, () => { }, this) this.itemDistanceX = this.realItemWidth + this.spacingX this.itemDistanceY = this.realItemHeight + this.spacingY this.setInstantCount() // //看下有没有不应该有的组件 // // Layout // if (this.scrollView.content.getComponent(cc.Layout)) { // console.error("scrollFinal 与 layout 冲突,清删除 content 中的 layout 组件") // } // // Widget // if (this.scrollView.node.getComponent(cc.Widget)) { // if (this.scrollView.node.getComponent(cc.Widget).isAlignTop && // this.scrollView.node.getComponent(cc.Widget).isAlignBottom) { // console.error("不能用widget做长度适配(因为Widget的延迟),只可用作坐标适配") // } // } } private setInstantCount() { if (this.scrollView == null) { return } switch (this.scrollDir) { case ScrollDirEnum.VERTICAL: this.hangCount = 1 this.lieCount = Math.ceil(this.scrollView.node.height / (this.itemDistanceY)) + 1 this.instantiateCount = this.lieCount break case ScrollDirEnum.HORIZONTAL: this.hangCount = Math.ceil(this.scrollView.node.width / (this.itemDistanceX)) + 1 this.lieCount = 1 this.instantiateCount = this.hangCount break case ScrollDirEnum.GRID: this.hangCount = Math.floor(this.scrollView.node.width / (this.itemDistanceX)) this.lieCount = Math.ceil(this.scrollView.node.height / (this.itemDistanceY)) + 1 this.instantiateCount = this.hangCount * this.lieCount break } } initScrollView(list: any[] = [], ...args: any[]) { // this.scheduleOnce(() => { this.init() this.clear() this.clearTag() this.adapterList = {} this.itemDataList = list this.extraParams = args if (this.outInner == ScrollOutInner.OUT) { this.extraParams.push(this.scrollView) } this.scrollView.stopAutoScroll() this.showUI() // }, 0) } set itemDataList(list: any[]) { this._itemDataList = list if (!this.adapterItem) { return } // 适配模式,需要去掉记录的信息 let newAdapterList: { [idx: string]: AdapterList } = {} for (let index = 0; index < list.length; index++) { if (this.adapterList[index]) { newAdapterList[index] = this.adapterList[index] } } this.adapterList = newAdapterList switch (this.scrollDir) { case ScrollDirEnum.VERTICAL: this.adapterContentLength = 0 for (const idx in this.adapterList) { this.adapterContentLength = Math.max(Math.abs(this.adapterList[idx].y) + this.adapterList[idx].height / 2) } break case ScrollDirEnum.HORIZONTAL: this.adapterContentLength = 0 for (const idx in this.adapterList) { this.adapterContentLength = Math.max(this.adapterList[idx].x + this.adapterList[idx].width / 2) } break } } get itemDataList(): any[] { return this._itemDataList } private getPositionInView(item: cc.Node): { x: number, y: number } { if (this.scrollView == null) { return cc.v2(0, 0) } let worldPos = item.parent.convertToWorldSpaceAR(item.position); let viewPos = this.scrollView.node.convertToNodeSpaceAR(worldPos); return viewPos; } private onBounceTop() { switch (this.scrollDir) { case ScrollDirEnum.HORIZONTAL: break case ScrollDirEnum.VERTICAL: case ScrollDirEnum.GRID: this.checkScrollState() if (this.adapterItem) { // @TODO } break } this.scrollIndex = -1 } private onScroll() { this.curScrollPos = 0 if (this.scrollDir == ScrollDirEnum.HORIZONTAL) { this.curScrollPos = this.scrollView.getScrollOffset().x this.isScrollUp = this.curScrollPos < this.lastScrollPos } else { this.curScrollPos = this.scrollView.getScrollOffset().y this.isScrollUp = this.curScrollPos > this.lastScrollPos } this.lastScrollPos = this.curScrollPos if (this.scrollView == null) { return } this.itemList.forEach(ele => { this.tryResetItem(ele) }) } private tryResetItem(ele: ItemList) { switch (this.scrollDir) { case ScrollDirEnum.HORIZONTAL: if (this.curScrollPos >= 0 || this.curScrollPos <= this.scrollMaxOffsetX) { return } break case ScrollDirEnum.VERTICAL: case ScrollDirEnum.GRID: if (this.curScrollPos <= -this.realItemHeight / 2 || this.curScrollPos >= this.scrollMaxOffsetY + this.realItemHeight / 2) { return } } let scrollWidth = this.scrollView.node.width let scrollHeight = this.scrollView.node.height let element = ele.node switch (this.scrollDir) { case ScrollDirEnum.HORIZONTAL: if (this.isScrollUp && this.getPositionInView(element).x < -(scrollWidth * this.node.anchorX + ele.node.width / 2)) { // 超出左边界显示区域 let idx = ele.index + this.instantiateCount if (idx < this.itemDataList.length) { this.setItemData(element, this.itemDataList[idx], idx) // element.x = element.x + this.hangCount * ele.node.width ele.index = idx this.setPosX(idx, element) } } else if (!this.isScrollUp && this.getPositionInView(element).x > scrollWidth * (1 - this.node.anchorX) + ele.node.width / 2) { // 超出右边界显示区域 let idx = ele.index - this.instantiateCount if (idx >= 0) { this.setItemData(element, this.itemDataList[idx], idx) // element.x = element.x - this.hangCount * ele.node.width ele.index = idx this.setPosX(idx, element) } } break case ScrollDirEnum.VERTICAL: case ScrollDirEnum.GRID: if (this.isScrollUp && this.getPositionInView(element).y > scrollHeight * (1 - this.node.anchorY) + ele.node.height / 2) { // 超出上边界显示区域 let idx = ele.index + this.instantiateCount if (idx < this.itemDataList.length && this.isScrollUp) { this.setItemData(element, this.itemDataList[idx], idx) ele.index = idx this.setPosY(idx, element) } } else if (!this.isScrollUp && this.curScrollPos > -ele.node.height / 2 && this.getPositionInView(element).y < -(scrollHeight * this.node.anchorY + ele.node.height / 2)) { // 超出下边界显示区域 let idx = ele.index - this.instantiateCount if (idx >= 0 && this.isScrollUp == false) { this.setItemData(element, this.itemDataList[idx], idx) ele.index = idx this.setPosY(idx, element) } } break } } private setPosX(index: number, node?: cc.Node): number { let x = 0 if (this.adapterItem) { if (this.adapterList[index] && this.adapterList[index].x != null) { if (node) { node.x = this.adapterList[index].x } return this.adapterList[index].x } else { let lastX = index == 0 ? 0 : this.adapterList[index - 1].x let lastWidth = index == 0 ? 0 : this.adapterList[index - 1].width // 上一个坐标 + 当前node偏移量 switch (this.scrollDir) { case ScrollDirEnum.VERTICAL: x = this.scrollView.content.width / 2 + this.padingX//this.realItemWidth / 2 + this.padingX break case ScrollDirEnum.HORIZONTAL: x += node.width / 2 + this.padingX + lastX + lastWidth / 2 + this.spacingX this.adapterContentLength = Math.max(x + node.width / 2) this.setScrollContentSize() break case ScrollDirEnum.GRID: x = index % this.hangCount * this.itemDistanceX + this.realItemWidth / 2 + this.padingX break } if (this.adapterList[index] == null) { this.adapterList[index] = { x: null, y: null, width: null, height: null } } this.adapterList[index].x = x this.adapterList[index].width = node.width if (node) { node.x = x } return x } } else { switch (this.scrollDir) { case ScrollDirEnum.VERTICAL: x = this.scrollView.content.width / 2 + this.padingX//this.realItemWidth / 2 + this.padingX break case ScrollDirEnum.HORIZONTAL: x = index * this.itemDistanceX + this.realItemWidth / 2 + this.padingX if (this.tagIndex >= -1 && index > this.tagIndex) { x += this.tagLang } break case ScrollDirEnum.GRID: x = index % this.hangCount * this.itemDistanceX + this.realItemWidth / 2 + this.padingX break } if (node) { node.x = x } return x } } private setPosY(index: number, node?: cc.Node): number { let y = 0 if (this.adapterItem) { if (this.adapterList[index] && this.adapterList[index].y != null) { if (node) { node.y = this.adapterList[index].y } return this.adapterList[index].y } else { let lastY = index == 0 ? 0 : this.adapterList[index - 1].y let lastHeight = index == 0 ? 0 : this.adapterList[index - 1].height switch (this.scrollDir) { case ScrollDirEnum.VERTICAL: y = lastY - this.padingY - node.height / 2 - lastHeight / 2 - this.spacingY this.adapterContentLength = Math.max(Math.abs(y) + node.height / 2) this.setScrollContentSize() // y = -index * this.itemDistanceY - this.realItemHeight / 2 - this.padingY // if (this.tagIndex >= -1 && index > this.tagIndex) { // y -= this.tagLang // } break case ScrollDirEnum.HORIZONTAL: y = -this.scrollView.content.height / 2 + this.padingY//-this.realItemHeight / 2 - this.padingY break case ScrollDirEnum.GRID: y = -Math.floor((index) / this.hangCount) * this.itemDistanceY - this.realItemHeight / 2 - this.padingY if (this.tagIndex >= -1 && (Math.floor(index / this.hangCount)) > this.tagIndex) { y -= this.tagLang } break } if (this.adapterList[index] == null) { this.adapterList[index] = { x: null, y: null, width: null, height: null } } this.adapterList[index].y = y this.adapterList[index].height = node.height if (node) { node.y = y } return y } } else { switch (this.scrollDir) { case ScrollDirEnum.VERTICAL: y = -index * this.itemDistanceY - this.realItemHeight / 2 - this.padingY if (this.tagIndex >= -1 && index > this.tagIndex) { y -= this.tagLang } break case ScrollDirEnum.HORIZONTAL: y = -this.scrollView.content.height / 2 + this.padingY//-this.realItemHeight / 2 - this.padingY break case ScrollDirEnum.GRID: y = -Math.floor((index) / this.hangCount) * this.itemDistanceY - this.realItemHeight / 2 - this.padingY if (this.tagIndex >= -1 && (Math.floor(index / this.hangCount)) > this.tagIndex) { y -= this.tagLang } break } if (node) { node.y = y } return y } } private setItemData(itemNode: cc.Node, data: any, index: number) { try { itemNode.getComponent(this.itemScript).setData(data, index, this.extraParams) } catch (error) { console.error("脚本中缺少setData方法,或者方法报错", error) } } // refreshItems调用,在刷新时可能需要重置item的index标签 private resetIndex(index: number): number { if (this.itemDataList[index] != null) { return index } return this.resetIndex(index - this.instantiateCount) } // 刷新单独的item refreshItem(index: number, data) { if (this.itemDataList[index] == null) { return } this.itemDataList[index] = data if (this.getItem(index) == null) { return } this.setItemData(this.getItem(index), this.itemDataList[index], index) } // refreshItem(index: number) { // this.setItemData(this.getItem(index), this.itemDataList[index], index) // } refreshItems(itemDataList: any[], ...args: any[]) { this.itemDataList = itemDataList if (args.length > 0) { this.extraParams = args } if (this.outInner == ScrollOutInner.OUT) { this.extraParams.push(this.scrollView) } this.fixItemNodes() // 最终构造完整的 itemList 列表,刷新数据 this.itemList.forEach(element => { try { let newIndex = this.resetIndex(element.index) element.index = newIndex this.setItemData(element.node, this.itemDataList[element.index], element.index) this.setPosX(element.index, element.node) this.setPosY(element.index, element.node) // element.node.getComponent(this.itemScript).setData(this.itemDataList[element.index], element.index, this.extraParams) } catch (error) { console.warn("脚本中缺少refreshItem方法,或者方法报错", error) } }) } // 数据新增或减少时,增加或减少item fixItemNodes() { // 判断是否需要删除 itemList 里的数据 if (this.itemDataList.length < this.instantiateCount && this.itemList.length > this.itemDataList.length) { let needDeleteCount = this.itemList.length - this.itemDataList.length for (let index = this.itemDataList.length; index < this.itemDataList.length + needDeleteCount; index++) { this.itemList[index].node.destroy() } this.itemList.splice(this.itemDataList.length, needDeleteCount) // 判断是否需要增加 itemList 里的数据 } else if (this.itemList.length < this.instantiateCount && this.itemList.length < this.itemDataList.length) { let addCount = Math.min(this.instantiateCount - this.itemList.length, this.itemDataList.length - this.itemList.length) let startIndex = 0 this.itemList.forEach(element => { startIndex = Math.max(element.index + 1, startIndex) }); for (let addIndex = startIndex; addIndex < (startIndex + addCount); addIndex++) { this.addItemNode(addIndex, this.itemDataList[addIndex], true) } } if (this.content) { this.setScrollContentSize() } } private get realItemWidth(): number { return this.itemWidth * this.itemScale } private get realItemHeight(): number { return this.itemHeight * this.itemScale } private initItemNode(): cc.Node { if (this.useNodePool) { // @TODO PoolManager return cc.instantiate(this.itemPrefab) // if (this.poolType == PoolEnum.ITEM_BAG) { // return PoolManager.getItemBag(this.itemPrefab) // } else if (this.poolType == PoolEnum.ITEM_BASE) { // return PoolManager.getItemBase(this.itemPrefab) // } } else { return cc.instantiate(this.itemPrefab) } } // back private onScrollBegan() { this.scrollIndex = 0 } private showUI() { if (this.itemPrefab == null) { console.error("item预制体加载失败") return } if (this.scrollView == null) { console.error("没有绑定scroll") return } this.setCreateItems(true) this.scrollIndex = -1 this.scrollView.content.setAnchorPoint(0, 1) this.scrollView.content.setPosition(-this.scrollView.node.width / 2, this.scrollView.node.height / 2) this.setScrollContentSize() switch (this.scrollDir) { case ScrollDirEnum.VERTICAL: this.scrollView.scrollToTop() break case ScrollDirEnum.HORIZONTAL: this.scrollView.scrollToLeft() break case ScrollDirEnum.GRID: this.scrollView.scrollToTop() break } } private setCreateItems(bool: boolean) { if (bool) { this.scrollView.enabled = false this.canCreateItem = true this.createIndex = 0 this.baseIndex = 0 this.life = 0 } else { this.scrollView.enabled = true this.canCreateItem = false this.createIndex = 0 this.baseIndex = 0 this.life = 0 } } update(dt: number) { if (this.scrollIndex >= 0) { this.scrollIndex += dt } if (!this.canCreateItem) { return } for (let index = 0; index < this.cnumber; index++) { this.updateForCreateItem(dt) } } private updateForCreateItem(dt: number) { if (!this.canCreateItem) { return } if (this.life == 0) { // 生 let _itemData = this.itemDataList[this.baseIndex] if (this.createIndex >= this.instantiateCount || _itemData == null) { this.setCreateItems(false) return } this.addItemNode(this.baseIndex, _itemData) this.createIndex += 1 this.baseIndex += 1 } this.life += dt if (this.life >= this.ctime) { this.life = 0 } } setScrollContentSize() { if (this.adapterItem) { switch (this.scrollDir) { case ScrollDirEnum.VERTICAL: this.scrollView.content.setContentSize(this.scrollView.node.width, this.adapterContentLength) break case ScrollDirEnum.HORIZONTAL: this.scrollView.content.setContentSize(this.adapterContentLength, this.scrollView.node.height) break case ScrollDirEnum.GRID: this.scrollView.content.setContentSize(this.scrollView.node.width, this.itemDistanceY * Math.ceil(this.itemDataList.length / this.hangCount) + this.padingY + this.tagLang + this.padingY2) break } } else { switch (this.scrollDir) { case ScrollDirEnum.VERTICAL: this.scrollView.content.setContentSize(this.scrollView.node.width, this.itemDistanceY * this.itemDataList.length + this.padingY + this.tagLang + this.padingY2 - this.spacingY) break case ScrollDirEnum.HORIZONTAL: this.scrollView.content.setContentSize(this.itemDistanceX * this.itemDataList.length + this.padingX + this.tagLang - this.spacingX, this.scrollView.node.height) break case ScrollDirEnum.GRID: this.scrollView.content.setContentSize(this.scrollView.node.width, this.itemDistanceY * Math.ceil(this.itemDataList.length / this.hangCount) + this.padingY + this.tagLang + this.padingY2 - this.spacingY) break } } this.scrollMaxOffsetX = -this.scrollView.getMaxScrollOffset().x this.scrollMaxOffsetY = this.scrollView.getMaxScrollOffset().y // console.log("---重新设置了滚动区域", this.scrollView.content.height) } /** * 弹出详情标签,将会重设后续item的坐标。 * @param index item标签 * @param lang 坐标偏移量 */ setTag(index: number, lang: number) { this.tagLang = lang switch (this.scrollDir) { case ScrollDirEnum.VERTICAL: case ScrollDirEnum.HORIZONTAL: this.tagIndex = index break case ScrollDirEnum.GRID: this.tagIndex = Math.floor(index / this.hangCount) break } this.setScrollContentSize() this.itemList.forEach((element) => { this.setPosX(element.index, element.node) this.setPosY(element.index, element.node) }); } /**清除详情标签,恢复item默认坐标 */ clearTag() { this.setTag(-999, 0) // 修正index和坐标 let scrollHeight = this.scrollView.node.height this.itemList.forEach(ele => { // 判断是否超出边界 if (this.getPositionInView(ele.node).y > scrollHeight * (1 - this.node.anchorY) + this.itemDistanceY / 2) { if (ele.index + this.instantiateCount < this.itemDataList.length) { ele.index += this.instantiateCount } this.setPosY(ele.index, ele.node) this.setItemData(ele.node, this.itemDataList[ele.index], ele.index) } else if (this.getPositionInView(ele.node).y < -(scrollHeight * this.node.anchorY + this.itemDistanceY / 2)) { if (ele.index - this.instantiateCount >= 0) { ele.index -= this.instantiateCount } this.setPosY(ele.index, ele.node) this.setItemData(ele.node, this.itemDataList[ele.index], ele.index) } }) this.setScrollContentSize() } /**删除某个元素 */ del(index: number) { if (this.itemDataList.length < index) { return } this.itemDataList.splice(index, 1) // // 判断下是否需要删除节点 if (this.itemList.length > this.itemDataList.length) { this.itemList.pop().node.destroy() } this.setScrollContentSize() for (let index = 0; index < this.itemList.length; index++) { let element = this.itemList[index] if (this.itemDataList[element.index] == null) { element.index -= this.instantiateCount break } } this.itemList.forEach((element) => { this.setPosX(element.index, element.node) this.setPosY(element.index, element.node) this.setItemData(element.node, this.itemDataList[element.index], element.index) }); } /**在末尾添加一个元素 */ add(data: any) { this.itemDataList.push(data) // 判断是否需要增加item if (this.itemList.length >= this.instantiateCount) { // 不需要 // 判断下是否需要把最前面的放到最后面:1 let needUpdate = false for (let index = 0; index < this.itemList.length; index++) { if (this.itemList[index].index == this.itemDataList.length - 2) { needUpdate = true break } } if (needUpdate) { let minIndex = 1000 //这个是最小值 let minUpdateIndex = 0// 这个是最小值的标签 for (let i = 0; i < this.itemList.length; i++) { if (this.itemList[i].index < minIndex) { minIndex = this.itemList[i].index minUpdateIndex = i } } // 判断下是否需要把最前面的放到最后面:2 let _nodePos = 0 let _offset = 0 let needUpdate2 = false switch (this.scrollDir) { case ScrollDirEnum.HORIZONTAL: _nodePos = this.itemList[minUpdateIndex].node.x _offset = this.scrollView.getScrollOffset().x needUpdate2 = _nodePos + _offset > this.itemWidth / 2 break case ScrollDirEnum.VERTICAL: case ScrollDirEnum.GRID: _nodePos = this.itemList[minUpdateIndex].node.y _offset = this.scrollView.getScrollOffset().y needUpdate2 = _nodePos + _offset > this.itemHeight / 2 break } if (needUpdate2) { this.itemList[minUpdateIndex].index += this.instantiateCount this.setItemData(this.itemList[minUpdateIndex].node, data, this.itemList[minUpdateIndex].index) this.setPosX(this.itemList[minUpdateIndex].index, this.itemList[minUpdateIndex].node) this.setPosY(this.itemList[minUpdateIndex].index, this.itemList[minUpdateIndex].node) } } } else { // 需要 let index = this.itemDataList.length - 1 this.addItemNode(index, data) } this.setScrollContentSize() } addItemNode(index: number, data: any, isRefresh: boolean = false) { let _node = this.initItemNode() // add放在前面.先激活onLoad方法,再走setData this.scrollView.content.addChild(_node) this.setItemData(_node, data, index) this.itemList.push({ index: index, node: _node }) this.setPosX(index, _node) this.setPosY(index, _node) if (isRefresh || this.showAnim == 0) { _node.scale = this.itemScale } else if (this.showAnim == 1) { _node.scale = 0 cc.tween(_node). to(this.animSpeed, { scale: this.itemScale + 0.1 }). to(this.animSpeed, { scale: this.itemScale }). start() } else if (this.showAnim == 2) { _node.scale = this.itemScale let delayTime = index % this.hangCount / 20 + 0.1 _node.x -= _node.width / 2 _node.scaleX = 0 cc.tween(_node).delay(delayTime).to(this.animSpeed, { x: _node.x + _node.width / 2, scaleX: this.itemScale }).start() } else if (this.showAnim == 3) { //@TODO _node.scale = this.itemScale let delayTime = index % this.hangCount / 20 + 0.1 _node.y += _node.height / 2 _node.scaleY = 0 cc.tween(_node).delay(delayTime).to(this.animSpeed, { y: _node.y - _node.height / 2, scaleY: this.itemScale }).start() } else if (this.showAnim == 4) { _node.scale = 0 cc.tween(_node). to(this.animSpeed, { scale: this.itemScale }). start() } else { _node.scale = this.itemScale } } /** * @param val 立刻滚动到目标标签,目标标签将在顶部|左侧出现 * @param type 滚到哪里的类型(1.居中 2.顶部|左侧 3.底部|右侧) */ scrollToIndexNow(val: number, type: number = 1, ...args: any[]) { if (this.adapterItem && this.adapterList[val] == null) { console.warn("未展示过,无法移动") return } if (val >= this.itemDataList.length) { val = this.itemDataList.length - 1 } if (val < 0) { this.refreshItems(this.itemDataList) return } this.scrollView.stopAutoScroll() this.baseIndex = val let scrollPosIndex = this.baseIndex if (this.itemDataList.length - this.baseIndex < this.instantiateCount) { this.baseIndex -= this.instantiateCount - (this.itemDataList.length - this.baseIndex) } else { if (type == 1) { this.baseIndex -= Math.ceil(this.instantiateCount / 2) } else if (type == 3) { this.baseIndex -= (this.lieCount - 1) * this.hangCount } } if (this.scrollDir == ScrollDirEnum.GRID) { this.baseIndex -= this.baseIndex % this.hangCount } this.baseIndex = Math.max(this.baseIndex, 0) let _itemWidth = this.adapterItem ? this.adapterList[val].width : this.itemWidth let _itemHeight = this.adapterItem ? this.adapterList[val].height : this.itemHeight // 从第几个标签开始显示 switch (this.scrollDir) { case ScrollDirEnum.HORIZONTAL: let _x = -(this.setPosX(scrollPosIndex) - (_itemWidth * this.itemScale + this.spacingX) / 2) // 判断是否超过边界 if (_x < this.scrollMaxOffsetX) { scrollPosIndex = this.itemDataList.length - this.instantiateCount this.scrollView.content.x = this.scrollMaxOffsetX } else { if (type == 1) { this.scrollView.content.x = Math.min(-(this.setPosX(scrollPosIndex) - this.scrollView.node.width / 2), 0) if (-(this.setPosX(scrollPosIndex) - this.scrollView.node.width / 2) >= 0) { this.baseIndex = 0 } } else if (type == 2) { this.scrollView.content.x = -(this.setPosX(scrollPosIndex) - (_itemWidth * this.itemScale + this.spacingX) / 2) } else { scrollPosIndex++ this.scrollView.content.x = Math.min(-(this.setPosX(scrollPosIndex) - this.scrollView.node.width - _itemWidth / 2 - this.spacingY), 0) if (-(this.setPosX(scrollPosIndex) - this.scrollView.node.width - _itemWidth / 2 - this.spacingY) >= 0) { this.baseIndex = 0 } } } break case ScrollDirEnum.VERTICAL: case ScrollDirEnum.GRID: let _y = (Math.abs(this.setPosY(scrollPosIndex)) - (_itemHeight * this.itemScale + this.spacingY) / 2) if (_y > this.scrollMaxOffsetY) { scrollPosIndex = this.itemDataList.length - this.instantiateCount this.scrollView.content.y = this.scrollMaxOffsetY } else { if (type == 1) { this.scrollView.content.y = Math.max(Math.abs(this.setPosY(scrollPosIndex)) - this.scrollView.node.height / 2, 0) if (Math.abs(this.setPosY(scrollPosIndex)) - this.scrollView.node.height / 2 <= 0) { this.baseIndex = 0 } } else if (type == 2) { this.scrollView.content.y = Math.abs(this.setPosY(scrollPosIndex)) - (_itemHeight * this.itemScale + this.spacingY) / 2 } else { scrollPosIndex = this.scrollDir == ScrollDirEnum.VERTICAL ? (scrollPosIndex + 1) : scrollPosIndex + this.hangCount this.scrollView.content.y = Math.max(Math.abs(this.setPosY(scrollPosIndex)) - this.scrollView.node.height - _itemHeight / 2 - this.spacingY, 0) if (Math.abs(this.setPosY(scrollPosIndex)) - this.scrollView.node.height - _itemHeight / 2 - this.spacingY <= 0) { this.baseIndex = 0 } } } break } if (args.length > 0) { this.extraParams = args } this.fixItemNodes() // 如果content有子项目,则重制目标点位置 if (this.itemList.length > 0) { for (let index = 0; index < this.itemList.length; index++) { // let i = this.baseIndex + index let elemnet = this.itemList[index] elemnet.index = this.baseIndex + index this.setPosX(elemnet.index, elemnet.node) this.setPosY(elemnet.index, elemnet.node) if (gameMethod.isEmpty(this.itemDataList[elemnet.index])) { continue } this.setItemData(elemnet.node, this.itemDataList[elemnet.index], elemnet.index) } } } /** * 尝试滚动到滚动视图中心 * @param index 标签 * @param type 滚到哪里的类型(1.居中 2.顶部|左侧 3.底部|右侧) * @param time */ scrollToIndex(index: number, type: number = 1, time: number = 1, offsetY: number = 0) { if (this.itemDataList[index] == null) { console.error("不存在此标签") return } switch (this.scrollDir) { case ScrollDirEnum.HORIZONTAL: if (type == 1) { this.scrollView.scrollToOffset(cc.v2(this.setPosX(index) - this.scrollView.node.width / 2, this.scrollView.getScrollOffset().y), time) } else if (type == 2) { this.scrollView.scrollToOffset(cc.v2(this.setPosX(index) - this.itemDistanceX / 2, this.scrollView.getScrollOffset().y), time) } else { index++ this.scrollView.scrollToOffset(cc.v2(this.setPosX(index) - this.scrollView.node.width, this.scrollView.getScrollOffset().y - this.itemWidth / 2 - this.spacingX), time) } break case ScrollDirEnum.VERTICAL: case ScrollDirEnum.GRID: if (type == 1) { this.scrollView.scrollToOffset(cc.v2(this.scrollView.getScrollOffset().x, Math.abs(this.setPosY(index)) - this.scrollView.node.height / 2 - offsetY), time) } else if (type == 2) { this.scrollView.scrollToOffset(cc.v2(this.scrollView.getScrollOffset().x, Math.abs(this.setPosY(index)) - this.itemDistanceY / 2 - offsetY), time) } else { index = this.scrollDir == ScrollDirEnum.VERTICAL ? (index + 1) : index + this.hangCount this.scrollView.scrollToOffset(cc.v2(this.scrollView.getScrollOffset().x, Math.abs(this.setPosY(index)) - this.scrollView.node.height - this.itemHeight / 2 - this.spacingY - offsetY), time) } break } // this.scheduleOnce(() => { // this.refreshItem(index) // }, time) } getItem(index: number): cc.Node | null { for (const key in this.itemList) { if (this.itemList[key].index == index) { return this.itemList[key].node } } return } checkScrollState() { if (this.scrollIndex > this.itemDataList.length) { this.scrollView.stopAutoScroll() this.scrollView.scrollToOffset(cc.v2(0, this.scrollMaxOffsetY / 2)) } } // 使用对象池时,在切换界面时必须使用这个方法,将所有对象放到池中 clear() { // @TODO PoolManager if (this.scrollView && this.scrollView.content) { this.scrollView.content.removeAllChildren() } // if (this.useNodePool) { // this.itemList.forEach(element => { // if (this.poolType == PoolEnum.ITEM_BAG) { // PoolManager.putItemBag(element.node) // } else if (this.poolType == PoolEnum.ITEM_BASE) { // PoolManager.putItemBase(element.node) // } // }); // } else { // if (this.scrollView && this.scrollView.content) { // this.scrollView.content.removeAllChildren() // } // } this.itemDataList = [] this.itemList = [] } }