MouseUISuperLayout.ts 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736
  1. /*
  2. * @Author: steveJobs
  3. * @Email: icipiqkm@gmail.com
  4. * @Date: 2020-11-19 01:15:52
  5. * @Last Modified by: steveJobs
  6. * @Last Modified time: 2021-01-23 18:05:39
  7. * @Description: 名词说明 什么是一组item?
  8. * 垂直模式
  9. * 1,2,3 一组item包含 1,2,3 1是一组item中header 也是整个列表的header 3是一组item中footer 9是整个列表的footer
  10. * 4,5,6
  11. * 7,8,9
  12. * 调用 isGroupHeader传入 1节点 返回true 调用 isGroupFooter传入 3节点返回true
  13. * 调用 getGroupLeftX 传入 2节点 返回1节点位置X getGroupRightX 返回3节点位置X
  14. * 调用 getGroupBottomY 传入 5节点 返回8节点位置Y getGroupTopY 返回2节点位置Y
  15. * 水平模式
  16. * |1|,4,7 一组item包含 1,2,3 1是一组item中header 也是整个列表的header 3是一组item中footer 9是整个列表的footer
  17. * |2|,5,8
  18. * |3|,6,9
  19. */
  20. import MouseUISpuerScrollView from './MouseUISuperScrollView';
  21. import MouseUISpuerItem from './MouseUISuperItem';
  22. const { ccclass, property, menu } = cc._decorator;
  23. const EPSILON = 1e-4;
  24. export const MouseUIChangeBrotherEvnet = "MouseUIChangeBrotherEvnet"
  25. export enum MouseUISuperAxis {
  26. HORIZONTAL = 0,
  27. VERTICAL = 1
  28. }
  29. export enum MouseUISuperDirection {
  30. HEADER_TO_FOOTER = 0,
  31. FOOTER_TO_HEADER = 1,
  32. }
  33. @ccclass
  34. @menu("MouseUISuperLayout")
  35. export default class MouseUISuperLayout extends cc.Component {
  36. /**刷新Item事件 */
  37. public static REFRESH_ITEM: string = "UISuperLayout_REFRESH_ITEM";
  38. @property({ type: cc.Enum(MouseUISuperAxis), displayName: "排列方向" }) startAxis: MouseUISuperAxis = MouseUISuperAxis.VERTICAL
  39. @property({ type: cc.Enum(MouseUISuperDirection), displayName: "排列子节点的方向" }) direction: MouseUISuperDirection = MouseUISuperDirection.HEADER_TO_FOOTER
  40. @property({ displayName: "上边距" }) paddingTop: number = 0
  41. @property({ displayName: "下边距" }) paddingBottom: number = 0
  42. @property({ displayName: "左边距" }) paddingLeft: number = 0
  43. @property({ displayName: "右边距" }) paddingRight: number = 0
  44. @property({ displayName: "间隔" }) spacing: cc.Vec2 = cc.Vec2.ZERO
  45. @property({ displayName: "每组item个数", tooltip: "单行的列数 或 单列的行数" }) column: number = 2
  46. @property({ displayName: "item创建倍率", tooltip: "相对于view的尺寸 默认2倍" }) multiple: number = 2
  47. @property({ type: cc.Prefab, displayName: "item Prefab" }) prefab: cc.Prefab = null
  48. @property({ displayName: "头部滑动循环" }) headerLoop: boolean = false
  49. @property({ displayName: "尾部滑动循环" }) footerLoop: boolean = false
  50. @property affectedByScale: boolean = true
  51. //@property(cc.Component.EventHandler) refreshItemEvents: cc.Component.EventHandler[] = []
  52. private _gener: Generator
  53. private _isinited: boolean = false
  54. private _maxPrefabTotal: number = 0
  55. private _children: cc.Node[] = [] //和this.node.children 保持同步
  56. private _viewSize: cc.Size
  57. private _scrollView: MouseUISpuerScrollView = null
  58. private _maxItemTotal: number = 0
  59. private _prevLayoutPosition: cc.Vec2 = cc.Vec2.ZERO
  60. /** 当前的滚动是否是由 scrollTo 方法执行的 和touch滑动做个区分*/
  61. public scrollToHeaderOrFooter: boolean = false
  62. /** 根据上一次和本次的坐标变化计算滑动方向 */
  63. private get layoutDirection(): cc.Vec2 {
  64. let pos = cc.Vec2.ZERO
  65. if (this.vertical) {
  66. pos.y = this.node.y - this._prevLayoutPosition.y
  67. } else {
  68. pos.x = this.node.x - this._prevLayoutPosition.x
  69. }
  70. this._prevLayoutPosition = this.node.getPosition()
  71. return pos
  72. }
  73. /** 是否是向下滑动 */
  74. private get isScrollToFooter(): boolean {
  75. if (this.vertical) {
  76. return this.layoutDirection.y < 0
  77. } else {
  78. return this.layoutDirection.x > 0
  79. }
  80. }
  81. /** 自己维护的子节点数组 和this.node.children 保持同步 */
  82. public get children() { return this._children }
  83. /** 最大数据总数 */
  84. public get maxItemTotal() { return this._maxItemTotal }
  85. /** 当前被创建的item总数 */
  86. public get maxPrefabTotal() { return this._maxPrefabTotal }
  87. /** scrollView.view尺寸 */
  88. public get viewSize(): cc.Size {
  89. return this.scrollView.view.getContentSize()
  90. }
  91. /** 是否是垂直模式 */
  92. public get vertical(): boolean {
  93. return this.startAxis == MouseUISuperAxis.VERTICAL
  94. }
  95. /** 是否是水平模式 */
  96. public get horizontal(): boolean {
  97. return this.startAxis == MouseUISuperAxis.HORIZONTAL
  98. }
  99. /** 是否是正序排列 */
  100. public get headerToFooter(): boolean {
  101. return this.direction == MouseUISuperDirection.HEADER_TO_FOOTER
  102. }
  103. /** 是否是倒序排列 */
  104. public get footerToHeader(): boolean {
  105. return this.direction == MouseUISuperDirection.FOOTER_TO_HEADER
  106. }
  107. /** 水平间隔总宽度 (Grid 模式返回多个间隔总宽度) */
  108. public get spacingWidth() {
  109. return this.spacing.x * (this.column - 1)
  110. }
  111. /** 水平间隔总高度 (Grid 模式返回多个间隔总高度) */
  112. public get spacingHeight() {
  113. return this.spacing.y * (this.column - 1)
  114. }
  115. /** 可容纳item的真实宽度 */
  116. public get accommodWidth() {
  117. return this.viewSize.width - this.paddingLeft - this.paddingRight
  118. }
  119. /** 可容纳item的真实高度 */
  120. public get accommodHeight() {
  121. return this.viewSize.height - this.paddingTop - this.paddingBottom
  122. }
  123. public get scrollView(): MouseUISpuerScrollView {
  124. if (!this._scrollView) this._scrollView = this.node.parent.parent.getComponent(MouseUISpuerScrollView)
  125. return this._scrollView
  126. }
  127. /** 当前头部的item */
  128. public get header(): cc.Node {
  129. return this._children[0]
  130. }
  131. /** 当前尾部的item */
  132. public get footer(): cc.Node {
  133. return this._children[this._children.length - 1]
  134. }
  135. /** 真实的上边距 */
  136. public get topBoundary() {
  137. if (this.headerToFooter) {
  138. return this.headerBoundaryY + this.paddingTop
  139. } else {
  140. return this.footerBoundaryY + this.paddingTop
  141. }
  142. }
  143. /** 真实的下边距 */
  144. public get bottomBoundary() {
  145. if (this.headerToFooter) {
  146. return this.footerBoundaryY - this.paddingBottom
  147. } else {
  148. return this.headerBoundaryY - this.paddingBottom
  149. }
  150. }
  151. /** 真实的左边距 */
  152. public get leftBoundary() {
  153. if (this.headerToFooter) {
  154. return this.headerBoundaryX - this.paddingLeft
  155. } else {
  156. return this.footerBoundaryX - this.paddingLeft
  157. }
  158. }
  159. /** 真实的右边距 */
  160. public get rightBoundary() {
  161. if (this.headerToFooter) {
  162. return this.footerBoundaryX + this.paddingRight
  163. } else {
  164. return this.headerBoundaryX + this.paddingRight
  165. }
  166. }
  167. /** 头部item的世界坐标边框 类似 xMin、xMax */
  168. public get headerBoundaryX() {
  169. if (this.headerToFooter) {
  170. return this.node.x + this.header.x - this.header.anchorX * this.getScaleWidth(this.header)
  171. } else {
  172. return this.node.x + this.header.x + (1 - this.header.anchorX) * this.getScaleWidth(this.header)
  173. }
  174. }
  175. /** 头部item的世界坐标边框 类似 yMin、yMax */
  176. public get headerBoundaryY() {
  177. if (this.headerToFooter) {
  178. return this.node.y + this.header.y + (1 - this.header.anchorY) * this.getScaleHeight(this.header)
  179. } else {
  180. return this.node.y + this.header.y - this.header.anchorY * this.getScaleHeight(this.header)
  181. }
  182. }
  183. /** 尾部item的世界坐标边框 类似 xMin、xMax */
  184. public get footerBoundaryX() {
  185. if (this.headerToFooter) {
  186. return this.node.x + this.footer.x + (1 - this.footer.anchorX) * this.getScaleWidth(this.footer)
  187. } else {
  188. return this.node.x + this.footer.x - this.footer.anchorX * this.getScaleWidth(this.footer)
  189. }
  190. }
  191. /** 尾部item的世界坐标边框 类似 yMin、yMax */
  192. public get footerBoundaryY() {
  193. if (this.headerToFooter) {
  194. return this.node.y + this.footer.y - this.footer.anchorY * this.getScaleHeight(this.footer)
  195. } else {
  196. return this.node.y + this.footer.y + (1 - this.footer.anchorY) * this.getScaleHeight(this.footer)
  197. }
  198. }
  199. public get isNormalSize(): boolean {
  200. return this.node.getContentSize().equals(this.viewSize)
  201. }
  202. /** 重写 this.node.getContentSize 动态计算头尾item 返回虚拟的尺寸 非content设置的尺寸 */
  203. public getContentSize() {
  204. let size = this.getReallySize()
  205. let viewSize = this.scrollView.view.getContentSize()
  206. // 列表为空时 直接返回 scrollView.view 的尺寸
  207. if (size.height < viewSize.height) {
  208. size.height = viewSize.height
  209. }
  210. if (size.width < viewSize.width) {
  211. size.width = viewSize.width
  212. }
  213. return size
  214. }
  215. /** 返回 header到 footer 之间的整体尺寸 */
  216. public getReallySize() {
  217. if (this.node.childrenCount == 0) return this.viewSize
  218. let size = cc.Size.ZERO
  219. if (this.headerToFooter) { // 根据header和footer计算出真实的content尺寸
  220. size.width = this.footerBoundaryX + -this.headerBoundaryX + this.paddingLeft + this.paddingRight
  221. size.height = this.headerBoundaryY + -this.footerBoundaryY + this.paddingTop + this.paddingBottom
  222. } else {
  223. size.width = this.headerBoundaryX + -this.footerBoundaryX + this.paddingLeft + this.paddingRight
  224. size.height = this.footerBoundaryY + -this.headerBoundaryY + this.paddingTop + this.paddingBottom
  225. }
  226. return size
  227. }
  228. /** 重置scrollview */
  229. public resetScrollView() {
  230. this.scrollView.reset()
  231. }
  232. /** 获取缩放系数 */
  233. public getUsedScaleValue(value: number) {
  234. return this.affectedByScale ? Math.abs(value) : 1
  235. }
  236. /** 设置最大item数量 */
  237. public async total(value: number) {
  238. this.scrollView.stopAutoScroll()
  239. this.scrollView.release() // 释放(功能用于上拉加载 下拉刷新)
  240. this.initlized() // 初始化
  241. await this.asyncCreateItem(value) // 分帧创建item
  242. let dataOffset = this.getDataOffset(value) //获取数据偏移量(根据value相对于 _maxItemTotal 计算增加、减少的数量)
  243. let reallyOffset = this.getReallyOffset(dataOffset) // 获取真实的数据偏移(Grid模式 功能用于判断是否需要偏移header来将下方填满)
  244. this.refreshItems(value, reallyOffset) //通过已有的item['index'] 加上数据偏移 来是刷新显示
  245. this._maxItemTotal = value // 记录当前总数
  246. }
  247. /** 获取兄弟节点 */
  248. public getBrotherNode(node: cc.Node) {
  249. let index = this.getSiblingIndex(node) - 1 // 此 getSiblingIndex 非 this.node.getSiblingIndex
  250. return this._children[index]
  251. }
  252. /** 是否是一组item中第一个(垂直滑动中 一组item 就是单行的所有列 、水平滑动中 一组item 就是单列中所有行)*/
  253. public isGroupHeader(node: cc.Node): boolean {
  254. let xOry = this.getGroupHeader(node)
  255. let pos = this.vertical ? cc.v2(xOry.x, 0) : cc.v2(0, xOry.y)
  256. let self = this.vertical ? cc.v2(node.x, 0) : cc.v2(0, node.y)
  257. return self.fuzzyEquals(pos, EPSILON)
  258. }
  259. /** 是否是一组item中最后一个(垂直滑动中 一组item 就是单行的所有列 、水平滑动中 一组item 就是单列中所有行)*/
  260. public isGroupFooter(node: cc.Node): boolean {
  261. let xOry = this.getGroupFooter(node)
  262. let pos = this.vertical ? cc.v2(xOry.x, 0) : cc.v2(0, xOry.y)
  263. let self = this.vertical ? cc.v2(node.x, 0) : cc.v2(0, node.y)
  264. return self.fuzzyEquals(pos, EPSILON)
  265. }
  266. /** 获取一组item中起始位置 (垂直滑动中 一组item 就是单行的所有列 、水平滑动中 一组item 就是单列中所有行)*/
  267. public getGroupHeader(node: cc.Node): cc.Vec2 {
  268. let pos = cc.Vec2.ZERO
  269. if (!node) return pos
  270. if (this.vertical) {
  271. if (this.headerToFooter) {
  272. pos.x = node.anchorX * this.getScaleWidth(node) + (this.paddingLeft * node.scaleX) - (this.node.anchorX * this.viewSize.width * node.scaleX)
  273. pos.y = (1 - node.anchorY) * -this.getScaleHeight(node) - this.paddingTop + (1 - this.node.anchorY) * this.viewSize.height
  274. } else {
  275. pos.x = node.anchorX * this.getScaleWidth(node) + this.paddingLeft - this.node.anchorX * this.viewSize.width
  276. pos.y = node.anchorY * this.getScaleHeight(node) + this.paddingBottom - this.node.anchorY * this.viewSize.height
  277. }
  278. } else {
  279. if (this.headerToFooter) {
  280. pos.x = node.anchorX * this.getScaleWidth(node) + this.paddingLeft - this.node.anchorX * this.viewSize.width
  281. pos.y = (1 - node.anchorY) * -node.height - this.paddingTop + (1 - this.node.anchorY) * this.viewSize.height
  282. } else {
  283. pos.x = this.accommodWidth * this.node.anchorX - this.getScaleWidth(node) * (1 - node.anchorX)
  284. pos.y = (1 - node.anchorY) * -node.height - this.paddingTop + (1 - this.node.anchorY) * this.viewSize.height
  285. }
  286. }
  287. return pos
  288. }
  289. /** 获取一组item中结束位置 (垂直滑动中 一组item 就是单行的所有列 、水平滑动中 一组item 就是单列中所有行)*/
  290. public getGroupFooter(node: cc.Node): cc.Vec2 {
  291. let pos = cc.Vec2.ZERO
  292. if (!node) return pos
  293. if (this.vertical) {
  294. pos.x = (this.accommodWidth + this.paddingLeft) * this.node.anchorX - (this.getScaleWidth(node) * (1 - node.anchorX) + this.node.anchorX * this.paddingRight)
  295. pos.y = node.y
  296. } else {
  297. pos.x = node.x
  298. pos.y = -((this.accommodHeight + this.paddingTop) * this.node.anchorY - this.getScaleHeight(node) * node.anchorY) + (1 - node.anchorY) * this.paddingBottom
  299. }
  300. return pos
  301. }
  302. /** 获取一组item中 node 相对 relative 右偏移量 (垂直滑动中 一组item 就是单行的所有列 、水平滑动中 一组item 就是单列中所有行)*/
  303. public getGroupRightX(node: cc.Node, relative: cc.Node) {
  304. if (!node || !relative) return this.getGroupHeader(node).x
  305. let prevWidth = relative.x + this.getScaleWidth(relative) * (1 - relative.anchorX)
  306. let selfWidth = this.getScaleWidth(node) * node.anchorX
  307. return prevWidth + selfWidth + this.spacing.x
  308. }
  309. /** 获取一组item中 node 相对 relative 左偏移量 (垂直滑动中 一组item 就是单行的所有列 、水平滑动中 一组item 就是单列中所有行)*/
  310. public getGroupLeftX(node: cc.Node, relative: cc.Node) {
  311. if (!node || !relative) return this.getGroupFooter(node).x
  312. let prevWidth = relative.x - this.getScaleWidth(relative) * relative.anchorX
  313. let selfWidth = this.getScaleWidth(node) * (1 - node.anchorX)
  314. return prevWidth - selfWidth - this.spacing.x
  315. }
  316. /** 获取一组item中 node 相对 relative 下偏移量 (垂直滑动中 一组item 就是单行的所有列 、水平滑动中 一组item 就是单列中所有行)*/
  317. public getGroupBottomY(node: cc.Node, relative: cc.Node) {
  318. let prevHeight = relative.y - this.getScaleHeight(relative) * relative.anchorY
  319. let selfHeight = this.getScaleHeight(node) * (1 - node.anchorY)
  320. return prevHeight - selfHeight - this.spacing.y
  321. }
  322. /** 获取一组item中 node 相对 relative 上偏移量 (垂直滑动中 一组item 就是单行的所有列 、水平滑动中 一组item 就是单列中所有行)*/
  323. public getGroupTopY(node: cc.Node, relative: cc.Node) {
  324. let prevHeight = relative.y + this.getScaleHeight(relative) * (1 - relative.anchorY)
  325. let selfHeight = this.getScaleHeight(node) * node.anchorY
  326. return prevHeight + selfHeight + this.spacing.y
  327. }
  328. /** 判断给定的 node 乘以 multiple 倍数后 是否超出了头部边框 ( multiple = 1 就是一个node的尺寸 默认1.5倍)*/
  329. public isOutOfBoundaryHeader(node: cc.Node, multiple: number = 1.5) {
  330. let width = node.width * this.getUsedScaleValue(node.scaleX) * multiple
  331. let height = -node.height * this.getUsedScaleValue(node.scaleY) * multiple
  332. let offset = this.scrollView.getHowMuchOutOfBoundary(cc.v2(width, height))
  333. return offset
  334. }
  335. /** 判断给定的 node 乘以 multiple 倍数后 是否超出了尾部部边框 ( multiple = 1 就是一个node的尺寸 默认1.5倍)*/
  336. public isOutOfBoundaryFooter(node: cc.Node, multiple: number = 1.5) {
  337. let width = -node.width * this.getUsedScaleValue(node.scaleX) * multiple
  338. let height = node.height * this.getUsedScaleValue(node.scaleY) * multiple
  339. let offset = this.scrollView.getHowMuchOutOfBoundary(cc.v2(width, height))
  340. return offset
  341. }
  342. /** 滚动到头部 (根据 排列方向、排列子节点的方向)来调用 scrollView.scrollTo... 方法 */
  343. public scrollToHeader(timeInSecond?: number, attenuated?: boolean) {
  344. this.scrollToHeaderOrFooter = timeInSecond > 0
  345. this.scrollView.stopAutoScroll()
  346. this.resetToHeader()
  347. if (this.headerToFooter) {
  348. if (this.vertical) {
  349. this.scrollView.scrollToTop(timeInSecond, attenuated)
  350. } else {
  351. this.scrollView.scrollToLeft(timeInSecond, attenuated)
  352. }
  353. } else {
  354. if (this.vertical) {
  355. this.scrollView.scrollToBottom(timeInSecond, attenuated)
  356. } else {
  357. this.scrollView.scrollToRight(timeInSecond, attenuated)
  358. }
  359. }
  360. }
  361. /** 滚动到尾部(根据 排列方向、排列子节点的方向)来调用 scrollView.scrollTo... 方法 */
  362. public scrollToFooter(timeInSecond?: number, attenuated?: boolean) {
  363. this.scrollToHeaderOrFooter = timeInSecond > 0
  364. this.scrollView.stopAutoScroll()
  365. this.resetToFooter()
  366. if (this.headerToFooter) {
  367. if (this.vertical) {
  368. this.scrollView.scrollToBottom(timeInSecond, attenuated)
  369. } else {
  370. this.scrollView.scrollToRight(timeInSecond, attenuated)
  371. }
  372. } else {
  373. if (this.vertical) {
  374. this.scrollView.scrollToTop(timeInSecond, attenuated)
  375. } else {
  376. this.scrollView.scrollToLeft(timeInSecond, attenuated)
  377. }
  378. }
  379. }
  380. /** 通知给定的node刷新数据 */
  381. public notifyRefreshItem(target: cc.Node) {
  382. //cc.Component.EventHandler.emitEvents(this.refreshItemEvents, target, target['index'])
  383. this.node.emit(MouseUISuperLayout.REFRESH_ITEM, target, target['index'])
  384. }
  385. /** 获取节点索引 */
  386. public getSiblingIndex(node: cc.Node) {
  387. return this._children.indexOf(node)
  388. }
  389. /** 自定义索引方法 这里不是通过实时修改节点索引的方法,只是模拟类似的功能,实际上并没有真正改变节点的实际顺序(优化项) */
  390. public setSiblingIndex(node: cc.Node, index: number) {
  391. // 此方法时参考引擎原setSiblingIndex方法 去掉了修改节点索引位置的调用(item本身的zIndex没有任何变化)
  392. index = index !== -1 ? index : this._children.length - 1
  393. var oldIndex = this._children.indexOf(node)
  394. if (index !== oldIndex) {
  395. this._children.splice(oldIndex, 1)
  396. if (index < this._children.length) {
  397. this._children.splice(index, 0, node)
  398. }
  399. else {
  400. this._children.push(node)
  401. }
  402. /**
  403. * 这里区别于原方法 原方法是改变node节点顺序后发送cc.Node.EventType.SIBLING_ORDER_CHANGED通知 这里不需要修改节点顺序
  404. * 这里发送一个自定义事件 模拟 SIBLING_ORDER_CHANGED 通知
  405. */
  406. this.node.emit(MouseUIChangeBrotherEvnet)
  407. }
  408. }
  409. onLoad() {
  410. this.initlized()
  411. }
  412. /** 初始化 */
  413. private initlized() {
  414. if (this._isinited) return
  415. this.node.anchorX = 0.5 //固定content的锚点为中心
  416. this.node.anchorY = 0.5
  417. this.node.setContentSize(this.viewSize) //将content的尺寸设置与view相同 (功能用于空列表时也可以下拉刷新和加载)
  418. // 重写 this.node.getContentSize 方法 因为content的真实尺寸不会随着item的数量变化
  419. this.node.getContentSize = this.getContentSize.bind(this)
  420. this.node.setPosition(cc.Vec2.ZERO)
  421. this.column = this.column < 1 ? 1 : this.column // 一组item的数量 最少是1 也就是普通的水平/垂直 大于1就是Grid模式
  422. // 监听content位置变化 刷新header footer节点的相对位置
  423. this.node.on(cc.Node.EventType.POSITION_CHANGED, this.onChangePosition, this)
  424. this.scrollView.view.on(cc.Node.EventType.SIZE_CHANGED, this.resetItemSize, this)
  425. this._isinited = true
  426. }
  427. onDestroy() {
  428. this.node.off(cc.Node.EventType.POSITION_CHANGED, this.onChangePosition, this)
  429. this.scrollView.view.off(cc.Node.EventType.SIZE_CHANGED, this.resetItemSize, this)
  430. }
  431. private onChangePosition() {
  432. let flag = this.isScrollToFooter // this.isScrollToFooter = true 向下滑动 false 向上滑动
  433. if (this.headerToFooter) {
  434. flag ? this.footerToHeaderWatchChilds(flag) : this.headerToFooterWatchChilds(flag) // 倒序刷新
  435. } else {
  436. flag ? this.headerToFooterWatchChilds(flag) : this.footerToHeaderWatchChilds(flag) // 正序刷新
  437. }
  438. // 当item 由多到少 并且 当content的位置被重置到初始状态时 重新设置头部的item归位
  439. if (this.vertical && 0 == this.node.y || this.horizontal && 0 == this.node.x) {
  440. this.header.setPosition(this.getGroupHeader(this.header))
  441. }
  442. }
  443. public resetItemSize() {
  444. // 重新设置原始尺寸
  445. for (let i = 0; i < this.children.length; i++) {
  446. this.children[i]['saveOriginSize']()
  447. }
  448. // 改变头部位置
  449. if (this.header) {
  450. let pos = this.getGroupHeader(this.header)
  451. if (this.vertical) {
  452. this.header.x = pos.x
  453. } else {
  454. this.header.y = pos.y
  455. }
  456. }
  457. // 通知改变坐标事件
  458. for (let i = 0; i < this.children.length; i++) {
  459. this.children[i].emit(cc.Node.EventType.POSITION_CHANGED)
  460. }
  461. }
  462. /** 获取缩放宽度 */
  463. private getScaleWidth(node: cc.Node): number {
  464. return node.width * this.getUsedScaleValue(node.scaleX)
  465. }
  466. /** 获取缩放高度 */
  467. private getScaleHeight(node: cc.Node): number {
  468. return node.height * this.getUsedScaleValue(node.scaleY)
  469. }
  470. /** 简单的浅拷贝 */
  471. private getTempChildren() {
  472. let list = []
  473. for (let i = 0; i < this._children.length; i++) {
  474. const child = this._children[i];
  475. list.push(child)
  476. }
  477. return list
  478. }
  479. /** 正序更新item */
  480. private headerToFooterWatchChilds(flag) {
  481. let children = this.getTempChildren()
  482. for (let i = 0; i < children.length; i++) {
  483. const child = children[i];
  484. child['watchSelf'](flag)
  485. }
  486. }
  487. /** 倒序更新item */
  488. private footerToHeaderWatchChilds(flag) {
  489. let children = this.getTempChildren()
  490. for (let i = children.length - 1; i >= 0; i--) {
  491. const child = children[i];
  492. child['watchSelf'](flag)
  493. }
  494. }
  495. /** 当数据增加、减少时 获取数据偏移 */
  496. private getDataOffset(value: number) {
  497. // 返回删除数据偏移 (比如当前最大数据值=10,新数据=9 返回-1)
  498. if (this.footer && this.footer['index'] + 1 >= value) {
  499. let offset = this.footer['index'] + 1 - value
  500. return offset == 0 ? 0 : -offset
  501. }
  502. // 返回增加数据偏移
  503. if (this._maxItemTotal == 0 || value < this._maxItemTotal || this._maxItemTotal < this._maxPrefabTotal) return 0 //比如当前最多允许创建10个item 当前显示5个 返回0
  504. if (this.isGroupFooter(this.footer)) return 0 //Grid模式 如果尾部的位置是在一组item中末尾的位置时 返回 0
  505. return value - this._maxItemTotal
  506. }
  507. /**
  508. * 当数据增加、减少时 获取节点偏移量
  509. * 当前数据是这样的 增加1个 增加2个
  510. * 0,1,2,3 1,2,3 2,3
  511. * 4,5,6 4,5,6,7 4,5,6,7
  512. * 8
  513. */
  514. private getReallyOffset(dataOffset: number) {
  515. if (!this.header) return 0
  516. if (dataOffset > 0) { // 代表增加item 表格模式下 通过偏移头部来让下方填满 填满后停止偏移
  517. for (let i = 0; i < dataOffset; i++) {
  518. if (this.isGroupFooter(this.footer)) return i //返回真实的偏移量
  519. // 此时如果header 已经是一组item中最后一个时 向下位移 并 设置到一组item的起始位置
  520. let pos = this.header.getPosition()
  521. if (this.vertical) { // 垂直滑动时
  522. if (this.isGroupFooter(this.header)) { // 当列表中第一个item正在一组item中末尾位置时
  523. if (this.headerToFooter) {
  524. pos.y = this.getGroupBottomY(this.header, this.header) //正序排列时 Y轴向下偏移(垂直排列时 一组item 头在左尾在右)
  525. } else {
  526. pos.y = this.getGroupTopY(this.header, this.header) //倒序排列时 Y轴向上偏移(垂直排列时 一组item 头在左尾在右)
  527. }
  528. pos.x = this.getGroupHeader(this.header).x // X轴向头部偏移
  529. } else { // 第一个item没有在一组item中末尾的位置 只将第一个item向右偏移 (只偏移X轴)
  530. pos.x = this.getGroupRightX(this.header, this.header) // X轴向右偏移
  531. }
  532. } else { // 水平滑动时
  533. if (this.isGroupFooter(this.header)) { // 当列表中第一个item正在一组item中末尾位置时
  534. if (this.headerToFooter) {
  535. pos.x = this.getGroupRightX(this.header, this.header) //正序排列时 X轴向右偏移(水平排列时 一组item 头在上尾在下)
  536. } else {
  537. pos.x = this.getGroupLeftX(this.header, this.header) //倒序排列时 X轴向左偏移(水平排列时 一组item 头在上尾在下)
  538. }
  539. pos.y = this.getGroupHeader(this.header).y // Y轴向头部偏移
  540. } else { // 第一个item没有在一组item中末尾的位置 只将第一个item向下偏移 (只偏移Y轴)
  541. pos.y = this.getGroupBottomY(this.header, this.header) // Y轴向下偏移
  542. }
  543. }
  544. this.header.setPosition(pos)
  545. }
  546. return dataOffset
  547. }
  548. // 代表减少了item 计算偏移量 offset<0 【注意!这里的逻辑和上面正好相反】
  549. for (let i = 0; i < Math.abs(dataOffset); i++) {
  550. let pos = cc.Vec2.ZERO
  551. if (this.vertical) {
  552. if (this.isGroupHeader(this.header)) {
  553. pos.x = this.getGroupFooter(this.header).x
  554. if (this.headerToFooter) {
  555. pos.y = this.getGroupTopY(this.header, this.header)
  556. } else {
  557. pos.y = this.getGroupBottomY(this.header, this.header)
  558. }
  559. } else {
  560. pos.x = this.getGroupLeftX(this.header, this.header)
  561. pos.y = this.header.y
  562. }
  563. } else {
  564. if (this.isGroupHeader(this.header)) {
  565. pos.y = this.getGroupFooter(this.header).y
  566. if (this.headerToFooter) {
  567. pos.x = this.getGroupLeftX(this.header, this.header)
  568. } else {
  569. pos.x = this.getGroupRightX(this.header, this.header)
  570. }
  571. } else {
  572. pos.y = this.getGroupTopY(this.header, this.header)
  573. pos.x = this.header.x
  574. }
  575. }
  576. this.header.setPosition(pos)
  577. }
  578. this.scrollView.calculateBoundary()
  579. return dataOffset
  580. }
  581. /** 刷新所有item数据 根据当前item的 index 刷新 */
  582. private refreshItems(value: number, offset: number = 0) {
  583. if (!this.header) return
  584. let startIndex = this.header['index'] - 1 + offset // 获取头部item持有的index 加上 数据偏移来计算起始index
  585. for (let i = 0; i < this._children.length; i++) {
  586. const child = this._children[i];
  587. startIndex++
  588. // 这里的判断用于无限循环滚动的逻辑 如果索引大于数据总数 索引归零
  589. if (startIndex > value - 1) {
  590. startIndex = 0
  591. } else if (startIndex < 0) { // 索引小于0 索引定位到数据尾部 保持首尾相连
  592. startIndex = value - 1
  593. }
  594. child['index'] = startIndex //设置当前索引
  595. this.notifyRefreshItem(child)
  596. }
  597. }
  598. /** 从头部到尾部重置数据 */
  599. private resetToHeader() {
  600. for (let i = 0; i < this._children.length; i++) {
  601. const child = this._children[i];
  602. child['index'] = i
  603. this.notifyRefreshItem(child)
  604. }
  605. if (!this.headerLoop && !this.footerLoop) {
  606. this.header?.setPosition(this.getGroupHeader(this.header))
  607. } else if (!this.scrollToHeaderOrFooter) {
  608. this.header?.setPosition(this.getGroupHeader(this.header))
  609. }
  610. }
  611. /** 从尾部到头部重置数据 */
  612. private resetToFooter() {
  613. let index = this._maxItemTotal
  614. for (let i = this._children.length - 1; i >= 0; i--) {
  615. var child = this._children[i]
  616. child['index'] = --index
  617. this.notifyRefreshItem(child)
  618. }
  619. }
  620. /** 删除多余的item */
  621. private removeChilds(value: number) {
  622. // 有多余的item 需要删除
  623. let length = this.node.childrenCount - value
  624. // 删除掉多余的item
  625. for (let i = 0; i < length; i++) {
  626. var child = this.footer
  627. this.remChild(child)
  628. child.destroy()
  629. this.node.removeChild(child)
  630. }
  631. if (!this.header) return
  632. // 将头部节点的位置重置到一组item的第一个位置
  633. let pos = this.getGroupHeader(this.header)
  634. if (this.vertical) {
  635. this.header.x = pos.x
  636. } else {
  637. this.header.y = pos.y
  638. }
  639. }
  640. /** 分帧创建item */
  641. private async asyncCreateItem(value: number) {
  642. this._gener?.return("")//取消上一次的分帧任务(如果任务正在执行)
  643. // 有多余的item 需要删除 不处理
  644. if (this.node.childrenCount > value) return this.removeChilds(value)
  645. // 已经固定item总数 不处理
  646. if (this._maxPrefabTotal > 0 && this._maxPrefabTotal == this.node.childrenCount) return
  647. // 开始分帧创建item
  648. let total = value - this.node.childrenCount //计算当前应该创建的总数
  649. this._gener = this.getGeneratorLength(total, () => {
  650. let child = cc.instantiate(this.prefab)
  651. child['index'] = this.node.childrenCount
  652. this.addChild(child)
  653. // 获取或添加 UISuperItem
  654. let spuerItem = child.getComponent(MouseUISpuerItem) || child.addComponent(MouseUISpuerItem)
  655. this.node.addChild(child)
  656. spuerItem.init(this)
  657. // item在首次创建时立即刷新 避免显示item初始状态
  658. this.notifyRefreshItem(child)
  659. // 如果创建的是第一个item 设置他的起始位置 之后的item会自动相对于他来设置自己 我们只需要确定第一个位置就行了
  660. if (this.node.childrenCount == 1) {
  661. let pos = this.getGroupHeader(this.header) //获取一组item中头部位置
  662. this.header.setPosition(pos)
  663. /**
  664. * 利用cc.ScrollView的方法来设置content的起始位置 由于content在初始化的时候固定了锚点都为0.5 所以这里必然是坐标0
  665. * 如果你没有其他需求确定用0.5锚点的话 这里可以自己设置为cc.Vec2.ZERO 节省不必要的计算(实际上计算量可忽略不计)
  666. */
  667. this.scrollView.calculateBoundary()
  668. }
  669. let selfHorW, viewHorW
  670. if (this.vertical) {
  671. selfHorW = this.getReallySize().height
  672. viewHorW = this.viewSize.height
  673. } else {
  674. selfHorW = this.getReallySize().width
  675. viewHorW = this.viewSize.width
  676. }
  677. /**
  678. * 根据排列方向 来判断对比宽度还是高度
  679. * 这里使用参数this.multiple来判断是否需要继续创建 默认为2倍 比如view可视尺寸为800 2倍就是1600
  680. * 根据之前所创建的所有item的尺寸计算是否满足这个尺寸 如果满足则不再继续创建
  681. * 由于是分帧加载 所以下一次创建会等这一次的返回结果 返回false 则终止分帧任务
  682. */
  683. if (selfHorW >= viewHorW * this.multiple && this.isGroupFooter(this.footer)) {
  684. this._maxPrefabTotal = this.node.childrenCount //固定item数量 不在继续创建
  685. return false
  686. }
  687. return true
  688. })
  689. await this.exeGenerator(this._gener, 10) //执行分帧任务 1帧创建10个
  690. }
  691. /** 同步添加本地变量 children 并发送 UIChangeBrotherEvnet 通知*/
  692. private addChild(node: cc.Node) {
  693. this._children.push(node)
  694. this.node.emit(MouseUIChangeBrotherEvnet)
  695. }
  696. /** 同步移除本地变量 children 并发送 UIChangeBrotherEvnet 通知 */
  697. private remChild(node: cc.Node) {
  698. let index = this._children.indexOf(node)
  699. if (index == -1) return
  700. this._children.splice(index, 1)
  701. this.node.emit(MouseUIChangeBrotherEvnet)
  702. }
  703. /** 分帧加载 */
  704. private * getGeneratorLength(length: number, callback: Function, ...params: any): Generator {
  705. for (let i = 0; i < length; i++) {
  706. let result = callback(i, ...params)
  707. if (result) {
  708. yield
  709. } else {
  710. return
  711. }
  712. }
  713. }
  714. /** 分帧执行 */
  715. private exeGenerator(generator: Generator, duration: number) {
  716. return new Promise((resolve, reject) => {
  717. let gen = generator
  718. let execute = () => {
  719. let startTime = new Date().getTime()
  720. for (let iter = gen.next(); ; iter = gen.next()) {
  721. if (iter == null || iter.done) {
  722. resolve(null)
  723. return
  724. }
  725. if (new Date().getTime() - startTime > duration) {
  726. setTimeout(() => execute(), cc.director.getDeltaTime() * 1000)
  727. return
  728. }
  729. }
  730. }
  731. execute()
  732. })
  733. }
  734. }