/** * 列表UI(单行或者单列) * 主要功能: * 1、支持方向选项:从左到右、从右到左、从上到下、从下到上 * 2、实现了对象缓存池功能 * 3、支持不同尺寸的单元格排列(需要重载 itemIndexToPrefabIndex 方法) * 4、支持单元格间距 * 5、支持前后边距 */ cc.Class({ extends: cc.Component, editor: { menu: '嘉米公用/JMListView' }, properties: { itemPrefabList: { default: [], type: cc.Prefab, tooltip: '单元格预制体列表,默认仅取第一个。设定为列表是为了子类自定义列表——含有不同样式的单元格的列表。' }, direction: { default: JMC.LIST_VIEW_DIRECTION.TOP_TO_BOTTOM, // 默认从上到下 type: JMC.LIST_VIEW_DIRECTION, tooltip: '列表单元格排列的方向' }, spacing: { default: 0, tooltip: '单元格间距' }, paddingStart: { default: 0, tooltip: '开始边距' }, paddingEnd: { default: 0, tooltip: '结束边距' } }, onDestroy () { if (this.nodePoolList) { for (let v of this.nodePoolList) { v.clear(); } } }, init () { if (this._didInit) { return; } this._didInit = true; this._initNodePool(); this._initScrollView(); this._resetParams(); }, /** * 刷新数据源 * * @author Pyden * @date 2019-03-21 * @param {List} dataSource 数据列表,每个数据会传给 Item 刷新 */ reloadData (dataSource) { this.init(); this.dataSource = dataSource; this.reloadView(); }, /** * 刷新 UI。会清理已展示的数据 * * @author Pyden * @date 2019-03-21 */ reloadView () { this._initDataSource(); this._resetShowParams(this.scrollView.getScrollOffset()); }, /** * Item 序号 转 预制体序号 * 单元格是用不同预制体的时候,子类自定义该函数:确定单元格序号和预制体的关系。 * * @author Pyden * @date 2019-03-21 * @param {int} itemIndex Item 序号。从0开始 * @returns {int} 预制体序号 */ itemIndexToPrefabIndex (itemIndex) { return 0; }, /** * 用预制体新建 Item * * @author Pyden * @date 2019-03-21 * @param {int} index 序号。从0开始 * @returns {cc.Node} */ newItem (index) { let prefab = this._getPrefab(index); let item = cc.instantiate(prefab); return item; }, // ---------------- 私有方法分割线 ---------------- /** * 初始化对象池 * * @author Pyden * @date 2019-03-21 */ _initNodePool () { // 对象池列表。一个预制体对应一个对象池 this.nodePoolList = []; let length = this.itemPrefabList.length; for (let i = 0; i < length; i++) { this.nodePoolList[i] = new cc.NodePool(); } }, /** * 初始化 ScrollView * * @author Pyden * @date 2019-03-21 */ _initScrollView () { this.scrollView = this.getComponent(cc.ScrollView); // 根据列表方向设置 ScrollView switch (this.direction) { case JMC.LIST_VIEW_DIRECTION.LEFT_TO_RIGHT: // 对齐 LEFT_CENTER this.scrollView.horizontal = true; this.scrollView.vertical = false; this.scrollView.content.anchorX = 0; this.scrollView.content.anchorY = 0.5; break; case JMC.LIST_VIEW_DIRECTION.RIGHT_TO_LEFT: // 对齐 RIGHT_CENTER this.scrollView.horizontal = true; this.scrollView.vertical = false; this.scrollView.content.anchorX = 1; this.scrollView.content.anchorY = 0.5; break; case JMC.LIST_VIEW_DIRECTION.TOP_TO_BOTTOM: // 对齐 TOP_CENTER this.scrollView.horizontal = false; this.scrollView.vertical = true; this.scrollView.content.anchorX = 0.5; this.scrollView.content.anchorY = 1; break; case JMC.LIST_VIEW_DIRECTION.BOTTOM_TO_TOP: // 对齐 BOTTOM_CENTER this.scrollView.horizontal = false; this.scrollView.vertical = true; this.scrollView.content.anchorX = 0.5; this.scrollView.content.anchorY = 0; break; default: break; } this.scrollView.node.on('scrolling', this._onScrolling, this); }, /** * 重置参数 * * @author Pyden * @date 2019-03-21 */ _resetParams () { this._startPosList = []; this._itemMap = {}; this._oldMinIndex = -1; this._oldMaxIndex = -1; this._minIndex = -1; this._maxIndex = -1; }, /** * 获取指定序号的预制体 * * @author Pyden * @date 2019-03-21 * @param {int} index 序号。从0开始 * @returns {cc.Prefab} */ _getPrefab (index) { return this.itemPrefabList[this.itemIndexToPrefabIndex (index)]; }, /** * 获取指定序号的对象池 * * @author Pyden * @date 2019-03-21 * @param {int} index 序号。从0开始 * @returns {cc.NodePool} */ _getNodePool (index) { return this.nodePoolList[this.itemIndexToPrefabIndex (index)]; }, /** * 获取指定序号的Item。优先从 NodePool 获取 Item,不足时才新建 Item * * @author Pyden * @date 2019-03-21 * @param {int} index 序号。从0开始 * @returns {cc.Node} */ _getItem (index) { let node = this._getItemFromPool(index); if (node) { return node; } else { return this.newItem(index); } }, /** * 从 NodePool 获取 Item * * @author Pyden * @date 2019-03-21 * @param {int} index 序号。从0开始 * @returns {cc.Node} */ _getItemFromPool (index) { let nodePool = this._getNodePool(index); if (nodePool.size() > 0) { return nodePool.get(); } else { return undefined; } }, /** * 将 Item 回收到 NodePool。同时会从界面移除 * * @author Pyden * @date 2019-03-21 * @param {int} index 序号。从0开始 * @param {cc.Node} item 回收的节点 */ _putItemToPool (index, item) { let nodePool = this._getNodePool(index); nodePool.put(item); }, /** * 获取指定序号 Item 在列表方向上的长度 * 如:从左到右的列表,返回单元格的宽度 * * @author Pyden * @date 2019-03-21 * @param {int} index 序号。从0开始 * @returns {float} 长度 */ _getItemLength (index) { let ret = 0; // 根据列表方向取长度 let prefab = this._getPrefab(index); if ( this.direction == JMC.LIST_VIEW_DIRECTION.LEFT_TO_RIGHT || this.direction == JMC.LIST_VIEW_DIRECTION.RIGHT_TO_LEFT ) { ret = prefab.data.width; } else { ret = prefab.data.height; } return ret; }, /** * 初始化数据源确定的参数 * this._startPosList: 各个 item 的起始坐标 * this._count: Item 总数 * this._viewLength: ListView 可视长度。水平方向的列表取宽度,垂直方向的列表取高度 * this._contentLength: content 总长度 * * @author Pyden * @date 2019-03-21 */ _initDataSource () { // 移除旧的数据 if (this._oldMinIndex >= 0) { for (let i = this._oldMinIndex; i <= this._oldMaxIndex; i++) { this._removeItem(i); } } // 初始化参数 this._resetParams(); let count = this.dataSource.length; let viewLength; let contentLength; // content长度 = paddingStart + itemLength + spacing + ... + itemLength + spacing + paddingEnd contentLength = this.paddingStart; for (let index = 0; index < count; index++) { this._startPosList[index] = contentLength; let itemLength = this._getItemLength (index); // 仅最后一个不加间隔 if (index == count - 1) { contentLength = contentLength + itemLength; } else { contentLength = contentLength + itemLength + this.spacing; } } contentLength = contentLength + this.paddingEnd; // 根据列表方向 取可视长度,设置内容长度 if (this.direction == JMC.LIST_VIEW_DIRECTION.LEFT_TO_RIGHT || this.direction == JMC.LIST_VIEW_DIRECTION.RIGHT_TO_LEFT) { viewLength = this.scrollView.node.width; this.scrollView.content.width = Math.max(contentLength, viewLength); } else { viewLength = this.scrollView.node.height; this.scrollView.content.height = Math.max(contentLength, viewLength); } this._count = count; this._viewLength = viewLength; this._contentLength = contentLength; }, /** * 重置需要展示内容的参数:minIndex、maxIndex。同时更新 Item * * @author Pyden * @date 2019-03-21 * @param {cc.Vec2} offset 当前的ScrollView的偏移 */ _resetShowParams (offset) { let count = this._count; let contentLength = this._contentLength; let viewLength = this._viewLength; let minPos = 0; let maxPos = 0; // 根据列表方向 获取可视范围 switch (this.direction) { case JMC.LIST_VIEW_DIRECTION.LEFT_TO_RIGHT: // 对齐LEFT_CENTER minPos = -offset.x; maxPos = -offset.x + viewLength; break; case JMC.LIST_VIEW_DIRECTION.RIGHT_TO_LEFT: // 对齐RIGHT_CENTER minPos = contentLength + offset.x - viewLength; maxPos = contentLength + offset.x; break; case JMC.LIST_VIEW_DIRECTION.TOP_TO_BOTTOM: // 对齐TOP_CENTER minPos = offset.y; maxPos = offset.y + viewLength; break; case JMC.LIST_VIEW_DIRECTION.BOTTOM_TO_TOP: // 对齐BOTTOM_CENTER minPos = contentLength - offset.y - viewLength; maxPos = contentLength - offset.y; break; default: break; } let minIndex = count - 1; let maxIndex = count - 1; let index = 0; // 先得出minIndex for (; index < count - 1; index++) { let startPos = this._startPosList[index]; if (startPos >= minPos) { minIndex = index; break; } let startPosNext = this._startPosList[index + 1]; if (startPosNext > minPos) { minIndex = index; break; } } // 再得出maxIndex for (; index < count - 1; index++) { let startPos = this._startPosList[index]; let startPosNext = this._startPosList[index + 1]; if (startPos < maxPos && maxPos <= startPosNext) { maxIndex = index; break; } } this._minIndex = minIndex; this._maxIndex = maxIndex; // 更新Item this._updateItem(); }, /** * 更新 Item:移除刚刚不可见的,展示刚刚可见的 * * @author Pyden * @date 2019-03-21 */ _updateItem () { let minIndex = this._minIndex; let maxIndex = this._maxIndex; let oldMinIndex = this._oldMinIndex; let oldMaxIndex = this._oldMaxIndex; // 保留下来不修改的Item序号范围 let saveMinIndex = Math.max(minIndex, oldMinIndex); let saveMaxIndex = Math.min(maxIndex, oldMaxIndex); // 先移除 for (let i = oldMinIndex; i < saveMinIndex; i++) { this._removeItem(i); } for (let i = saveMaxIndex + 1; i <= oldMaxIndex; i++) { this._removeItem(i); } // 再添加 for (let i = minIndex; i < saveMinIndex; i++) { this._addItem(i); } for (let i = saveMaxIndex + 1; i <= maxIndex; i++) { this._addItem(i); } this._oldMinIndex = minIndex; this._oldMaxIndex = maxIndex; }, /** * 添加指定需要的 Item 到界面 * * @author Pyden * @date 2019-03-21 * @param {int} index 序号。从0开始 */ _addItem (index) { let item = this._getItem(index); item.parent = this.scrollView.content; // 根据列表方向设置坐标 switch (this.direction) { case JMC.LIST_VIEW_DIRECTION.LEFT_TO_RIGHT: // 对齐 LEFT_CENTER item.x = this._startPosList[index] + item.width * item.anchorX; item.y = item.height * (item.anchorY - 0.5); break; case JMC.LIST_VIEW_DIRECTION.RIGHT_TO_LEFT: // 对齐 RIGHT_CENTER item.x = -this._startPosList[index] - item.width * (1 - item.anchorX); item.y = item.height * (item.anchorY - 0.5); break; case JMC.LIST_VIEW_DIRECTION.TOP_TO_BOTTOM: // 对齐 TOP_CENTER item.x = item.width * (item.anchorX - 0.5); item.y = -this._startPosList[index] - item.height * (1 - item.anchorY); break; case JMC.LIST_VIEW_DIRECTION.BOTTOM_TO_TOP: // 对齐 BOTTOM_CENTER item.x = item.width * (item.anchorX - 0.5); item.y = this._startPosList[index] + item.height * item.anchorY; break; default: break; } // 刷新单元格数据 let listViewItem = item.getComponent('JMListViewItem'); let params = this.dataSource[index]; listViewItem.reloadData(index, params); // 缓存 this._itemMap[index] = item; }, /** * 移除指定需要的 Item。同时会回收到 NodePool * * @author Pyden * @date 2019-03-21 * @param {int} index 序号。从0开始 */ _removeItem (index) { this._putItemToPool(index, this._itemMap[index]); this._itemMap[index] = undefined; }, /** * 滚动监听 * * @author Pyden * @date 2019-03-21 * @param {cc.ScrollView} sender 滚动视图组件 */ _onScrolling (sender) { this._resetShowParams(sender.getScrollOffset()); } });