Drag-and-Drop — 是一种互动式手势,用手指在屏幕上拖动视觉对象。该手势由三个阶段组成:捕获(长按或触摸)、移动(手指带着对象移动)和释放(抛到目标区域)。用于改变元素顺序、在文件夹之间移动文件、组织界面(拖动重排、拖动调整大小)。在 iOS 中通过 UIDragInteraction 实现,在 Android 中通过 DragEvent 和 startDragAndDrop 实现。
要点
Drag-and-Drop(DnD,拖动)— 是一种操控图形界面元素的方法,用户用手指或鼠标捕获对象,将其移动到另一位置并释放。在移动界面中,Drag-and-Drop 通常通过长按(Long Press)发起 — 用户按住元素 300–500 毫秒,然后系统将界面切换到拖拽模式,手指的移动会带动对象。
历史上,Drag-and-Drop 出现在桌面界面中(Xerox Star,1981;Macintosh,1984),作为复制/粘贴命令的替代方案。在移动平台上,完整支持出现在 iOS 11(2017)的 UIDragInteraction 和 Android 7.0(2016)的 Multi-Window Drag-and-Drop。此前,移动应用中的拖拽是通过自定义解决方案使用 gesture recognizers 实现的。
关键概念:drag session(拖拽会话)、drag items(被拖拽对象)、drag preview(对象的视觉图像)、drop target(抛放区域)、drop proposal(抛放时的动作:移动、复制、链接)。系统在操作系统层面管理会话,这使得对象可以在不同应用之间拖拽(例如,照片从相册拖到备忘录)。
Drag-and-Drop 分为三个连续阶段。阶段 1 — 捕获(Drag Start):用户发起手势(通常为 Long Press),系统创建拖拽会话,捕获对象数据并显示 drag preview — 一个半透明的元素副本,跟随手指。在此阶段,系统会拒绝其他手势(滚动、选择文本)直到 DnD 完成。
阶段 2 — 移动(Drag Move):手指在屏幕上移动,drag preview 跟随触摸点。系统不断确定手指下方的 drop target,并发送 drag enter、drag move 和 drag exit 事件。Drop target 可以视觉反应:亮起、扩展或显示插入线。这个阶段持续 0.5 到 10+ 秒,具体取决于任务的复杂程度。
阶段 3 — 释放(Drop):手指离开屏幕。系统结束拖拽会话并在目标元素处调用 drop callback。如果 drop target 拒绝抛放(例如,将文件拖到没有写入权限的文件夹),drag preview 会以动画方式返回原始位置(取消动画)。如果抛放被接受,元素从原始位置消失并出现在目标位置。
| 阶段 | iOS 事件 | Android 事件 | 视觉状态 |
|---|---|---|---|
| 捕获 | UIDragInteractionDelegate.dragSessionWillBegin | View.OnLongClickListener → startDragAndDrop | Drag preview 出现在手指下方 |
| 移动 | UIDropInteractionDelegate.dragSessionDidUpdate | View.OnDragListener.onDrag (ACTION_DRAG_MOVED) | Preview 移动,target 亮起 |
| 释放 | UIDropInteractionDelegate.performDrop | View.OnDragListener.onDrag (ACTION_DROP) | 抛放或返回动画 |
应用间拖拽 — 两个平台均可用的功能。iOS 支持在 Split View 和 Slide Over 模式下(iPad)在应用之间进行 Drag-and-Drop。Android 支持在 Split Screen 模式下在应用之间拖拽。数据通过 UIPasteConfiguration(iOS)或 ClipData(Android)传输。开发者指定支持的数据类型(iOS 的 UTI,Android 的 MIME)。
UIDragInteraction — iOS 的主要拖拽 API,在 iOS 11 中添加。要从 View 启用拖拽,请添加 UIDragInteraction 和 UIDragInteractionDelegate。委托人确定哪些元素被拖拽、它们的外观和行为。对于接收元素,使用带有 UIDropInteractionDelegate 的 UIDropInteraction,该委托人确定 View 是否可以接受抛放以及如何处理数据。
// UIDragInteraction — 从集合中拖拽元素
class DragViewController: UIViewController, UIDragInteractionDelegate {
@IBOutlet var dragSourceView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let dragInteraction = UIDragInteraction(delegate: self)
dragSourceView.addInteraction(dragInteraction)
}
func dragInteraction(_ interaction: UIDragInteraction,
itemsForBeginning session: UIDragSession) -> [UIDragItem] {
guard let data = Data(contentsOf: selectedFileURL) else { return [] }
let itemProvider = NSItemProvider(object: data as NSData)
let dragItem = UIDragItem(itemProvider: itemProvider)
// Drag preview — 元素的缩小副本
dragItem.previewProvider = {
let previewView = UIImageView(image: thumbnailImage)
previewView.alpha = 0.8
return UIDragPreview(view: previewView)
}
return [dragItem]
}
}UIDropInteractionDelegate — 用于接收抛放的镜像接口。canHandle(_:) 方法检查数据类型是否被支持。sessionDidUpdate(_:) 视觉更新状态(亮起、插入线)。performDrop(_:) 执行数据的最终处理。iOS 会自动调用带有操作类型的 UIDropProposal:.move(移动)、.copy(复制)、.forbidden(禁止)。操作类型影响视觉显示(复制时在 preview 角落显示 + 图标,forbidden 显示禁止符号)。
View.startDragAndDrop() — Android SDK 用于开始拖拽的方法,从 API 24(Android 7.0)开始可用。在捕获元素后调用(通常在 onLongClick 中)。参数:ClipData(数据)、DragShadowBuilder(视觉阴影)、Object myLocalState(本地状态)、flags。DragShadowBuilder 创建一个跟随手指的 Bitmap。系统在拖拽过程中自动管理动画和触摸事件。
// 使用重新排序拖拽 RecyclerView 元素
class ReorderAdapter(private val items: MutableList<String>) :
RecyclerView.Adapter<ReorderAdapter.ViewHolder>() {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.itemView.setOnLongClickListener {
// 创建用于数据传输的 ClipData
val clipData = ClipData.newPlainText("item", items[position])
// 带有缩小 preview 的自定义 DragShadowBuilder
val shadowBuilder = object : View.DragShadowBuilder(holder.itemView) {
override fun onProvideShadowMetrics(shadowSize: Point, shadowTouchPoint: Point) {
shadowSize.set(itemView.width, itemView.height / 2)
}
}
holder.itemView.startDragAndDrop(clipData, shadowBuilder, position, 0)
true
}
}
}接受抛放 在 Android 中通过 View.setOnDragListener() 实现。监听器接收 ACTION_DRAG_STARTED、ACTION_DRAG_ENTERED、ACTION_DRAG_LOCATION、ACTION_DRAG_EXITED、ACTION_DROP、ACTION_DRAG_ENDED 事件。在 ACTION_DROP 事件中,数据从 ClipData 提取并执行目标操作。对于需要重新排序的 RecyclerView,建议使用 ItemTouchHelper.SimpleCallback — 这是一个内置机制,自动处理用于捕获的 Long Press、元素移动动画以及 onMove/onSwiped 辅助方法。
重新排序列表(drag to reorder)— 最常见的 DnD 场景:用户拖拽列表元素以改变它们的顺序。示例:重新排列收藏联系人、在任务跟踪器(Trello、Todoist)中排序任务、改变播放列表中歌曲的顺序(Spotify、Apple Music)。通过 Android 中的 ItemTouchHelper 或 iOS 中的 UILongPressGestureRecognizer 加单元格重排实现。
文件管理器 — 用于在文件夹之间复制和移动文件的 Drag-and-Drop。iOS Files App、Android Files by Google、Dropbox — 所有都支持 DnD。将文件拖到文件夹上会将其移动到内部(move),拖动时按住 Option/Ctrl 键会复制(copy)。Drop target 在靠近时视觉亮起,显示文件夹边界。
看板 — Trello、Jira、Notion 使用 Drag-and-Drop 将卡片在列之间移动(To Do → In Progress → Done)。拖拽时,卡片从源列脱离,跟随手指,并在越过另一列的边界时以动画方式插入。iOS 11+ 支持带有视差效果的 UIDragPreview,其中 preview 稍微偏离触摸点,创造深度感。
Springboard 模块(iOS) — 在 iOS 主屏幕上拖拽应用图标。长按图标会激活“jiggling”模式(图标振动),然后用户可以在页面之间移动图标并创建文件夹。这是最复杂的 DnD UX 模式之一,因为它需要同时处理拖拽、振动动画和归类到文件夹。
视觉反馈 — 对 Drag-and-Drop 至关重要。被拖拽的对象应该变化:缩小(iOS:scale 0.8–0.9)、变成半透明(alpha 0.7–0.8)并带着阴影升到界面上方(shadow offset)。Drop target 应该对靠近做出视觉反应:亮起(highlight)、扩展(expand)或显示边框(border)。没有这些信号,用户无法理解可以将对象抛到哪里。
返回动画(取消拖拽)— 如果用户在抛放区域外或无效区域上方释放元素,对象应以动画方式返回原始位置。iOS UIDragInteractionDelegate 提供 dragInteraction(_:sessionDidMove:) 用于跟踪和 dragInteraction(_:session:willEndWith:) 用于返回动画。Android View.setOnDragListener 接收 ACTION_DRAG_ENDED,在其中可以通过 ValueAnimator 或 ObjectAnimator 启动返回动画。
| 建议 | iOS | Android |
|---|---|---|
| 手势捕获 | Long Press(默认) | OnLongClickListener |
| Drag preview | UIDragItem.previewProvider | DragShadowBuilder |
| 触觉反馈 | UIImpactFeedbackGenerator | HapticFeedbackConstants.LONG_PRESS |
| 返回动画 | UIDragPreviewTarget | ViewPropertyAnimator |
| Drop proposal | UIDropProposal (.move/.copy) | ACTION_DROP + 标志 |
| 应用间 DnD | iOS 11+ Split View (iPad) | Android 7.0+ Split Screen |
无障碍:有运动限制的用户可能无法执行拖拽手势。通过列表的“向上/向下移动”按钮、文件的“移到文件夹”上下文菜单提供替代方案。iOS VoiceOver 支持用于移动元素的“Drag”旋转器操作。Android TalkBack 通过 AccessibilityNodeInfo.addAction() 提供自定义操作。Nike Run Club 和 Apple Health 使用 DnD 重新排序仪表盘上的指标 — 通过标准按钮访问。
常见问题
发起 Drag-and-Drop 的标准手势是 Long Press(长按),持续 300–500 毫秒。iOS 使用内置的 UIDragInteraction 机制,自动将长时间触摸转换为捕获。Android 需要在 OnLongClickListener 回调中显式调用 startDragAndDrop()。在某些情况下(例如在游戏中),DnD 可以通过简单触摸开始而无需延迟。
可以,应用间 Drag-and-Drop 在 iOS 11+(iPad)和 Android 7.0+ 上受支持。在 iPad 上,拖拽在 Split View 和 Slide Over 模式下工作 — 用户可以从 Safari 获取文本并将其移到备忘录。在 Android 中,应用间 DnD 在 Split Screen 中工作。数据通过 UIPasteConfiguration(iOS)或 ClipData(Android)传输,并指定支持的类型(UTI、MIME)。
DnD 和 Scroll 的冲突通过捕获延迟解决。如果用户在 Long Press 超时(500 毫秒)之前开始移动手指,该手势将被解释为滚动,而非 Drag-and-Drop。在 iOS 中,UIDragInteraction 通过 cancelsTouchesInView 自动管理此冲突。在 Android 中,ItemTouchHelper 使用阈值 — 如果移动超过 TouchSlop 阈值,Long Press 将被取消以进行滚动。
Drag-and-Drop 支持所有类型的数据:文本、图像、文件、URL、自定义对象。iOS 使用带有 UTI 类型的 NSItemProvider(public.text、public.image、public.file-url)。Android 使用带有 MIME 类型的 ClipData(text/plain、image/png、application/octet-stream)。对于自定义数据,创建带有 Intent 的 ClipData.Item。在应用间 DnD 中,数据被封装为通用格式:plein text、HTML、RTF、UIImage/PNG。
在 iOS 中,返回动画通过 UIDragPreviewTarget 管理 — preview 以动画方式返回的最终位置。在 Android 中,使用带有 translateX/translateY 的 ViewPropertyAnimator 将元素返回原始位置。动画时长为 200–300 毫秒,采用 easing 曲线(EaseOut)。UIKit 自动添加带有视差效果的 UIDragPreview,在取消手势时平滑返回。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。