模板工程升级修改

This commit is contained in:
2026-08-11 22:17:54 +08:00
parent 047561675b
commit 3b7c103387
38 changed files with 16607 additions and 1205 deletions
@@ -84,7 +84,21 @@
* 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 会清除旧行、重新创建新行
@@ -176,6 +190,26 @@
* }
* }
*/
/**
* 主轴/交叉轴的引擎操作码与 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) {
// ========================================================================
// 参数验证
@@ -189,8 +223,11 @@ function DynamicSpriteList(config) {
if (!config.clipArea) {
throw new Error('DynamicSpriteList: clipArea is required');
}
if (typeof config.rowHeight !== 'number' || config.rowHeight <= 0) {
throw new Error('DynamicSpriteList: rowHeight must be a positive number');
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';
@@ -220,11 +257,31 @@ function DynamicSpriteList(config) {
};
/**
* 每行高度
* 排列方向:'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 = config.rowHeight;
this.rowHeight = step;
/**
* 模板精灵配置
* @type {Object}
@@ -431,31 +488,31 @@ DynamicSpriteList.prototype.destroy = function() {
/**
* 滚动到指定位置
*
* @param {number} y - 目标Y坐标
* @param {number} offset - 目标主轴偏移量(纵向=Y方向,横向=X方向)
* @param {boolean} [animated=true] - 是否使用动画
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.scrollTo = function(y, animated) {
var currentY = get_self(this.containerId, 19, 0, 0, 0);
var targetY = this.clipArea.y - y;
// 边界限制
var contentHeight = this._data.length * this.rowHeight;
var minY = this.clipArea.y + this.clipArea.height - contentHeight;
if (targetY < minY) {
targetY = minY;
}
if (targetY > this.clipArea.y) {
targetY = this.clipArea.y;
}
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, 19, currentY, targetY, 0,
Math.abs(targetY - currentY), 0, 0, 0, this.bounceSpeed, 0, 0, 0);
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, 19, targetY, 0, 0, 0);
set_self(this.containerId, axis.posOp, target, 0, 0, 0);
}
return this;
};
@@ -476,8 +533,8 @@ DynamicSpriteList.prototype.scrollToTop = function(animated) {
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.scrollToBottom = function(animated) {
var contentHeight = this._data.length * this.rowHeight;
var maxScroll = contentHeight - this.clipArea.height;
var contentSize = this._data.length * this._step;
var maxScroll = contentSize - this.clipArea[this._axis.clipSize];
if (maxScroll < 0) {
maxScroll = 0;
}
@@ -492,7 +549,7 @@ DynamicSpriteList.prototype.scrollToBottom = function(animated) {
* @returns {DynamicSpriteList} 返回自身,支持链式调用
*/
DynamicSpriteList.prototype.scrollToRow = function(rowIndex, animated) {
var y = rowIndex * this.rowHeight;
var y = rowIndex * this._step;
return this.scrollTo(y, animated);
};
@@ -531,36 +588,35 @@ DynamicSpriteList.prototype.handleMouseDown = function(spid, x, y) {
* 需要在 utlmousemove 回调中调用
*
* @param {number} spid - 移动的精灵ID
* @param {number} offsetY - Y方向移动偏移量
* @param {number} offsetMain - 主轴方向移动偏移量(纵向传 Y 方向偏移,横向传 X 方向偏移)
* @returns {boolean} 如果事件被处理返回 true
*
*
* @example
* // 在 SpriteEventController 或 Game_Modify 中
* // 在 SpriteEventController 或 Game_Modify 中(纵向列表传 offmovey,横向列表传 offmovex)
* utlmousemove: function(gameid, spid, downx, downy, movex, movey, timelong, offmovex, offmovey) {
* if (myList.handleMouseMove(spid, offmovey)) {
* if (myList.handleMouseMove(spid, offmovey)) { // 横向列表改传 offmovex
* return; // 事件已处理
* }
* // 其他处理...
* }
*/
DynamicSpriteList.prototype.handleMouseMove = function(spid, offsetY) {
DynamicSpriteList.prototype.handleMouseMove = function(spid, offsetMain) {
if (!this.enableScroll) {
return false;
}
if (spid === this.containerId) {
if (Math.abs(offsetY) > this.scrollSensitivity) {
// 增量移动容器
set_self(this.containerId, 19, offsetY, 1, 0, 0);
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 currentY = get_self(this.containerId, 19, 0, 0, 0);
var scrollY = this.clipArea.y - currentY;
var contentHeight = this._data.length * this.rowHeight;
var maxScrollY = contentHeight - this.clipArea.height;
this.onScroll(scrollY, maxScrollY > 0 ? maxScrollY : 0);
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;
@@ -661,29 +717,30 @@ DynamicSpriteList.prototype._addSpriteInternal = function (name, templateSpriteI
* @private
*/
DynamicSpriteList.prototype._render = function() {
set_self(this.containerId, 18, this.clipArea.x, 0, 0);
set_self(this.containerId, 19, this.clipArea.y, 0, 0);
var contentHeight = this._data.length * this.rowHeight;
set_self(this.containerId, 21, contentHeight, 0, 0);
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) {
var w = get_self(this.templates[tnames[0]].spriteId, 20, 0, 0, 0);
set_self(this.containerId, 20, w, 0, 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 区(点击/拖动检测)内。
// 否则容器保持编辑器预置窄宽度,超出部分点击/拖动报的 spid≠containerId 而失效(对账 bak set_self(fSpid,20,bg宽))。
set_self(this.containerId, 20, this.clipArea.width, 0, 0);
// 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 rowY = rowIndex * this.rowHeight;
var rowBase = rowIndex * this._step;
if (this.renderRow) {
this.renderRow(this._makeRowContext(rowIndex, rowData, rowY));
this.renderRow(this._makeRowContext(rowIndex, rowData, rowBase));
} else {
var names = Object.keys(this.templates);
for (var i = 0; i < names.length; i++) {
@@ -691,7 +748,9 @@ DynamicSpriteList.prototype._render = function() {
var t = this.templates[name];
var ox = (t.offset && t.offset.x) || 0;
var oy = (t.offset && t.offset.y) || 0;
var sid = this._addSpriteInternal(name, t.spriteId, ox, rowY + oy, rowIndex, t.clickable !== false);
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);
}
@@ -708,19 +767,27 @@ DynamicSpriteList.prototype._render = function() {
* 创建行上下文对象(传给 renderRow 回调)
* @param {number} rowIndex - 行索引
* @param {Object} rowData - 行数据
* @param {number} rowBaseY - 行基准 Y 坐标
* @param {number} rowBase - 行基准坐标(纵向=Y,横向=X)
* @returns {Object} 行上下文
* @private
*/
DynamicSpriteList.prototype._makeRowContext = function (rowIndex, rowData, rowBaseY) {
DynamicSpriteList.prototype._makeRowContext = function (rowIndex, rowData, rowBase) {
var self = this;
var isH = (this.orientation === 'horizontal');
return {
rowIndex: rowIndex,
rowData: rowData,
rowBaseY: rowBaseY,
rowBase: rowBase,
// 保留 rowBaseY:renderRow 回调的既有字段协议,不得破坏。
// 当前生产消费方为 RecordView.js(唯一使用本组件的调用方);
// 回归测试 DynamicSpriteList.test.js 对该字段有硬断言。
rowBaseY: rowBase,
orientation: this.orientation,
containerId: this.containerId,
addSprite: function (name, templateSpriteId, x, y) {
return self._addSpriteInternal(name, templateSpriteId, x, rowBaseY + y, rowIndex, true);
var px = isH ? (rowBase + x) : x;
var py = isH ? y : (rowBase + y);
return self._addSpriteInternal(name, templateSpriteId, px, py, rowIndex, true);
}
};
};
@@ -730,38 +797,39 @@ DynamicSpriteList.prototype._makeRowContext = function (rowIndex, rowData, rowBa
* @private
*/
DynamicSpriteList.prototype._handleBounce = function() {
var currentY = get_self(this.containerId, 19, 0, 0, 0);
var contentHeight = get_self(this.containerId, 21, 0, 0, 0);
var clipY = this.clipArea.y;
var clipH = this.clipArea.height;
var targetY = currentY;
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 (contentHeight <= clipH) {
// 内容不足一屏,回弹到顶部
if (currentY !== clipY) {
targetY = clipY;
if (contentSize <= clipSize) {
// 内容不足一屏,回弹到起点
if (current !== clipStart) {
target = clipStart;
needBounce = true;
}
} else {
if (currentY > clipY) {
// 超出顶部
targetY = clipY;
if (current > clipStart) {
// 越过起点
target = clipStart;
needBounce = true;
} else {
var minY = clipY + clipH - contentHeight;
if (currentY < minY) {
// 超出底部
targetY = minY;
var min = clipStart + clipSize - contentSize;
if (current < min) {
// 越过末端
target = min;
needBounce = true;
}
}
}
if (needBounce) {
play_ani(1, this.containerId, 19, currentY, targetY, 0,
Math.abs(targetY - currentY), 0, 0, 0, this.bounceSpeed, 0, 0, 0);
play_ani(1, this.containerId, axis.posOp, current, target, 0,
Math.abs(target - current), 0, 0, 0, this.bounceSpeed, 0, 0, 0);
}
};
@@ -772,6 +840,16 @@ DynamicSpriteList.prototype._handleBounce = function() {
* @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];