03|HarmonyOS资料板拖拽取消与悬停反馈治理【共创稿事节】
现场资料板的卡片开始支持拖入之后,操作员很快会遇到一个别扭的问题:把配电柜照片拖到“设备照片”栏上方,整列变成高亮;手指向旁边移动,列却一直保持高亮,好像还在等待放入。有人按返回键取消,卡片又留下一块半透明的拖拽影子。再复杂一点,用户长按的是巡查报告,手滑经过照片栏,照片栏竟然也给出可以接收的提示,松手后业务层才拒绝,操作显得前后矛盾。
上一章给附件加了attachmentId、declaredMime、sourceUri、sourceState等元信息,把图片、PDF、表格和视频先按目标栏的接收规则过滤。资料板本身保留boardId、itemId、columnId和revision。本篇只处理一次拖拽进行中的临时状态:什么时候高亮、什么时候恢复、松手无效时为什么不能改动业务数据。拖动途中的指针位置不应该成为永久的归属变更,更不应该把取消操作写成一次有效移动。

资料项状态和指针反馈分开放
业务数据至少有两层:正式卡片属于哪一栏,以及某一次拖拽临时悬停在哪个目标区域。前者存储在资料板模型中,后者只在拖拽期间有效。可以将会话定义为sessionId、itemId、boardId、startColumnId、itemRevision、phase和hoverTarget。其中phase使用idle / dragging / hovering / committed / cancelled这样的有限状态集合,避免到处叠加布尔变量造成互相矛盾。
悬停并非提交。一张照片卡片从“待整理”经过“巡查记录”栏,经过时应该显示不接受的边框;转向“设备照片”栏后才提示可投放;再次移出列边界,则立刻移除当前列的高亮,但拖拽会话仍在进行。只有松手且投放判定通过时,才修改columnId并递增卡片版本。离开一个目标区域和取消整个拖拽,是两种不同动作;把它们混成cancel(),容易造成跨栏移动时会话提前终止。
实际应用常有嵌套组件,父容器、卡片列表和投放区可能分别接到事件。不要让每个组件各自维护一份会话事实源。把资料板所有拖拽事件统一交给一个协调器,组件只绘制协调器派发的反馈;哪一栏被高亮取决于hoverTarget,而不是该栏的某个本地变量曾经变为true。
悬停反馈是一道可复核的业务判定
照片栏接收JPEG、PNG,巡查记录栏接收PDF及指定文档格式,待整理栏允许回收当前板内的有效卡片。类型检查使用上一章形成的可信附件分类,文件名后缀只是呈现信息。若sourceState已经从“可读取”变为“已失效”,即使显示名称没有变化,也不能给出可投放反馈。跨板拖动时还要核查boardId,避免把另一现场的卡片带进当前维修任务。
const ACCEPTS = {
inbox: new Set(['image', 'report', 'drawing']),
photo: new Set(['image']),
report: new Set(['report', 'drawing'])
};
function inspectTarget(item, target, activeBoardId) {
if (!item || !target) return { allowed: false, reason: 'no target' };
if (item.boardId !== activeBoardId) {
return { allowed: false, reason: 'board changed' };
}
if (item.sourceState !== 'ready') {
return { allowed: false, reason: 'source unavailable' };
}
const accepted = ACCEPTS[target.columnId];
if (!accepted || !accepted.has(item.kind)) {
return { allowed: false, reason: 'type not accepted' };
}
return { allowed: true, reason: 'can drop' };
}
这段判断不依赖屏幕坐标,可用于长按拖拽、鼠标拖拽以及以后提供的菜单式移动操作。屏幕坐标的职责是识别指针当前覆盖哪一个投放区域;一旦得到target.columnId,后续接受规则完全按照对象身份与类型执行。反馈可以分为绿色可接收、橙色不可接收、无目标时中性边框,但还应有清晰文字,不能依靠颜色传递全部含义。

