Cocos Creator拖拽排序列表:ScrollView冲突与坐标换算实现
简介在Cocos Creator游戏与交互界面开发中拖拽排序是常见需求。面向有一定引擎基础、希望实现可定制排序列表的开发者这是一份可直接参考的Cocos Creator拖拽排序列表工程示例覆盖场景搭建与脚本逻辑。压缩包共16个文件约243KB包含TypeScript脚本、json配置、meta元数据及fire场景文件等目录结构清晰便于直接导入或对照学习。目前已有114人学习下载。示例围绕拖拽移动、排序存储、状态更新三个核心环节展开通过事件监听分别处理拖拽开始、移动与结束用数组实时记录元素位置拖拽完成后刷新节点布局使排序结果同步到界面。工程还展示了项目级配置方式可作为同类交互的参考模板帮助开发者快速迁移到实际项目减少从零调试的工作量。对于刚接触Cocos Creator的开发者也能从中理解事件注册、数据存储与视图刷新之间的协作方式。1. Cocos Creator 拖拽排序列表一次把“手指移动”变成“数据重排”用 Cocos Creator 做列表界面时“渲染一组数据”很容易ScrollView 加 Layout 拖进去就完事。可一旦需求变成“手指按住某个 Item 直接拖动换位”引擎自带的滚动组件就哑火了——它会把你的一切滑动当成滚动手势Item 跟着手指走了松手后数据却没变。这套拖拽排序列表资源解决的就是这个最后一公里问题按住 Item、越过阈值进入拖拽态、实时换位、松手回写排序结果。适合背包整理、队伍编组、收藏夹排序以及任何“可视化编排顺序”的场景也适合第一次接触触摸事件与 UI 坐标换算的 Cocos Creator 新手作为完整案例来拆。2. 方案选型与触摸事件链为什么 ScrollView 不帮你排手写逻辑到底少在哪2.1 一次触摸的传递路径与 ScrollView 接管条件先说清楚最容易被忽视的底层问题手指落下去之后事件是怎么走到 Item 上的。Cocos Creator 里UI 触摸事件从根节点开始命中测试再沿着节点树向下派发触摸点所在的最上层节点会先收到 TOUCH_START然后事件继续冒泡到父节点。ScrollView 组件监听的是它自己节点上的触摸事件只要它收到 TOUCH_MOVE 且位移超过内部判定阈值就会把这次触摸标记为“滚动”之后整个手势都会被它消费。这意味着一个最基本的冲突手指按在 Item 上滑动时Item 和 ScrollView 都能收到触摸事件。如果你不做任何干预ScrollView 会优先把手指位移解释成“列表滚动”你的 Item 换位逻辑就永远抢不到控制权。解决思路不是去改 ScrollView 源码而是在 Item 或者更上层的手势节点上把事件拦下来让 ScrollView 根本收不到这次触摸。这里要分清“手指按在空白处”和“手指按在 Item 上”两种场景。用户滚动列表时手指大概率落在 Item 上所以不能无条件拦截所有触摸否则列表彻底滚不动了。标准做法是先做一次命中检测判断触摸点是否落在某个 Item 的包围盒里命中则进入“可能拖拽”状态并且立刻停止事件向 ScrollView 传播没命中就不拦截让 ScrollView 正常滚动。2.2 ScrollView 只管理视口与 content不管理子节点顺序ScrollView 的职责边界很窄它维护一个 content 节点的位置让 content 在它的可视区域内滚动再配合 Mask 做裁剪。至于 content 下面挂了多少个子节点、这些子节点谁先谁后ScrollView 完全不管。排序这件事引擎层面没有内置组件第三方插件要么停留在旧版本 API要么在事件冲突上处理得不够干净升级引擎后大概率要重写。自己写的话核心成本其实很低。排序本质是两件事第一调整 content 下子节点的兄弟顺序第二把这个顺序同步回数据结构里。兄弟顺序的调整ScrollView 管不着但 Layout 组件会帮忙——当你调用setSiblingIndex修改节点顺序后Layout 会自动把视觉位置重排到对应顺序你连手动计算每个 Item 的新坐标都不用做。所以我的选择很明确不用插件自己维护一个手势监听层拖拽期间把被拖拽的 Item 临时摘出来悬浮松手时再塞回 content 里让 Layout 负责最终布局。这个方案对引擎版本依赖小2.x 和 3.x 之间只差事件名和少数接口逻辑完全能迁移。2.3 最小骨架命中检测、位移累加、顺序回写在写完整脚本前先把最关键的三步提炼成骨架。第一步TOUCH_START 时从 content 的子节点里找出触摸点命中的 Item记录它的世界坐标起始点第二步TOUCH_MOVE 时把 Item 世界坐标改成“起始坐标加位移”并实时计算它应该插入到哪个位置第三步TOUCH_END 时把 Item 放回 content按最新顺序更新数据数组。onTouchStart(event: EventTouch) { const uiPos event.getUILocation(); const item this.pickItem(uiPos); if (!item) return; event.propagationStopped true; // v2.x 里是 event.stopPropagation() this.draggingItem item; this.startUIPos uiPos.clone(); this.draggingItem.getWorldPosition(this.startWorldPos); this.draggingItem.setParent(this.dragLayer, true); } onTouchMove(event: EventTouch) { if (!this.draggingItem) return; const uiPos event.getUILocation(); const dx uiPos.x - this.startUIPos.x; const dy uiPos.y - this.startUIPos.y; this.draggingItem.setWorldPosition( this.startWorldPos.x dx, this.startWorldPos.y dy, 0 ); this.applySort(); } onTouchEnd(event: EventTouch) { if (!this.draggingItem) return; this.draggingItem.setParent(this.contentNode, true); this.draggingItem.setSiblingIndex(this.targetIndex); this.draggingItem null; this.reorderData(); }这段代码里最需要注意的是setParent(this.dragLayer, true)这一步。第二个参数是“保持世界坐标”不传的话节点会直接跳到新父节点的原点Item 瞬间飞走。拖拽期间 Item 要悬浮在 dragLayer 上是因为如果它继续留在 content 里Layout 会每帧重新布局它你手动设置的世界坐标会被 Layout 覆盖结果就是 Item 一边被你拖着走一边被 Layout 拽回去视觉上疯狂抖动。这个坑后面避坑章节还会展开讲。3. 排序核心脚本一次到位索引重算与 Layout 实时换位3.1 场景与预制体节点结构、锚点与拖拽层先按下面的节点结构搭场景。核心是把“事件接收层”和“渲染层”分开HandLayer 是一个空的节点挂在 ScrollView 的上层专门负责接收触摸dragLayer 也放在 HandLayer 下面但它在 ScrollView 的渲染层级之上拖拽期间 Item 移到这里后不会被 ScrollView 的 Mask 裁剪掉。Canvas ├── ScrollView │ ├── view │ │ ├── mask │ │ │ └── contentLayout 容器放所有 Item │ │ └── dragLayer空节点锚点 0.5,0.5位于 mask 之后 └── HandLayer挂 SortableList 脚本层级在 ScrollView 之上Item 预制体的节点结构建议是根节点 UITransform 尺寸固定比如宽 600、高 120锚点设(0.5, 0.5)下面挂一个背景 Sprite一个显示文本的 Label再给根节点挂 Button 组件如果不想要点击效果挂一个 Button 只是为了拿到过渡动画反馈。锚点统一为 0.5 很重要后面排序判定直接用节点的worldPosition代表 Item 中心点如果锚点不统一你还要额外减去锚点偏移代码复杂度直接翻倍。content 节点上挂 Layout 组件type 选 VERTICALresizeMode 选 CONTAINERspacingY 按设计稿给数值。content 的锚点建议设(0.5, 1.0)配合 Layout 的 verticalDirection 默认 TOP_TO_BOTTOM这样从视觉上最接近绝大多数列表的自然排布。3.2 SortableList 脚本一次到位索引重算脚本挂到 HandLayer 上用属性面板把 content、dragLayer、scrollView 三个节点拖进去。import { _decorator, Component, EventTouch, Node, ScrollView, UITransform, Vec2, Vec3 } from cc; const { ccclass, property } _decorator; ccclass(SortableList) export class SortableList extends Component { property({ type: Node, tooltip: Layout 容器一般是 ScrollView 的 content }) contentNode: Node null; property({ type: Node, tooltip: 拖拽悬浮层在 ScrollView 上方 }) dragLayer: Node null; property({ type: ScrollView, tooltip: 拖拽期间临时禁用滚动 }) scrollView: ScrollView null; property({ tooltip: 小于该位移视为点击不进入拖拽 }) clickThreshold 20; private draggingItem: Node | null null; private startUIPos: Vec2 new Vec2(); private startWorldPos: Vec3 new Vec3(); private beginDrag false; private targetIndex -1; onEnable() { this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this); this.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this); this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this); this.node.on(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this); } onDisable() { this.node.off(Node.EventType.TOUCH_START, this.onTouchStart, this); this.node.off(Node.EventType.TOUCH_MOVE, this.onTouchMove, this); this.node.off(Node.EventType.TOUCH_END, this.onTouchEnd, this); this.node.off(Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this); } private onTouchStart(event: EventTouch) { const uiPos event.getUILocation(); const item this.pickItem(uiPos); if (!item) return; // 命中 Item 后立即拦截避免 ScrollView 滚动手势抢占 event.propagationStopped true; this.draggingItem item; this.startUIPos.set(uiPos.x, uiPos.y); this.draggingItem.getWorldPosition(this.startWorldPos); this.beginDrag false; this.targetIndex item.getSiblingIndex(); // 移入拖拽层保持世界坐标避免 Layout 干扰 this.draggingItem.setParent(this.dragLayer, true); this.scrollView.enabled false; } private onTouchMove(event: EventTouch) { if (!this.draggingItem) return; const uiPos event.getUILocation(); event.propagationStopped true; const dx uiPos.x - this.startUIPos.x; const dy uiPos.y - this.startUIPos.y; if (!this.beginDrag) { if (Math.abs(dx) Math.abs(dy) this.clickThreshold) return; this.beginDrag true; } this.draggingItem.setWorldPosition( this.startWorldPos.x dx, this.startWorldPos.y dy, 0 ); this.applySort(); } private onTouchEnd() { if (!this.draggingItem) return; // 放回 content保持世界坐标再指定落点索引 this.draggingItem.setParent(this.contentNode, true); this.draggingItem.setSiblingIndex(this.targetIndex); const layout this.contentNode.getComponent(cc.Layout) as any; if (layout layout.updateLayout) { layout.updateLayout(); } this.draggingItem null; this.scrollView.enabled true; this.reorderData(); } private pickItem(uiPos: Vec2): Node | null { const children this.contentNode.children; for (let i children.length - 1; i 0; i--) { const child children[i]; const ut child.getComponent(UITransform); if (ut ut.getBoundingBoxToWorld().contains(uiPos)) { return child; } } return null; } private applySort() { if (!this.draggingItem) return; const children this.contentNode.children; let insertCount 0; // 垂直列表统计有多少个兄弟的中心在拖拽项中心之上 for (const child of children) { if (child.worldPosition.y this.draggingItem.worldPosition.y) { insertCount; } } // 目标索引范围 [0, children.length] this.targetIndex Math.min(insertCount, children.length); } private reorderData() { const order: string[] []; for (const child of this.contentNode.children) { order.push(child.name); } console.log(排序结果:, order.join( - )); } }代码逻辑按触摸三个阶段拆开。pickItem用getBoundingBoxToWorld().contains(uiPos)判断触摸点是否落在 Item 的世界包围盒里从后往前遍历是为了让层级靠后的 Item 优先被命中——Cocos 里同级节点越靠后渲染层级越高。命中后立即把 Item 挪到 dragLayer从这一刻起它不再受 content 的 Layout 约束你拖到哪就是哪。applySort是排序判定的核心。它遍历 content 下所有普通 Item统计“中心点在拖拽项中心上方”的数量。这个数量就是拖拽项应该插入的位置它上面有几个兄弟它就排在第几个。举例来说列表从上到下是 A、B、C、D你拖着 D 往上移当 D 的中心越过 C 的中心时C 的中心就在 D 上方insertCount 变成 3D 的目标索引就是 3松手后 D 会落到 C 的位置。这个过程每帧重算手指跨过几个 Item索引一次到位不会出现“拖一个要等好几帧才追上”的情况。setParent(this.contentNode, true)之后的setSiblingIndex需要注意顺序。先保持世界坐标放回去再改兄弟索引最后手动调用 Layout 的updateLayout让节点在视觉上立刻归位。如果省略最后一步要等下一帧 Layout 自动刷新视觉上会有一帧的停顿。3.3 双向扩展水平列表与 Item 尺寸不一致的处理上面的脚本默认列表是垂直排布判据是worldPosition.y。改成水平列表只需要把applySort里的比较换成 x 轴统计“中心点在拖拽项左侧的兄弟数量”contentNode的 Layout 类型改成 HORIZONTAL其他逻辑不用动。Item 尺寸不一致时用中心点做判定会产生一点误差。比如拖拽项高度 200上方兄弟 A 高度 50、下方兄弟 B 高度 200拖拽项底部已经进入 B 的范围但只要拖拽项中心还在 A 的中心之上排序就不会触发。想要更精准可以把比较条件从“中心点”改成“拖拽项中心 vs 兄弟项上/下边界”也就是读取兄弟节点的 UITransform 高度后用worldPosition.y height / 2和worldPosition.y - height / 2拼出上下边界再比较。大多数列表 Item 高度一致中心点方案足够用。4. 接入配置与坐标换算ScrollView、Layout 和触摸点的九组参数4.1 ScrollView、Layout、Item 的关键参数清单拖拽排序能不能稳定跑起来一半靠脚本一半靠配置。下面这份参数表是我每次接入时都会检查一遍的按组件分好类。组件参数推荐值说明ScrollViewhorizontal / vertical按列表方向二选一两个都勾会导致手势判定方向冲突ScrollViewinertiafalse惯性会让 content 在松手后继续滑动拖拽场景建议关掉ScrollViewelastictrue边缘回弹保留但 bounceDuration 建议调到 0.2 以下ScrollViewblockInputWhileScrollingfalse滚动中不吞掉输入拖拽切换时更跟手LayouttypeVERTICAL / HORIZONTAL与列表方向一致LayoutresizeModeCONTAINER让 content 尺寸随子节点自动扩展LayoutspacingY / spacingX10与设计稿一致影响排序手感但不大Item UITransformanchor(0.5, 0.5)保证 worldPosition 即中心点Item ButtontransitionSCALE / NONE推荐 SCALE拖拽时视觉反馈更明显ScrollView 的 inertia 在拖拽场景里建议关掉。原因很简单惯性滚动发生在松手之后如果用户拖到一个位置松手content 还在往前滑排序结果和用户预期会差一截。elastic 可以保留但如果你发现拖拽到边缘时 ScrollView 自己弹了一下导致 Item 位置错乱就把 elastic 也关掉。Layout 的 resizeMode 用 CONTAINER意思是 content 的尺寸由子节点撑开。如果用默认的 NONEcontent 不会自动变高滚动区域就没了。如果列表只有固定几个 ItemCHILDREN 模式也可以但 CONTAINER 对动态增删更友好我默认都用它。4.2 坐标换算getUILocation、世界坐标与局部坐标别混用拖拽排序里最容易翻车的就是坐标系混用。很多新手直接用event.getLocation()拿到的屏幕像素坐标去和 UI 的世界坐标比较结果在不同分辨率适配下完全对不上Item 的移动轨迹和手指错开一大截。Cocos Creator 里触摸事件有两个常用的取坐标方法。getLocation()返回的是屏幕像素坐标单位是设备像素getUILocation()返回的是 UI 坐标已经考虑了 Canvas 适配和缩放可以直接用来和 UI 节点的世界坐标做计算。所以代码里所有触摸点获取都用getUILocation()。移动 Item 时我用的公式是“起始世界坐标 触摸位移增量”。这个方式的优势在于绕开了父节点坐标转换——你不需要去把 Item 的世界坐标转成 dragLayer 的局部坐标再赋值避免了局部坐标原点不一致带来的偏移。如果你的需求里必须转换成某个节点的局部坐标正确做法是调用UITransform.convertToNodeSpaceAR()传入的参数是new Vec3(uiPos.x, uiPos.y, 0)而不是直接传屏幕坐标const uiPos event.getUILocation(); const localPos dragLayer.getComponent(UITransform).convertToNodeSpaceAR( new Vec3(uiPos.x, uiPos.y, 0) );注意convertToNodeSpaceAR的 AR 后缀表示“以锚点为原点”不带 AR 的版本是以节点左下角为原点。拖拽场景里用 AR 版本更直观因为锚点是 0.5 时转换后的坐标天然就是 Item 中心应该所在的位置。4.3 接入核对清单每次在新项目接入这套拖拽排序我都会按下面的清单检查一遍顺序不能乱第一确认 HandLayer 在 ScrollView 的层级之上。如果 HandLayer 被 ScrollView 挡住触摸事件根本到不了它。第二确认 contentNode 挂载了 Layout 组件且 Layout 的 type 与列表方向一致。第三确认 dragLayer 没有被 Mask 组件覆盖否则拖出去的 Item 会被裁剪掉。第四确认 Item 预制体根节点的 UITransform 尺寸和锚点设置正确。第五在真机上验证一次坐标差异编辑器里的模拟器和真机在触摸坐标的来源上偶尔会有细微差别以真机为准。5. 避坑拖拽抖动、点按误排、边界跑偏与 v2/v3 API 差异5.1 拖拽中 Item 疯狂抖动乱跳现象按住 Item 拖动时Item 不停颤抖有时一帧飞出几屏远松手后顺序完全乱了。原因最常见的是没有在拖拽开始时把 Item 从 content 里摘出来。Item 继续挂在 content 下Layout 组件每个渲染帧都会重新布局所有子节点把 Item 的位置强制拉回它当前兄弟索引对应的坐标。你手动 setWorldPosition 设置了一个位置下一帧 Layout 又把它拽回去视觉表现就是剧烈抖动。另一种情况是每次 TOUCH_MOVE 都调用 setSiblingIndex索引频繁变化导致 Layout 反复重排抖动同样明显。解决拖拽开始时执行setParent(this.dragLayer, true)拖拽期间 Item 移出 Layout 管辖范围结束时再保持世界坐标放回 content。排序索引的重算只在跨越兄弟节点边界时更新不要每帧都去 setSiblingIndex。如果移出后依然抖检查 dragLayer 是否也被上级节点上的 Layout 组件管理dragLayer 本身不能挂 Layout。5.2 手指只轻点了一下列表顺序就变了现象用户手指按在 Item 上没怎么移动只是点击一下松手后 Item 就跳到了列表头或列表尾。原因TOUCH_MOVE 在手指有轻微抖动时也会触发位移只有一两个像素但排序逻辑已经开始跑。applySort里计算的 insertCount 瞬时变化松手时把 Item 塞到了错误位置。另一个原因是setSiblingIndex的目标索引没有做范围 clamp越界后引擎自动把它塞到首尾。解决加一个位移阈值判断代码里就是clickThreshold 20。在位移绝对值之和超过阈值之前只记录坐标不进入拖拽状态。这个阈值不是越大越好设太大会让拖拽启动变得迟钝20 像素是我在 640p 到 1080p 分辨率下试下来手感比较折中的值。目标索引计算结束后用Math.min(insertCount, children.length)做一次 clamp杜绝越界。5.3 拖到 ScrollView 边界Item 跑偏现象把 Item 拖到 ScrollView 可视区域顶部或底部时Item 位置突然与手指脱离或者 Item 直接消失在滚动区域内。原因ScrollView 在拖拽期间仍然处于启用状态content 会随着手指越界产生弹性偏移或者滚动。你用的是“起始世界坐标 位移增量”来定位 Item但 content 本身也在位置变化两段位移叠在一起Item 就跑偏了。如果 dragLayer 在 ScrollView 内部且被 Mask 裁剪Item 拖出视口时还会直接被裁掉。解决拖拽开始时把this.scrollView.enabled false从源头禁止 ScrollView 参与这次手势。同时保证 dragLayer 在 Mask 节点之下、同级且渲染层级更靠后让拖拽中超出列表视口的 Item 依然完整显示。如果你需要实现“拖到边缘自动滚动”就不能简单禁用 ScrollView 了而是要在自动滚动时把 content 的位移量同步补偿到 Item 的坐标上这个处理放在后面进阶章节。5.4 Item 尺寸不一致导致排序判定错位现象Item 有高有矮时拖拽项明明已经超过下方矮 Item 的中心排序却不触发或者超过上方高 Item 的底部边缘排序提前触发。原因applySort里用的是“中心点 vs 中心点”的判定这只在 Item 高度一致时成立。高度不一致时两个 Item 的中心点不能代表它们相互覆盖的视觉范围。解决把比较口径从中心点改成边界。比如判断拖拽项是否应该插到某兄弟后面应该用“拖拽项中心是否超过了兄弟的下边界”垂直列表向下拖动的情况而不是“拖拽项中心是否超过了兄弟中心”。代码实现时在 applySort 里读取兄弟节点的 UITransform用worldPosition.y - height / 2得到下边界再比较。资源里我把 Item 高度统一设成定值省掉这个分支你有可变高度需求时记得把这段逻辑补上。5.5 v2.x 与 v3.x 事件写法混用现象从网上复制一段 2.x 的拖拽代码到 3.x 项目里编译报错提示 EventTouch 上没有 stopPropagation 方法或者节点监听事件名的写法不识别。原因2.x 和 3.x 在触摸事件 API 上有几个明确的差异。2.x 用event.stopPropagation()停止事件传递3.x 改为event.propagationStopped true2.x 监听事件用node.on(touch-move, ...)3.x 用node.on(Node.EventType.TOUCH_MOVE, ...)3.x 里setPosition与setWorldPosition都要求传三个参数2.x 有时接受两个参数。解决以 3.x 为准遇到旧代码按下面表格替换不要两套写法混着用。功能2.x 写法3.x 写法停止传递event.stopPropagation()event.propagationStopped true监听触摸node.on(touch-move)node.on(Node.EventType.TOUCH_MOVE)获取 UI 坐标event.getUILocation()event.getUILocation()设置世界坐标node.setWorldPosition(x, y)node.setWorldPosition(x, y, 0)替换时特别留意第三行。getUILocation和getUILocation只差一个字母拼错不会立刻报错而是会在运行时返回 undefinedItem 直接飞到原点属于最难排查的一类问题。6. 进阶边缘自动滚动与松手后的数据回写自动滚动是拖拽排序在长列表场景下绕不开的加分项。列表超出 ScrollView 可视区域后用户把 Item 拖到边缘列表如果不能自动跟着滚动排序就只能局限于当前屏内的数据这个功能就废了一半。实现的核心不是“让 ScrollView 滚”而是“滚动的同时补偿拖拽项坐标”否则 Item 会像被钉在原地一样跟着列表一起滚出屏幕。我的做法是在 TOUCH_MOVE 里检测触摸点是否进入 ScrollView 可视区域的上下边缘带然后记录一个滚动方向在 update 循环里按帧移动 content。private autoScrollDir 0; private checkEdge(event: EventTouch) { const uiPos event.getUILocation(); const rect this.scrollView.getComponent(UITransform)!.getBoundingBoxToWorld(); const EDGE 40; if (uiPos.y rect.yMax - EDGE) { this.autoScrollDir 1; } else if (uiPos.y rect.yMin EDGE) { this.autoScrollDir -1; } else { this.autoScrollDir 0; } } private onUpdate(dt: number) { if (!this.draggingItem || this.autoScrollDir 0) return; const content this.scrollView.content; const step this.autoScrollSpeed * dt * this.autoScrollDir; content.setPosition(content.x, content.y step, 0); // 关键补偿content 动了拖拽项世界坐标要跟着同步 this.draggingItem.setWorldPosition( this.draggingItem.worldPosition.x, this.draggingItem.worldPosition.y step, 0 ); this.applySort(); }你可能注意到直接改 content 的 position 后ScrollView 内部记录的滚动偏移并没有更新所以在下一次惯性计算或滚动约束时可能被重置回原来的位置。这个 bug 的典型表现是边缘自动滚动时列表来回跳滚一点又弹回去。更稳妥的做法是调用scrollView.scrollToOffset()每帧传入一个很小的增量偏移让 ScrollView 自己维护 content 位置。你在实际项目中二选一如果列表长度固定且边界条件简单直接改 content position 加手动 clamp 到最小偏移就行如果列表是动态数据用scrollToOffset更省心。自动滚动跑通后还要注意 TOUCH_MOVE 阶段和 update 阶段的时序。同一帧里TOUCH_MOVE 先执行update 后执行所以 checkEdge 必须在 TOUCH_MOVE 里记录方向update 里消费方向顺序不能反过来。补偿坐标时我直接把 content 的位移量同步加到了拖拽项上这样用户在视觉上看到的永远是“手指按住的那个格子”不会出现列表在滚而 Item 悬在原地的割裂感。松手之后的数据回写也值得多说一句。很多方案在拖拽结束时只做了setSiblingIndexUI 顺序对了后台数据数组还是旧顺序下次刷新列表又恢复原样。我在reorderData里通过遍历 content 的 children按当前显示顺序收集每个 Item 节点的 name实际项目中通常是 dataId然后抛给外部回调。这个过程必须在setSiblingIndex之后、且 Layout 完成重排之后再读否则读到的是上一次排序。private reorderData() { const order this.contentNode.children .map(child child.name) .join(,); this.emit(reorder, order); // 触发数据层更新 }真机调试拖拽排序时坐标问题很难靠肉眼定位我现在的习惯是给脚本加一个 DEBUG 开关拖拽过程中把draggingItem.worldPosition和content.position每帧打印一次。很多玄学问题看一眼日志就明白了——要么是 content 在偷偷滚要么是 Item 的父节点还没切干净。从那以后我每次写和 ScrollView 相关的触摸逻辑都强制自己在代码里留这个坐标日志排序出了问题第一步先开日志不靠猜。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

LeetCode 1340跳跃游戏V:从记忆化搜索到单调栈+线段树优化

LeetCode 1340跳跃游戏V:从记忆化搜索到单调栈+线段树优化

LeetCode 1340 这道题,我印象特别深。当时打开题目页面,心里还想着"不过是跳一跳而已",结果从读题到第一版代码通过,硬是耗了 97 分钟。事后复盘,这 97 分钟大半都浪费在"跳跃条件"的翻译上——题…

2026/10/16 13:27:56 阅读更多 →
Keras版YOLOv4实战:模块化拆解与训练调优指南

Keras版YOLOv4实战:模块化拆解与训练调优指南

1. 为什么还要折腾 Keras 版的 YOLOv4 YOLOv4 刚出来那阵子,大部分人的第一反应是去跑 Darknet 原版,毕竟作者就是拿 Darknet 框架发的论文,配置文件和权重都是现成的。但实际用起来你会发现,Darknet 那套 C 语言的编译环境在 Win…

2026/10/16 13:27:56 阅读更多 →
Python + Tkinter + SQLite:从零构建校园志愿者服务平台

Python + Tkinter + SQLite:从零构建校园志愿者服务平台

1. 项目到底要做什么:需求拆解与方案选型1.1 从一句需求到功能清单校园志愿者服务平台这个题目,听起来不大,但真做起来很容易变成“注册登录 一个表格”的应付型作业。我拿到这个项目后的第一反应是:先把“志愿者服务平台”拆开&…

2026/10/16 13:27:56 阅读更多 →

最新新闻

C#实现HL7通讯测试解析:从协议基础到MLLP收发

C#实现HL7通讯测试解析:从协议基础到MLLP收发

简介:这是一份面向C#开发者的HL7医疗数据通讯解析示例资源,主要解决医院信息系统对接中HL7消息通过TCP/MLLP协议传输与解析的难题。资源基于VS2010环境编写,除核心HL7消息接收与解析逻辑外,还附带socket服务端与客户端的完整演示代…

2026/10/16 14:06:07 阅读更多 →
Qt调用OpenCV显示图片:Mat转QImage完整指南与避坑详解

Qt调用OpenCV显示图片:Mat转QImage完整指南与避坑详解

接手一个桌面图像处理项目时,最基础也最绕不开的一步,就是把OpenCV处理完的图片拿到Qt界面上显示出来。这个需求听起来简单,但实际落地时会遇到一堆说大不大、说小不小的坑:图片颜色蓝红互换、显示空白、内存异常、实时预览卡顿、…

2026/10/16 14:06:07 阅读更多 →
A Dynamic Stackelberg Game Framework for Agentic AI Defense Against LLM Jailbreaking

A Dynamic Stackelberg Game Framework for Agentic AI Defense Against LLM Jailbreaking

文章主要内容和创新点 主要内容 本文聚焦于大型语言模型(LLMs)面临的“越狱”问题(即攻击者通过操纵模型绕过安全机制生成有害内容),提出了一套基于动态斯塔克尔伯格博弈(Stackelberg Game)的防御框架,并设计了名为“Purple Agent”的代理式AI防御方案。具体内容包括…

2026/10/16 14:06:07 阅读更多 →
TruthTorchLM: A Comprehensive Library for Predicting Truthfulness in LLM Outputs

TruthTorchLM: A Comprehensive Library for Predicting Truthfulness in LLM Outputs

文章主要内容和创新点 主要内容 本文介绍了一个名为TruthTorchLM(TTLM) 的开源Python库,旨在预测大型语言模型(LLMs)输出的真实性。该库整合了30多种真实性预测方法(Truth Methods),涵盖不同维度的技术方案,包括计算成本、模型访问级别(黑盒/白盒)、是否依赖外部文…

2026/10/16 14:06:07 阅读更多 →
Aspose.Words 18.8 License加载实战:从jar包引入到去除水印的完整指南

Aspose.Words 18.8 License加载实战:从jar包引入到去除水印的完整指南

简介:面向Java开发者的Aspose.Word 18.8文档处理库,内置许可证文件,可在商业环境中合法使用;无需安装Microsoft Office,即可在Java应用中创建、读取、编辑并转换Word文档,支持输出PDF、HTML和图片格式。压缩…

2026/10/16 14:06:07 阅读更多 →
策略路由、GRE与NAT协同配置:企业网络流量精细化管理实战

策略路由、GRE与NAT协同配置:企业网络流量精细化管理实战

1. 从一条业务流量说起:为什么需要策略路由、GRE和NAT三者配合很多刚接触企业网络的朋友会有一个疑问:我明明已经配好了默认路由,内网能上网,为什么还要折腾策略路由、GRE隧道和NAT这些东西?这个问题我在带新人的时候被…

2026/10/16 14:05:05 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →