/** * ============================================================================ * DynamicSpriteList.js - 动态精灵列表工具 * ============================================================================ * * 基于 gameabc 框架的精灵复制功能,提供动态列表创建、管理和交互的完整解决方案。 * * 核心功能: * - 动态创建/删除精灵列表 * - 自动滑动和边界回弹 * - 点击检测和事件分发 * - 裁剪区域管理 * * 适用场景: * - 小局结算列表 * - 大局结算列表 * - 听牌提示列表 * - 战绩列表 * - 排行榜 * * 使用示例: * ```javascript * // 1. 创建列表实例 * var myList = new DynamicSpriteList({ * containerId: 100, // 父精灵ID(容器) * clipArea: { x: 50, y: 100, width: 600, height: 400 }, * rowHeight: 80, * templates: { * background: { spriteId: 101 }, * title: { spriteId: 102, offset: { x: 20, y: 10 } }, * button: { spriteId: 103, offset: { x: 500, y: 20 } } * } * }); * * // 2. 设置数据并渲染 * myList.setData([ * { title: '第1行', buttonText: '查看' }, * { title: '第2行', buttonText: '查看' } * ]); * * // 3. 监听点击事件 * myList.onClick = function(type, index, data) { * console.log('点击了', type, '第', index, '行'); * }; * * // 4. 在事件处理函数中调用 * // mousedown: myList.handleMouseDown(spid, x, y); * // mousemove: myList.handleMouseMove(spid, offsetY); * // mouseup: myList.handleMouseUp(spid, x, y); * // drawbegin: myList.handleDrawBegin(spid); * * // 5. 销毁 * myList.destroy(); * ``` * * ============================================================================ * @guide 开发指南 - DynamicSpriteList 完整使用规范 * ============================================================================ * * 【准备工作:在编辑器中预置精灵】 * 1. 在编辑器中放置一个容器精灵(作为 containerId) * 2. 在容器精灵旁边放置各列模板精灵(通常隐藏): * - 行背景模板(每行底色/分隔线) * - 数据列模板(牌图、分数、数量等,每种一个) * 3. 记录这些模板精灵的 ID,用于配置 templates * * 【初始化列表(听牌提示列表示例)】 * var tingList = new DynamicSpriteList({ * containerId: sprites.LIST_CONTAINER, // 容器精灵 * clipArea: { * x: 0, y: 0, // 相对容器左上角 * width: 640, height: 480 // 可见区域尺寸 * }, * rowHeight: 80, // 每行高度(像素) * templates: { * // 每列都需要一个 spriteId(模板) * rowBg: { spriteId: sprites.ROW_TEMPLATE }, * card: { spriteId: sprites.CARD_TEMPLATE, offset: { x: 20, y: 10 } }, * score: { spriteId: sprites.SCORE_TEMPLATE, offset: { x: 120, y: 10 } }, * jingBadge: { spriteId: sprites.JING_BADGE, offset: { x: 90, y: 5 } }, * remainBadge: { spriteId: sprites.REMAIN_BADGE, offset: { x: 200, y: 5 } }, * rowSeparator: { spriteId: sprites.ROW_SEPARATOR } * }, * scrollSensitivity: 5, // 移动5px以上才触发滚动 * bounceSpeed: 1 // 回弹速度 * }); * * 【横向列表(左右滑动)】 * var themeList = new DynamicSpriteList({ * containerId: sprites.LIST_CONTAINER, * clipArea: { x: 340, y: 240, width: 600, height: 160 }, * orientation: 'horizontal', // 默认 'vertical' * itemWidth: 262, // 主轴步长(项宽 + 项间距),与纵向 rowHeight 对称 * renderRow: function (ctx) { * // 横向下 ctx.addSprite 的 x 会叠加该项的主轴基准,y 原样 * var sid = ctx.addSprite('preview', TPL_ID, 0, 0); * } * }); * // 事件转发:横向传 offmovex(纵向传 offmovey) * themeList.handleMouseMove(event.spriteId, event.offset.x); * * 【设置数据(触发渲染)】 * // 数据数组中每个元素对应一行 * // setData 会清除旧行、重新创建新行 * tingList.setData(tingHints); // tingHints = [{ code, score, remain, isJing }, ...] * * 【监听点击(在 setData 之前或之后设置均可)】 * tingList.onClick = function(type, rowIndex, rowData) { * // type: 模板键名,如 'card'、'score' * // rowIndex: 第几行(0-based) * // rowData: 该行对应的数据对象 * if (type === 'card') { * self._onCardClick(rowData); * } * }; * * 【接入 SpriteEventController 事件(必须接入,否则列表不响应交互)】 * var self = this; * // 在 init 中注册容器精灵的鼠标事件 * SpriteEventController.registerMouseDown(sprites.LIST_CONTAINER, function(event) { * tingList.handleMouseDown(event.spid, event.offsetX, event.offsetY); * }); * SpriteEventController.registerMouseMove(sprites.LIST_CONTAINER, function(event) { * tingList.handleMouseMove(event.spid, event.offsetY); * }); * SpriteEventController.registerMouseUp(sprites.LIST_CONTAINER, function(event) { * tingList.handleMouseUp(event.spid, event.offsetX, event.offsetY); * }); * SpriteEventController.registerDrawBegin(sprites.LIST_CONTAINER, function(event) { * tingList.handleDrawBegin(event.spid); * }); * * 【销毁(组件关闭/destroy时必须调用)】 * tingList.destroy(); // 清除所有复制精灵,解除事件绑定 * * 【注意事项】 * - destroy() 必须在组件 onDestroy/hide 时调用,否则复制精灵会残留 * - 每次 setData 会完全重建行,不要频繁调用(建议数据变化时才调用) * - templates 中的 spriteId 是编辑器中预置的精灵ID,不是复制出来的ID * - 如果列表不需要滚动,设置 enableScroll: false 可提升性能 * * @author JinXian Team * @version 1.0.0 * @since 2025-12-13 */ 'use strict'; // ============================================================================ // DynamicSpriteList 类定义 // ============================================================================ /** * 动态精灵列表构造函数 * * @constructor * @param {Object} config - 列表配置对象 * @param {number} config.containerId - 父精灵ID(容器精灵,所有复制精灵将附着在此精灵上) * @param {Object} config.clipArea - 裁剪区域配置(可见区域) * @param {number} config.clipArea.x - 裁剪区域左上角X坐标 * @param {number} config.clipArea.y - 裁剪区域左上角Y坐标 * @param {number} config.clipArea.width - 裁剪区域宽度 * @param {number} config.clipArea.height - 裁剪区域高度 * @param {number} config.rowHeight - 每行高度(像素) * @param {Object} config.templates - 模板精灵配置 * @param {number} [config.scrollSensitivity=2] - 滑动灵敏度(移动多少像素触发滑动) * @param {number} [config.bounceSpeed=1] - 回弹动画速度 * @param {boolean} [config.enableScroll=true] - 是否启用滚动 * @param {boolean} [config.enableClick=true] - 是否启用点击 * * @example * // 模板配置示例 * templates: { * // 行背景 - 必须有 * background: { * spriteId: 101, // 模板精灵ID * offset: { x: 0, y: 0 } // 相对行起点的偏移(可选,默认0,0) * }, * // 文字模板 * title: { * spriteId: 102, * offset: { x: 20, y: 15 }, * textProperty: 'title' // 对应数据中的属性名 * }, * // 按钮模板 * button: { * spriteId: 103, * offset: { x: 500, y: 20 }, * clickable: true // 标记为可点击 * } * } */ /** * 主轴/交叉轴的引擎操作码与 clipArea 字段映射 * 前缀 DSL_ 是必要的:友乐运行时所有