基于指针事件的文件树原位捏持滑动重排与开火车队列顺移动效 (In-Place Slide Reordering with Train Queue Shift)
1. 核心交互演进与设计定位 (Interaction Evolution & Design Positioning)
在树状/列表卡片调序设计中,常规的 "Drag & Put"(拿起来形成脱离列表的幽灵卡片,并在目标位置插入外挂占位槽)往往存在以下体验问题:
- 割裂感强:拖拽对象脱离了原本的视觉流,变成漂浮半透明盒子,用户丢失了“在同一堆卡片中翻移”的物理手感;
- 动效过重:额外插入独立的占位槽(如虚线高亮胶囊、流光框)导致周围卡片剧烈跳动,在紧凑文件侧边栏中显得冗余嘈杂。
原位捏持滑动(In-Place Slide Reordering)与开火车队列顺移(Train Queue Shift) 模式将交互还原为最直观的物理推移:
- 不拿起来,原位滑动:按住卡片左侧握柄后,卡片自身被提升(
z-index: 60、微发光与投影),但始终保留在列表流内,随着手指或光标沿着 Y 轴瞬时滑移; - 开火车队列顺移:当被捏住的卡片滑过邻近卡片的中线时,受影响的卡片如同铁轨上的列车车厢(Train Carriages)一般,沿着 Y 轴以微动效(
translateY0.18s)整体顺次往后或往前避让顺移; - 文件夹吸附:当滑行经过文件夹时,卡片顺移动效复位,文件夹触发磁吸附高亮(Magnetic Snap),释放时将文件归入该目录。
2. 核心架构与数学模型 (Core Architecture & Mathematical Model)
2.1 步长计算 (Stride Calculation)
在单向垂直排列的列表中,任意相邻卡片从中心到中心的位移为标准步长 stride(即卡片高度 + 间距 gap):
const sourceRect = sourceRow.getBoundingClientRect();
let stride = sourceRect.height + 4;
if (siblingRows.length > 1) {
const other = siblingRows[sourceIndex === 0 ? 1 : 0];
const diff = Math.abs(other.getBoundingClientRect().top - sourceRect.top);
if (diff > 0) stride = diff;
}2.2 50% 几何交叉与目标槽位映射 (Halfway Crossing & Slot Mapping)
在 pointermove 中,根据当前指针相对于初始按压点的位移量 deltaY,通过 Math.round(deltaY / stride) 形成天然的 50% 交叉触发点:
const deltaY = moveEvent.clientY - startY;
const slotShift = Math.round(deltaY / stride);
const targetIndex = Math.max(0, Math.min(siblingRows.length - 1, sourceIndex + slotShift));- 当
deltaY未过半时,slotShift为 0,不触发任何扰动; - 当向下滑动跨过邻近卡片 50% 高度时,
slotShift瞬时跃迁为 1,目标位置更新。
2.3 队列顺移算法 (Train Queue Shift Algorithm)
无需任何 DOM 节点的插入或删除,仅依靠硬件加速的 CSS transform 驱动兄弟卡片避让:
siblingRows.forEach((r, idx) => {
if (idx === sourceIndex) return;
if (targetIndex > sourceIndex) {
// 向下滑动:处于 (sourceIndex, targetIndex] 之间的卡片整体向上顺移 1 步
if (idx > sourceIndex && idx <= targetIndex) {
r.style.transform = `translateY(-${stride}px)`;
} else {
r.style.transform = '';
}
} else if (targetIndex < sourceIndex) {
// 向上滑动:处于 [targetIndex, sourceIndex) 之间的卡片整体向下顺移 1 步
if (idx < sourceIndex && idx >= targetIndex) {
r.style.transform = `translateY(${stride}px)`;
} else {
r.style.transform = '';
}
} else {
r.style.transform = '';
}
});3. 极简轻量 CSS 设计 (Minimalist CSS Design)
动效克制,仅依靠贝塞尔平移曲线营造顺滑手感:
/* 被激活原位滑动的卡片 */
.tree-row.is-sliding-active {
z-index: 60 !important;
position: relative !important;
background: var(--paper-card) !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.14) !important;
border: 1px solid var(--seal) !important;
border-radius: 7px;
cursor: grabbing !important;
user-select: none;
}
/* 兄弟卡片排队顺滑动效 (开火车顺移) */
.tree-file-row {
color: var(--ink);
transition: transform 0.18s cubic-bezier(0.2, 0, 0.2, 1), background 0.15s ease;
will-change: transform;
}
/* 文件夹磁吸附 */
.tree-folder-row.drop-folder-magnetic-snap {
background: var(--paper-hover) !important;
box-shadow: 0 0 0 2px var(--seal), 0 4px 16px rgba(184, 51, 42, 0.22) !important;
border-radius: 6px;
transform: scale(1.025);
transition: transform 0.12s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.12s ease;
}4. 释放落槽与数据最终一致性 (Settle Animation & Order Finalization)
在 pointerup 阶段:
- 落槽平滑归位动画:计算最终位移偏移量
finalOffset = (finalTargetIndex - sourceIndex) * stride,给sourceRow赋予 150ms 贝塞尔曲线过渡,让其平滑滑入避让形成的物理空位中; - 数据一致性更新:在落槽动画结束(
setTimeout150ms)后,更新真实数据数组customOrder并提交持久化(IndexedDB / localStorage),清除所有内联transform,重新渲染树视图,消除 DOM 重排闪烁。
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Frontend