把会话收口到一个状态机
拖拽事件有开始、进入、移动、离开、投放和结束等时刻。平台事件负责传递交互发生的位置与数据,资料板状态机负责业务可否接收、是否提交及何时释放。调用平台接口前需查当前SDK文档中对应组件的可拖拽设置与事件可用条件;以下代码只表达应用层状态规则,避免让系统回调直接更新业务板数组。
class BoardDragSession {
constructor(boardId) {
this.boardId = boardId;
this.state = { phase: 'idle', hoverTarget: null };
}
start(item) {
if (!item || item.boardId !== this.boardId) throw new Error('wrong board');
if (this.state.phase === 'dragging' || this.state.phase === 'hovering') {
throw new Error('drag already active');
}
this.state = { phase: 'dragging', item: { ...item },
sourceColumn: item.columnId, hoverTarget: null, committed: false };
}
enter(target) {
if (!['dragging', 'hovering'].includes(this.state.phase)) return null;
const result = inspectTarget(this.state.item, target, this.boardId);
this.state.phase = 'hovering';
this.state.hoverTarget = { ...target, ...result };
return result;
}
leave(targetId) {
if (this.state.hoverTarget?.id === targetId) {
this.state.hoverTarget = null;
this.state.phase = 'dragging';
}
}
cancel() {
if (!['dragging', 'hovering'].includes(this.state.phase)) return false;
this.state = { phase: 'cancelled', hoverTarget: null };
return true;
}
}
这里的leave按目标标识清除反馈,防止事件跨越相邻子组件时,后到的旧离开事件把当前新目标的高亮也擦掉。还要处理同一栏内部多个嵌套节点连续触发进入的情况:业务层应将它们归一成同一目标ID,避免高亮反复闪烁。取消入口可能来自返回键、页面离开、源文件失效、拖拽结束未投放或用户撤销;这些路径最终都应该清除本次临时状态。
真正落盘前,再做一次接收检查
光标悬停期间附件可能被删除、目标栏目可能切换规则,甚至用户从当前资料板切换到了另一个项目。光靠进入目标区时的判断结果不够。投放瞬间要重新读取最新卡片对象及目标配置,核对itemRevision是否还是拖动开始时的版本。若不一致,给出“卡片已变更,请重新拖动”,把原本的栏位保留下来;不要用旧会话数据覆盖新内容。
function commitDrop(session, currentItem, target, updateItem) {
const s = session.state;
if (!['dragging', 'hovering'].includes(s.phase)) return false;
if (!currentItem || currentItem.revision !== s.item.revision) {
session.cancel();
return false;
}
const check = inspectTarget(currentItem, target, session.boardId);
if (!check.allowed) {
session.cancel();
return false;
}
updateItem({ ...currentItem, columnId: target.columnId,
revision: currentItem.revision + 1 });
session.state = { phase: 'committed', hoverTarget: null,
committed: true };
return true;
}
updateItem应接入资料板已有的单一更新入口,避免源栏删除和目标栏添加各自提交一次。若两列显示的是同一组卡片的不同过滤结果,只需要更新其columnId,视图即可重新投影;若数据持久化采用批量修改,也要保证移动动作要么整体成功,要么保持原状。取消不会调用updateItem,这是容易检查、也非常值得坚持的约束。

交互反馈还要照顾不同输入习惯
触屏用户通常跟随手指观察投放区,高亮最好发生在卡片进入有效接收范围之后;鼠标用户会在多个栏位间快速穿梭,反馈应持续反映当前指向的区域,而不是残留在上一次位置。需要明确几类提示:可接收时写“松开移入设备照片”,不接收时写“当前栏目不接收报告”,找不到目标时写“拖到栏目上方继续”。这种文字还可以交给辅助阅读逻辑复用,让视觉样式和行为语义一致。若用户放大系统字体导致栏目尺寸变化,目标区域应跟随布局重新计算,不应固化起始时的像素矩形。
一个容易忽略的情况是拖动期间数据筛选条件变化。比如操作员在拖动照片时,另一个交互把“设备照片”栏切换成只看本周资源;已提交卡片可能因筛选条件不满足而立即消失,此时需要解释归属已更新但列表当前过滤了它。取消路径则完全不同:取消后卡片仍在原栏,筛选与滚动位置不应因为一次失败投放而被重置。将两种结束状态分清,会明显降低现场人员误以为附件丢失的概率。
错误排查从“画面状态”回到“会话事件”
碰到“拖拽停止后栏位还亮着”,先检查结束事件是否把hoverTarget清空,再核对页面切换时协调器有无销毁。遇到“拖到无效栏位却移动成功”,检查提交阶段是否重新调用类型判定,而不是信任之前的allowed缓存。若一张卡被移动两次,重点排查投放事件在父子组件的传播、回调重入及持久化入口是否缺少版本检查。每一条事件只需留存会话标识、资料项ID、目标栏和状态迁移,避免记录包含私密文件路径的完整资源内容。
平台文档里,onDragEnter、onDragMove、onDragLeave和onDrop用于表达拖拽事件,draggable决定组件是否参与相关交互;这些接口负责交互触发,不替业务判断文件能否属于某栏目。阅读华为开发者的Drag Event事件参考和拖拽交互设计规范,应同时关注目标组件的事件条件、数据类型和结束时清理。
本篇与前两章最大的差别,是刻意不去碰业务数据的身份结构。卡片身份、附件类型和栏目接收规则继续有效,只增加拖动期间的短生命周期状态以及投放前的再次确认。下一章将基于这套可靠的取消语义,处理同一栏卡片重排、跨栏插入位置和排序键更新,避免“拖动位置正确,持久化顺序却不对”。
更多推荐

所有评论(0)