JMListView.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. /**
  2. * 列表UI(单行或者单列)
  3. * 主要功能:
  4. * 1、支持方向选项:从左到右、从右到左、从上到下、从下到上
  5. * 2、实现了对象缓存池功能
  6. * 3、支持不同尺寸的单元格排列(需要重载 itemIndexToPrefabIndex 方法)
  7. * 4、支持单元格间距
  8. * 5、支持前后边距
  9. */
  10. cc.Class({
  11. extends: cc.Component,
  12. editor: {
  13. menu: '嘉米公用/JMListView'
  14. },
  15. properties: {
  16. itemPrefabList: {
  17. default: [],
  18. type: cc.Prefab,
  19. tooltip: '单元格预制体列表,默认仅取第一个。设定为列表是为了子类自定义列表——含有不同样式的单元格的列表。'
  20. },
  21. direction: {
  22. default: JMC.LIST_VIEW_DIRECTION.TOP_TO_BOTTOM, // 默认从上到下
  23. type: JMC.LIST_VIEW_DIRECTION,
  24. tooltip: '列表单元格排列的方向'
  25. },
  26. spacing: {
  27. default: 0,
  28. tooltip: '单元格间距'
  29. },
  30. paddingStart: {
  31. default: 0,
  32. tooltip: '开始边距'
  33. },
  34. paddingEnd: {
  35. default: 0,
  36. tooltip: '结束边距'
  37. }
  38. },
  39. onDestroy () {
  40. if (this.nodePoolList) {
  41. for (let v of this.nodePoolList) {
  42. v.clear();
  43. }
  44. }
  45. },
  46. init () {
  47. if (this._didInit) {
  48. return;
  49. }
  50. this._didInit = true;
  51. this._initNodePool();
  52. this._initScrollView();
  53. this._resetParams();
  54. },
  55. /**
  56. * 刷新数据源
  57. *
  58. * @author Pyden
  59. * @date 2019-03-21
  60. * @param {List<any>} dataSource 数据列表,每个数据会传给 Item 刷新
  61. */
  62. reloadData (dataSource) {
  63. this.init();
  64. this.dataSource = dataSource;
  65. this.reloadView();
  66. },
  67. /**
  68. * 刷新 UI。会清理已展示的数据
  69. *
  70. * @author Pyden
  71. * @date 2019-03-21
  72. */
  73. reloadView () {
  74. this._initDataSource();
  75. this._resetShowParams(this.scrollView.getScrollOffset());
  76. },
  77. /**
  78. * Item 序号 转 预制体序号
  79. * 单元格是用不同预制体的时候,子类自定义该函数:确定单元格序号和预制体的关系。
  80. *
  81. * @author Pyden
  82. * @date 2019-03-21
  83. * @param {int} itemIndex Item 序号。从0开始
  84. * @returns {int} 预制体序号
  85. */
  86. itemIndexToPrefabIndex (itemIndex) {
  87. return 0;
  88. },
  89. /**
  90. * 用预制体新建 Item
  91. *
  92. * @author Pyden
  93. * @date 2019-03-21
  94. * @param {int} index 序号。从0开始
  95. * @returns {cc.Node}
  96. */
  97. newItem (index) {
  98. let prefab = this._getPrefab(index);
  99. let item = cc.instantiate(prefab);
  100. return item;
  101. },
  102. // ---------------- 私有方法分割线 ----------------
  103. /**
  104. * 初始化对象池
  105. *
  106. * @author Pyden
  107. * @date 2019-03-21
  108. */
  109. _initNodePool () {
  110. // 对象池列表。一个预制体对应一个对象池
  111. this.nodePoolList = [];
  112. let length = this.itemPrefabList.length;
  113. for (let i = 0; i < length; i++) {
  114. this.nodePoolList[i] = new cc.NodePool();
  115. }
  116. },
  117. /**
  118. * 初始化 ScrollView
  119. *
  120. * @author Pyden
  121. * @date 2019-03-21
  122. */
  123. _initScrollView () {
  124. this.scrollView = this.getComponent(cc.ScrollView);
  125. // 根据列表方向设置 ScrollView
  126. switch (this.direction) {
  127. case JMC.LIST_VIEW_DIRECTION.LEFT_TO_RIGHT:
  128. // 对齐 LEFT_CENTER
  129. this.scrollView.horizontal = true;
  130. this.scrollView.vertical = false;
  131. this.scrollView.content.anchorX = 0;
  132. this.scrollView.content.anchorY = 0.5;
  133. break;
  134. case JMC.LIST_VIEW_DIRECTION.RIGHT_TO_LEFT:
  135. // 对齐 RIGHT_CENTER
  136. this.scrollView.horizontal = true;
  137. this.scrollView.vertical = false;
  138. this.scrollView.content.anchorX = 1;
  139. this.scrollView.content.anchorY = 0.5;
  140. break;
  141. case JMC.LIST_VIEW_DIRECTION.TOP_TO_BOTTOM:
  142. // 对齐 TOP_CENTER
  143. this.scrollView.horizontal = false;
  144. this.scrollView.vertical = true;
  145. this.scrollView.content.anchorX = 0.5;
  146. this.scrollView.content.anchorY = 1;
  147. break;
  148. case JMC.LIST_VIEW_DIRECTION.BOTTOM_TO_TOP:
  149. // 对齐 BOTTOM_CENTER
  150. this.scrollView.horizontal = false;
  151. this.scrollView.vertical = true;
  152. this.scrollView.content.anchorX = 0.5;
  153. this.scrollView.content.anchorY = 0;
  154. break;
  155. default:
  156. break;
  157. }
  158. this.scrollView.node.on('scrolling', this._onScrolling, this);
  159. },
  160. /**
  161. * 重置参数
  162. *
  163. * @author Pyden
  164. * @date 2019-03-21
  165. */
  166. _resetParams () {
  167. this._startPosList = [];
  168. this._itemMap = {};
  169. this._oldMinIndex = -1;
  170. this._oldMaxIndex = -1;
  171. this._minIndex = -1;
  172. this._maxIndex = -1;
  173. },
  174. /**
  175. * 获取指定序号的预制体
  176. *
  177. * @author Pyden
  178. * @date 2019-03-21
  179. * @param {int} index 序号。从0开始
  180. * @returns {cc.Prefab}
  181. */
  182. _getPrefab (index) {
  183. return this.itemPrefabList[this.itemIndexToPrefabIndex (index)];
  184. },
  185. /**
  186. * 获取指定序号的对象池
  187. *
  188. * @author Pyden
  189. * @date 2019-03-21
  190. * @param {int} index 序号。从0开始
  191. * @returns {cc.NodePool}
  192. */
  193. _getNodePool (index) {
  194. return this.nodePoolList[this.itemIndexToPrefabIndex (index)];
  195. },
  196. /**
  197. * 获取指定序号的Item。优先从 NodePool 获取 Item,不足时才新建 Item
  198. *
  199. * @author Pyden
  200. * @date 2019-03-21
  201. * @param {int} index 序号。从0开始
  202. * @returns {cc.Node}
  203. */
  204. _getItem (index) {
  205. let node = this._getItemFromPool(index);
  206. if (node) {
  207. return node;
  208. } else {
  209. return this.newItem(index);
  210. }
  211. },
  212. /**
  213. * 从 NodePool 获取 Item
  214. *
  215. * @author Pyden
  216. * @date 2019-03-21
  217. * @param {int} index 序号。从0开始
  218. * @returns {cc.Node}
  219. */
  220. _getItemFromPool (index) {
  221. let nodePool = this._getNodePool(index);
  222. if (nodePool.size() > 0) {
  223. return nodePool.get();
  224. } else {
  225. return undefined;
  226. }
  227. },
  228. /**
  229. * 将 Item 回收到 NodePool。同时会从界面移除
  230. *
  231. * @author Pyden
  232. * @date 2019-03-21
  233. * @param {int} index 序号。从0开始
  234. * @param {cc.Node} item 回收的节点
  235. */
  236. _putItemToPool (index, item) {
  237. let nodePool = this._getNodePool(index);
  238. nodePool.put(item);
  239. },
  240. /**
  241. * 获取指定序号 Item 在列表方向上的长度
  242. * 如:从左到右的列表,返回单元格的宽度
  243. *
  244. * @author Pyden
  245. * @date 2019-03-21
  246. * @param {int} index 序号。从0开始
  247. * @returns {float} 长度
  248. */
  249. _getItemLength (index) {
  250. let ret = 0;
  251. // 根据列表方向取长度
  252. let prefab = this._getPrefab(index);
  253. if (
  254. this.direction == JMC.LIST_VIEW_DIRECTION.LEFT_TO_RIGHT ||
  255. this.direction == JMC.LIST_VIEW_DIRECTION.RIGHT_TO_LEFT
  256. ) {
  257. ret = prefab.data.width;
  258. } else {
  259. ret = prefab.data.height;
  260. }
  261. return ret;
  262. },
  263. /**
  264. * 初始化数据源确定的参数
  265. * this._startPosList: 各个 item 的起始坐标
  266. * this._count: Item 总数
  267. * this._viewLength: ListView 可视长度。水平方向的列表取宽度,垂直方向的列表取高度
  268. * this._contentLength: content 总长度
  269. *
  270. * @author Pyden
  271. * @date 2019-03-21
  272. */
  273. _initDataSource () {
  274. // 移除旧的数据
  275. if (this._oldMinIndex >= 0) {
  276. for (let i = this._oldMinIndex; i <= this._oldMaxIndex; i++) {
  277. this._removeItem(i);
  278. }
  279. }
  280. // 初始化参数
  281. this._resetParams();
  282. let count = this.dataSource.length;
  283. let viewLength;
  284. let contentLength;
  285. // content长度 = paddingStart + itemLength + spacing + ... + itemLength + spacing + paddingEnd
  286. contentLength = this.paddingStart;
  287. for (let index = 0; index < count; index++) {
  288. this._startPosList[index] = contentLength;
  289. let itemLength = this._getItemLength (index);
  290. // 仅最后一个不加间隔
  291. if (index == count - 1) {
  292. contentLength = contentLength + itemLength;
  293. } else {
  294. contentLength = contentLength + itemLength + this.spacing;
  295. }
  296. }
  297. contentLength = contentLength + this.paddingEnd;
  298. // 根据列表方向 取可视长度,设置内容长度
  299. if (this.direction == JMC.LIST_VIEW_DIRECTION.LEFT_TO_RIGHT || this.direction == JMC.LIST_VIEW_DIRECTION.RIGHT_TO_LEFT) {
  300. viewLength = this.scrollView.node.width;
  301. this.scrollView.content.width = Math.max(contentLength, viewLength);
  302. } else {
  303. viewLength = this.scrollView.node.height;
  304. this.scrollView.content.height = Math.max(contentLength, viewLength);
  305. }
  306. this._count = count;
  307. this._viewLength = viewLength;
  308. this._contentLength = contentLength;
  309. },
  310. /**
  311. * 重置需要展示内容的参数:minIndex、maxIndex。同时更新 Item
  312. *
  313. * @author Pyden
  314. * @date 2019-03-21
  315. * @param {cc.Vec2} offset 当前的ScrollView的偏移
  316. */
  317. _resetShowParams (offset) {
  318. let count = this._count;
  319. let contentLength = this._contentLength;
  320. let viewLength = this._viewLength;
  321. let minPos = 0;
  322. let maxPos = 0;
  323. // 根据列表方向 获取可视范围
  324. switch (this.direction) {
  325. case JMC.LIST_VIEW_DIRECTION.LEFT_TO_RIGHT:
  326. // 对齐LEFT_CENTER
  327. minPos = -offset.x;
  328. maxPos = -offset.x + viewLength;
  329. break;
  330. case JMC.LIST_VIEW_DIRECTION.RIGHT_TO_LEFT:
  331. // 对齐RIGHT_CENTER
  332. minPos = contentLength + offset.x - viewLength;
  333. maxPos = contentLength + offset.x;
  334. break;
  335. case JMC.LIST_VIEW_DIRECTION.TOP_TO_BOTTOM:
  336. // 对齐TOP_CENTER
  337. minPos = offset.y;
  338. maxPos = offset.y + viewLength;
  339. break;
  340. case JMC.LIST_VIEW_DIRECTION.BOTTOM_TO_TOP:
  341. // 对齐BOTTOM_CENTER
  342. minPos = contentLength - offset.y - viewLength;
  343. maxPos = contentLength - offset.y;
  344. break;
  345. default:
  346. break;
  347. }
  348. let minIndex = count - 1;
  349. let maxIndex = count - 1;
  350. let index = 0;
  351. // 先得出minIndex
  352. for (; index < count - 1; index++) {
  353. let startPos = this._startPosList[index];
  354. if (startPos >= minPos) {
  355. minIndex = index;
  356. break;
  357. }
  358. let startPosNext = this._startPosList[index + 1];
  359. if (startPosNext > minPos) {
  360. minIndex = index;
  361. break;
  362. }
  363. }
  364. // 再得出maxIndex
  365. for (; index < count - 1; index++) {
  366. let startPos = this._startPosList[index];
  367. let startPosNext = this._startPosList[index + 1];
  368. if (startPos < maxPos && maxPos <= startPosNext) {
  369. maxIndex = index;
  370. break;
  371. }
  372. }
  373. this._minIndex = minIndex;
  374. this._maxIndex = maxIndex;
  375. // 更新Item
  376. this._updateItem();
  377. },
  378. /**
  379. * 更新 Item:移除刚刚不可见的,展示刚刚可见的
  380. *
  381. * @author Pyden
  382. * @date 2019-03-21
  383. */
  384. _updateItem () {
  385. let minIndex = this._minIndex;
  386. let maxIndex = this._maxIndex;
  387. let oldMinIndex = this._oldMinIndex;
  388. let oldMaxIndex = this._oldMaxIndex;
  389. // 保留下来不修改的Item序号范围
  390. let saveMinIndex = Math.max(minIndex, oldMinIndex);
  391. let saveMaxIndex = Math.min(maxIndex, oldMaxIndex);
  392. // 先移除
  393. for (let i = oldMinIndex; i < saveMinIndex; i++) {
  394. this._removeItem(i);
  395. }
  396. for (let i = saveMaxIndex + 1; i <= oldMaxIndex; i++) {
  397. this._removeItem(i);
  398. }
  399. // 再添加
  400. for (let i = minIndex; i < saveMinIndex; i++) {
  401. this._addItem(i);
  402. }
  403. for (let i = saveMaxIndex + 1; i <= maxIndex; i++) {
  404. this._addItem(i);
  405. }
  406. this._oldMinIndex = minIndex;
  407. this._oldMaxIndex = maxIndex;
  408. },
  409. /**
  410. * 添加指定需要的 Item 到界面
  411. *
  412. * @author Pyden
  413. * @date 2019-03-21
  414. * @param {int} index 序号。从0开始
  415. */
  416. _addItem (index) {
  417. let item = this._getItem(index);
  418. item.parent = this.scrollView.content;
  419. // 根据列表方向设置坐标
  420. switch (this.direction) {
  421. case JMC.LIST_VIEW_DIRECTION.LEFT_TO_RIGHT:
  422. // 对齐 LEFT_CENTER
  423. item.x = this._startPosList[index] + item.width * item.anchorX;
  424. item.y = item.height * (item.anchorY - 0.5);
  425. break;
  426. case JMC.LIST_VIEW_DIRECTION.RIGHT_TO_LEFT:
  427. // 对齐 RIGHT_CENTER
  428. item.x = -this._startPosList[index] - item.width * (1 - item.anchorX);
  429. item.y = item.height * (item.anchorY - 0.5);
  430. break;
  431. case JMC.LIST_VIEW_DIRECTION.TOP_TO_BOTTOM:
  432. // 对齐 TOP_CENTER
  433. item.x = item.width * (item.anchorX - 0.5);
  434. item.y = -this._startPosList[index] - item.height * (1 - item.anchorY);
  435. break;
  436. case JMC.LIST_VIEW_DIRECTION.BOTTOM_TO_TOP:
  437. // 对齐 BOTTOM_CENTER
  438. item.x = item.width * (item.anchorX - 0.5);
  439. item.y = this._startPosList[index] + item.height * item.anchorY;
  440. break;
  441. default:
  442. break;
  443. }
  444. // 刷新单元格数据
  445. let listViewItem = item.getComponent('JMListViewItem');
  446. let params = this.dataSource[index];
  447. listViewItem.reloadData(index, params);
  448. // 缓存
  449. this._itemMap[index] = item;
  450. },
  451. /**
  452. * 移除指定需要的 Item。同时会回收到 NodePool
  453. *
  454. * @author Pyden
  455. * @date 2019-03-21
  456. * @param {int} index 序号。从0开始
  457. */
  458. _removeItem (index) {
  459. this._putItemToPool(index, this._itemMap[index]);
  460. this._itemMap[index] = undefined;
  461. },
  462. /**
  463. * 滚动监听
  464. *
  465. * @author Pyden
  466. * @date 2019-03-21
  467. * @param {cc.ScrollView} sender 滚动视图组件
  468. */
  469. _onScrolling (sender) {
  470. this._resetShowParams(sender.getScrollOffset());
  471. }
  472. });