Files
2026-08-11 22:17:54 +08:00

909 lines
30 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ============================================================================
* 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_ 是必要的:友乐运行时所有 <script> 共享同一全局作用域,
* 通用名(如 LIST_AXIS)会与其它文件互相覆盖。
* 操作码:18=左右移动 19=上下移动 20=设宽 21=设高
* (docs/important/client/精灵操作详细指南.md:38-41)
*/
var DSL_LIST_AXIS = {
vertical: {
posOp: 19, sizeOp: 21, crossPosOp: 18, crossSizeOp: 20,
clipPos: 'y', clipSize: 'height', crossClipPos: 'x', crossClipSize: 'width',
stepField: 'rowHeight'
},
horizontal: {
posOp: 18, sizeOp: 20, crossPosOp: 19, crossSizeOp: 21,
clipPos: 'x', clipSize: 'width', crossClipPos: 'y', crossClipSize: 'height',
stepField: 'itemWidth'
}
};
function DynamicSpriteList(config) {
// ========================================================================
// 参数验证
// ========================================================================
if (!config) {
throw new Error('DynamicSpriteList: config is required');
}
if (typeof config.containerId !== 'number') {
throw new Error('DynamicSpriteList: containerId must be a number');
}
if (!config.clipArea) {
throw new Error('DynamicSpriteList: clipArea is required');
}
var orientation = (config.orientation === 'horizontal') ? 'horizontal' : 'vertical';
var axis = DSL_LIST_AXIS[orientation];
var step = config[axis.stepField];
if (typeof step !== 'number' || step <= 0) {
throw new Error('DynamicSpriteList: ' + axis.stepField + ' must be a positive number');
}
var hasTemplates = config.templates && typeof config.templates === 'object';
var hasRenderRow = typeof config.renderRow === 'function';
if (!hasTemplates && !hasRenderRow) {
throw new Error('DynamicSpriteList: templates or renderRow is required');
}
// ========================================================================
// 实例属性初始化
// ========================================================================
/**
* 容器精灵ID
* @type {number}
*/
this.containerId = config.containerId;
/**
* 裁剪区域配置
* @type {Object}
*/
this.clipArea = {
x: config.clipArea.x || 0,
y: config.clipArea.y || 0,
width: config.clipArea.width || 0,
height: config.clipArea.height || 0
};
/**
* 排列方向:'vertical'(默认) | 'horizontal'
* @type {string}
*/
this.orientation = orientation;
/**
* 轴映射(操作码 + clipArea 取用字段),内部所有方向相关操作一律经此表
* @type {Object}
* @private
*/
this._axis = axis;
/**
* 主轴步长:纵向=行高(rowHeight),横向=项宽含间距(itemWidth)
* @type {number}
* @private
*/
this._step = step;
/**
* 每行高度(纵向语义保留字段;横向下等于主轴步长)
* @type {number}
*/
this.rowHeight = step;
/**
* 模板精灵配置
* @type {Object}
*/
this.templates = config.templates || null;
this.renderRow = config.renderRow || null;
/**
* 滑动灵敏度
* @type {number}
*/
this.scrollSensitivity = config.scrollSensitivity || 2;
/**
* 回弹动画速度
* @type {number}
*/
this.bounceSpeed = config.bounceSpeed || 1;
/**
* 是否启用滚动
* @type {boolean}
*/
this.enableScroll = config.enableScroll !== false;
/**
* 是否启用点击
* @type {boolean}
*/
this.enableClick = config.enableClick !== false;
// ========================================================================
// 运行时状态
// ========================================================================
/**
* 当前数据
* @type {Array}
* @private
*/
this._data = [];
/**
* 是否正在滑动
* @type {boolean}
* @private
*/
this._isSliding = false;
/**
* 是否已渲染
* @type {boolean}
* @private
*/
this._isRendered = false;
/**
* 已创建的精灵记录 (用于删除)
* @type {Array}
* @private
*/
this._createdSprites = [];
this._tagIndex = {};
this._nextTag = 1;
// ========================================================================
// 事件回调
// ========================================================================
/**
* 点击事件回调
* @type {Function|null}
* @param {string} templateName - 点击的模板类型名称
* @param {number} rowIndex - 行索引(从0开始)
* @param {Object} rowData - 该行的数据对象
*
* @example
* list.onClick = function(templateName, rowIndex, rowData) {
* if (templateName === 'button') {
* console.log('点击了第', rowIndex, '行的按钮');
* console.log('数据:', rowData);
* }
* };
*/
this.onClick = null;
/**
* 滚动事件回调
* @type {Function|null}
* @param {number} scrollY - 当前滚动位置
* @param {number} maxScrollY - 最大滚动位置
*
* @example
* list.onScroll = function(scrollY, maxScrollY) {
* var progress = scrollY / maxScrollY;
* console.log('滚动进度:', progress);
* };
*/
this.onScroll = null;
}
// ============================================================================
// 公共方法
// ============================================================================
/**
* 设置数据并渲染列表
*
* @param {Array} data - 数据数组,每个元素对应一行
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*
* @example
* list.setData([
* { title: '战绩1', score: '+100', time: '12:30' },
* { title: '战绩2', score: '-50', time: '12:25' },
* { title: '战绩3', score: '+200', time: '12:20' }
* ]);
*/
DynamicSpriteList.prototype.setData = function(data) {
if (!Array.isArray(data)) {
console.error('DynamicSpriteList.setData: data must be an array');
return this;
}
// 先清除旧内容
this.clear();
// 保存数据
this._data = data;
// 渲染列表
this._render();
return this;
};
/**
* 获取当前数据
*
* @returns {Array} 当前数据数组的副本
*/
DynamicSpriteList.prototype.getData = function() {
return this._data.slice();
};
/**
* 获取数据行数
*
* @returns {number} 数据行数
*/
DynamicSpriteList.prototype.getRowCount = function() {
return this._data.length;
};
/**
* 显示列表
*
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.show = function() {
set_self(this.containerId, 37, 1, 0, 0);
return this;
};
/**
* 隐藏列表
*
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.hide = function() {
set_self(this.containerId, 37, 0, 0, 0);
return this;
};
/**
* 清除所有动态创建的精灵
*
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.clear = function() {
for (var i = 0; i < this._createdSprites.length; i++) {
ifast_dllpritefromspritecopy(this.containerId, this._createdSprites[i].tag);
}
this._createdSprites = [];
this._tagIndex = {};
this._nextTag = 1;
this._isRendered = false;
this._isSliding = false;
return this;
};
/**
* 销毁列表实例
* 释放所有资源,清除所有精灵
*/
DynamicSpriteList.prototype.destroy = function() {
this.clear();
this.hide();
this._data = [];
this.onClick = null;
this.onScroll = null;
};
/**
* 滚动到指定位置
*
* @param {number} offset - 目标主轴偏移量(纵向=Y方向,横向=X方向)
* @param {boolean} [animated=true] - 是否使用动画
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.scrollTo = function(offset, animated) {
var axis = this._axis;
var clipStart = this.clipArea[axis.clipPos];
var clipSize = this.clipArea[axis.clipSize];
var current = get_self(this.containerId, axis.posOp, 0, 0, 0);
var target = clipStart - offset;
// 边界钳制(双向):先钳末端,再钳起点。
// 内容不足一屏时 min > clipStart,两条钳制叠加后恒为起点——这正是期望行为。
var contentSize = this._data.length * this._step;
var min = clipStart + clipSize - contentSize;
if (target < min) { target = min; }
if (target > clipStart) { target = clipStart; }
if (animated !== false) {
play_ani(1, this.containerId, axis.posOp, current, target, 0,
Math.abs(target - current), 0, 0, 0, this.bounceSpeed, 0, 0, 0);
} else {
set_self(this.containerId, axis.posOp, target, 0, 0, 0);
}
return this;
};
/**
* 滚动到顶部
*
* @param {boolean} [animated=true] - 是否使用动画
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.scrollToTop = function(animated) {
return this.scrollTo(0, animated);
};
/**
* 滚动到底部
*
* @param {boolean} [animated=true] - 是否使用动画
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.scrollToBottom = function(animated) {
var contentSize = this._data.length * this._step;
var maxScroll = contentSize - this.clipArea[this._axis.clipSize];
if (maxScroll < 0) {
maxScroll = 0;
}
return this.scrollTo(maxScroll, animated);
};
/**
* 滚动到指定行
*
* @param {number} rowIndex - 行索引(从0开始)
* @param {boolean} [animated=true] - 是否使用动画
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.scrollToRow = function(rowIndex, animated) {
var y = rowIndex * this._step;
return this.scrollTo(y, animated);
};
// ============================================================================
// 事件处理方法 - 需要在对应的事件回调中调用
// ============================================================================
/**
* 处理鼠标/触摸按下事件
* 需要在 utlmousedown 回调中调用
*
* @param {number} spid - 按下的精灵ID
* @param {number} x - 按下位置X坐标
* @param {number} y - 按下位置Y坐标
* @returns {boolean} 如果事件被处理返回 true
*
* @example
* // 在 SpriteEventController 或 Game_Modify 中
* utlmousedown: function(gameid, spid, downx, downy) {
* if (myList.handleMouseDown(spid, downx, downy)) {
* return; // 事件已处理
* }
* // 其他处理...
* }
*/
DynamicSpriteList.prototype.handleMouseDown = function(spid, x, y) {
if (spid === this.containerId) {
this._isSliding = false;
return true;
}
return false;
};
/**
* 处理鼠标/触摸移动事件
* 需要在 utlmousemove 回调中调用
*
* @param {number} spid - 移动的精灵ID
* @param {number} offsetMain - 主轴方向移动偏移量(纵向传 Y 方向偏移,横向传 X 方向偏移)
* @returns {boolean} 如果事件被处理返回 true
*
* @example
* // 在 SpriteEventController 或 Game_Modify 中(纵向列表传 offmovey,横向列表传 offmovex)
* utlmousemove: function(gameid, spid, downx, downy, movex, movey, timelong, offmovex, offmovey) {
* if (myList.handleMouseMove(spid, offmovey)) { // 横向列表改传 offmovex
* return; // 事件已处理
* }
* // 其他处理...
* }
*/
DynamicSpriteList.prototype.handleMouseMove = function(spid, offsetMain) {
if (!this.enableScroll) {
return false;
}
if (spid === this.containerId) {
if (Math.abs(offsetMain) > this.scrollSensitivity) {
var axis = this._axis;
// 增量移动容器(第4参 1 = 相对增量)
set_self(this.containerId, axis.posOp, offsetMain, 1, 0, 0);
this._isSliding = true;
if (typeof this.onScroll === 'function') {
var current = get_self(this.containerId, axis.posOp, 0, 0, 0);
var scrolled = this.clipArea[axis.clipPos] - current;
var maxScroll = this._data.length * this._step - this.clipArea[axis.clipSize];
this.onScroll(scrolled, maxScroll > 0 ? maxScroll : 0);
}
}
return true;
}
return false;
};
/**
* 处理鼠标/触摸松开事件
* 需要在 mouseup 回调中调用
*
* @param {number} spidDown - 按下时的精灵ID
* @param {number} spidUp - 松开时的精灵ID
* @param {number} upX - 松开位置X坐标
* @param {number} upY - 松开位置Y坐标
* @returns {boolean} 如果事件被处理返回 true
*
* @example
* // 在 SpriteEventController 或 Game_Modify 中
* mouseup: function(gameid, spid_down, downx, downy, spid_up, upx, upy, timelong) {
* if (myList.handleMouseUp(spid_down, spid_up, upx, upy)) {
* return; // 事件已处理
* }
* // 其他处理...
* }
*/
DynamicSpriteList.prototype.handleMouseUp = function(spidDown, spidUp, upX, upY) {
if (spidDown !== this.containerId) {
return false;
}
// 处理边界回弹
this._handleBounce();
// 处理点击(非滑动状态)
if (!this._isSliding && this.enableClick && spidDown === spidUp) {
this._handleClick(upX, upY);
}
return true;
};
/**
* 处理绘制开始事件
* 需要在 utlgamemydrawbegin 回调中调用,用于设置裁剪区域
*
* @param {number} spid - 正在绘制的精灵ID
* @returns {boolean} 如果事件被处理返回 true
*
* @example
* // 在 SpriteEventController 或 Game_Modify 中
* utlgamemydrawbegin: function(gameid, spid, times, timelong) {
* if (myList.handleDrawBegin(spid)) {
* return; // 已设置裁剪区域
* }
* // 其他处理...
* }
*/
DynamicSpriteList.prototype.handleDrawBegin = function(spid) {
if (spid === this.containerId) {
set_clip(0, 0,
this.clipArea.x,
this.clipArea.y,
this.clipArea.width,
this.clipArea.height
);
return true;
}
return false;
};
// ============================================================================
// 私有方法
// ============================================================================
/**
* 内部创建精灵并登记到 _tagIndex / _createdSprites
* @param {string} name - 精灵名称(模板键名或自定义名)
* @param {number} templateSpriteId - 模板精灵 ID
* @param {number} x - X 坐标
* @param {number} y - Y 坐标(已叠加 rowBaseY)
* @param {number} rowIndex - 所在行索引
* @param {boolean} clickable - 是否可点击
* @returns {*} 创建出的精灵 ID
* @private
*/
DynamicSpriteList.prototype._addSpriteInternal = function (name, templateSpriteId, x, y, rowIndex, clickable) {
var tag = this._nextTag++;
var spriteId = ifast_addtospritefromspritecopy(this.containerId, templateSpriteId, x, y, tag);
var rec = { tag: tag, spriteId: spriteId, rowIndex: rowIndex, name: name, clickable: (clickable !== false) };
this._createdSprites.push(rec);
this._tagIndex[tag] = rec;
return spriteId;
};
/**
* 渲染列表
* @private
*/
DynamicSpriteList.prototype._render = function() {
var axis = this._axis;
set_self(this.containerId, axis.posOp, this.clipArea[axis.clipPos], 0, 0);
set_self(this.containerId, axis.crossPosOp, this.clipArea[axis.crossClipPos], 0, 0);
var contentSize = this._data.length * this._step;
set_self(this.containerId, axis.sizeOp, contentSize, 0, 0);
if (this.templates) {
var tnames = Object.keys(this.templates);
if (tnames.length > 0) {
// 交叉轴尺寸取首个模板的对应尺寸(纵向=宽 op20,横向=高 op21)
var crossFromTpl = get_self(this.templates[tnames[0]].spriteId, axis.crossSizeOp, 0, 0, 0);
set_self(this.containerId, axis.crossSizeOp, crossFromTpl, 0, 0);
}
} else {
// renderRow 模式:交叉轴尺寸取裁剪区对应边,保证整个可视区都在容器 hit 区(点击/拖动检测)内
set_self(this.containerId, axis.crossSizeOp, this.clipArea[axis.crossClipSize], 0, 0);
}
for (var rowIndex = 0; rowIndex < this._data.length; rowIndex++) {
var rowData = this._data[rowIndex];
var rowBase = rowIndex * this._step;
if (this.renderRow) {
this.renderRow(this._makeRowContext(rowIndex, rowData, rowBase));
} else {
var names = Object.keys(this.templates);
for (var i = 0; i < names.length; i++) {
var name = names[i];
var t = this.templates[name];
var ox = (t.offset && t.offset.x) || 0;
var oy = (t.offset && t.offset.y) || 0;
var px = (this.orientation === 'horizontal') ? (rowBase + ox) : ox;
var py = (this.orientation === 'horizontal') ? oy : (rowBase + oy);
var sid = this._addSpriteInternal(name, t.spriteId, px, py, rowIndex, t.clickable !== false);
if (t.textProperty && rowData[t.textProperty] !== undefined) {
set_self(sid, 7, rowData[t.textProperty], 0, 0);
}
if (t.frameProperty && rowData[t.frameProperty] !== undefined) {
set_self(sid, 43, rowData[t.frameProperty], 0, 0);
}
}
}
}
this._isRendered = true;
};
/**
* 创建行上下文对象(传给 renderRow 回调)
* @param {number} rowIndex - 行索引
* @param {Object} rowData - 行数据
* @param {number} rowBase - 行基准坐标(纵向=Y,横向=X)
* @returns {Object} 行上下文
* @private
*/
DynamicSpriteList.prototype._makeRowContext = function (rowIndex, rowData, rowBase) {
var self = this;
var isH = (this.orientation === 'horizontal');
return {
rowIndex: rowIndex,
rowData: rowData,
rowBase: rowBase,
// 保留 rowBaseY:renderRow 回调的既有字段协议,不得破坏。
// 当前生产消费方为 RecordView.js(唯一使用本组件的调用方);
// 回归测试 DynamicSpriteList.test.js 对该字段有硬断言。
rowBaseY: rowBase,
orientation: this.orientation,
containerId: this.containerId,
addSprite: function (name, templateSpriteId, x, y) {
var px = isH ? (rowBase + x) : x;
var py = isH ? y : (rowBase + y);
return self._addSpriteInternal(name, templateSpriteId, px, py, rowIndex, true);
}
};
};
/**
* 处理边界回弹
* @private
*/
DynamicSpriteList.prototype._handleBounce = function() {
var axis = this._axis;
var current = get_self(this.containerId, axis.posOp, 0, 0, 0);
var contentSize = get_self(this.containerId, axis.sizeOp, 0, 0, 0);
var clipStart = this.clipArea[axis.clipPos];
var clipSize = this.clipArea[axis.clipSize];
var target = current;
var needBounce = false;
if (contentSize <= clipSize) {
// 内容不足一屏,回弹到起点
if (current !== clipStart) {
target = clipStart;
needBounce = true;
}
} else {
if (current > clipStart) {
// 越过起点
target = clipStart;
needBounce = true;
} else {
var min = clipStart + clipSize - contentSize;
if (current < min) {
// 越过末端
target = min;
needBounce = true;
}
}
}
if (needBounce) {
play_ani(1, this.containerId, axis.posOp, current, target, 0,
Math.abs(target - current), 0, 0, 0, this.bounceSpeed, 0, 0, 0);
}
};
/**
* 处理点击事件
* @param {number} x - 点击X坐标
* @param {number} y - 点击Y坐标
* @private
*/
DynamicSpriteList.prototype._handleClick = function(x, y) {
// 裁剪区外的点击一律不算数。
// 原因:引擎的 set_clip 只裁【绘制】、不裁【命中】,而 _render 会把容器的主轴尺寸
// 撑到内容全长(远超可视区)。于是滑出可视区、屏幕上已经看不见的那些项,其矩形
// 仍在容器内,ifast_check_add 照样判为命中 —— 用户点到列表旁的空白带,却选中了
// 一个根本看不见的项。这里按 clipArea 兜住,横纵通用。
var a = this.clipArea;
if (x < a.x || x >= a.x + a.width || y < a.y || y >= a.y + a.height) {
return;
}
var clickedTag = ifast_check_add(this.containerId, x, y);
if (clickedTag === -99999999) { return; }
var rec = this._tagIndex[clickedTag];
if (!rec || rec.clickable === false) { return; }
if (typeof this.onClick === 'function' && rec.rowIndex < this._data.length) {
this.onClick(rec.name, rec.rowIndex, this._data[rec.rowIndex]);
}
};
// ============================================================================
// 静态工具方法
// ============================================================================
/**
* 创建简单列表的快捷方法
*
* @static
* @param {Object} options - 配置选项
* @param {number} options.containerId - 容器精灵ID
* @param {number} options.itemTemplateId - 列表项模板精灵ID
* @param {Object} options.clipArea - 裁剪区域
* @param {number} options.rowHeight - 行高
* @returns {DynamicSpriteList} 列表实例
*
* @example
* var simpleList = DynamicSpriteList.createSimple({
* containerId: 100,
* itemTemplateId: 101,
* clipArea: { x: 50, y: 100, width: 600, height: 400 },
* rowHeight: 60
* });
*/
DynamicSpriteList.createSimple = function(options) {
return new DynamicSpriteList({
containerId: options.containerId,
clipArea: options.clipArea,
rowHeight: options.rowHeight,
templates: {
item: {
spriteId: options.itemTemplateId,
offset: { x: 0, y: 0 }
}
}
});
};
// ============================================================================
// 导出
// ============================================================================
// 兼容多种模块系统
if (typeof module !== 'undefined' && module.exports) {
module.exports = DynamicSpriteList;
} else if (typeof window !== 'undefined') {
window.DynamicSpriteList = DynamicSpriteList;
}