Introducing Drag and Drop

作者冬瓜,爱奇艺iOS工程师

在你可以在 iPad 进行拖放操作,将文字、图片和文件从一个 App 移动到另一个。这一功能针对 iPad 多点触控大显示屏量身设计,整个操作如同魔术。你可以轻触并移动处于屏幕上各个位置的几乎任何内容,甚至还能同时移动多个项目。

Drag and Drop 是什么

Drag and Drop 是一种 App 的数据交互方式,这种方式依赖于用户的拖拽手势。使用 Drag and Drop 可以实现 App 中的文本、图片等资源文件快速而形象地剪切和拷贝操作。

在官方文档中,Overview 对 Drag and Drop 共有四个短句介绍:

  • 易用性(Easy to use):Drag and Drop 利用 Multiple-Touch 特性,让用户在使用 iPad 端的 App 时以自然的方式交互数据。只需要点击并悬停在你想要的图像、文本或文件,然后拖拽至你希望的位置。在原生应用中,你可以拖拽联系人信息、事件提醒、地图的注释信息等等。
  • Spring-loading:笔者不知道该如何翻译这个名字。但是很直观的感受就是和“弹性”和“载入”有关系。确实是这样,你可以在拖拽过程中把手指移动到按钮图标上,之后会产生类似点击的效果。这个效果解决了单手操作的点击事件问题。
  • 多选特性(Multi-select):iOS 11 引入了一个快捷方法来选取多个内容。当一个内容处于拖动状态时,可以用其他手指点击其他内容即可快速添加选中内容。
  • 系统整合(Systemwide Integration):Drag and Drop 已经整合到 iOS 中,你可以在这些位置使用:主屏幕、Dock、提醒、日历、消息、Spotlight、文件、Safari、联系人、iBooks、新闻、Notes、照片、地图、Keynote、Pages 以及 Numbers。并且提供了强大的 Drag and Drop API,可以在自己的 App 中实现。

Drag and Drop 的目标

  • The interface is live - 保持界面的实时性
  • Deep integration with all of iOS - iOS 统一整合拖拽功能
  • Great visual feedback - 更直观的视觉感受
  • Hover to navigate - 悬停导航
  • Items can be added - 可添加多内容
  • Transfer drags between fingers - 手指间拖拽合作
  • Multiple drag interactions - 多重拖拽交互合作

Drag and Drop 是 Multiple-Touch 技术的又一次实践。在这之前,MT 技术主要运用在游戏中,因为在日常工作的操作中几乎一个手指就可以完成。在 App 中的数据交互方面,传统的方法就是选中 → 长按复制 → 切换 App → 粘贴。这样的操作习惯就是直观且琐碎,步骤繁杂(这也是老罗托槽的地方)。

说起老罗发布的 One Step 技术,Apple 的 Drag and Drop 主要是在 iPad 端,而手机端在 WWDC 中并没有过多的演示。因为在 iOS 端拖拽显得过于鸡肋,屏幕太小拖拽的体验并没有体现出来。

使用 Drag

先来看一下 Drag 的简介和使用方法。Drag 的核心在于为 View 增加了Interaction 层,而这一层是用来输入 Drag 手势响应的。在 Session 203 中,Bruce Nilo 用这么一张图来形象深度的描述了其关系:

在 View 层上通过增加 Interaction 层来感知输入手势。这样做的好处是,使得在 iOS 平台上的 Drag and Drop 效果统一,并且不用重构项目即可增加 Drag and Drop 功能。

在功能实现上,Drag 采用 Cocoa 一贯的作风,使用代理来实现各个状态时的回调事件。

通过简单的代码就可以了解 UIDragInteraction 的使用方式。而对于多个内容(Item)的拖拽过程,Bruce Nilo 为我们引入了 Drag 中的一个新概念——lift

Drag Interaction 协议方法提供了多内容的拖拽,如何抉择是哪些内容接受了 Drag 手势的影响,这需要将 View 进行 lift 操作后,就可以完成。让内容达到 lift 状态很简单,在其他内容处于 Drag 状态时,只需要轻按其他内容,即可添加。当没有拖拽手势响应的内容时,正如上 Keynote 所示,拖拽手势将立即失效。

除了 lift,这里还引入了 Drag 内容(Drag Items)这么一个概念,什么是 Drag 内容呢?Drag 内容是为了完成 Drag 手势交互而附加到 View 上的模型对象。Drag 内容对象用于实现在屏幕上的拖动状态,并预览拖动的过程。

使用 Drop

下面我们来说说 Drop 的启用方式。第一种方式是我们需要初始化一个 Paste Configuration。顾名思义,这个配置的对象和粘贴(paste)行为有关。这个配置从使用上来讲就是确定了 Drag 之后进行的 Drop 行为类型,可以是普通的 Drop 也可以是 Paste。想使用 Paste Configuration,需要实现一个 pasteitemProviders 的新方法。

我们发现,使用 Paste Configuration 来实现 Drop 过于局限,因为供我们选择的情况只有两种。对于更加复杂的方式,其实现方法同上文的 Drag 类似,就是利用 Drop Interactions 层并通过代理的方式控制行为。

当我们的手指离开屏幕的时候,数据就会传输到 Drag 手势的结束位置,即 Drop 手势的响应位置。这期间是一个什么过程呢?

当手指拖拽 Drag 内容到可以 Drop 的位置时,由于这个位置上的 View 是设置过 Drop Interactions 的,所以就会更改提示图样,表示:“我对这些 Drag 内容感兴趣,可以接收”。在这个范围内的 Drop 会触发指定的 Interactions 代理方法的回调,回调方法中甚至可以拿到 Drag 内容并进行处理。而这一切的数据传输,都是以异步的形式。

从示意图中可以看出,所有的数据交互最终都由 NSItemProvider 进行获取和接收。在 iOS 8 之后,Apple 终于又想起来了这个曾经用于媒体数据分享的封装类。NSItemProvider 由于获取的是媒体资源,所以过程开销较长,十分容易阻塞线程。于是在 Drag and Drop 中为其开启异步执行的方法,增强体验。

Drag and Drop Timeline

下面我们来说一下 Drag and Drop 的 Timeline。整个的流程如 Session 203 中的图一样,我在这里把它修改成了中文描述:

描述一下上图表述的流程:当用户按下屏幕上的一个视图对象的时候,Lift 状态出发并相应 Lift Animation 效果。然后用户也许会将该对象拖拽到其他的 App 中,这时候进入了 Drag 状态。用户的手势释放后,Drop 状态启动,内部处理此位置是否可以接受该数据,根据代码实现的各种条件来确定 Drop Animation 以及数据的传输,或者无法接受数据,取消掉当前操作并执行 Drop Animation。

Drag and Drop 协议中的交互方法

Drag Interaction Delegate

Lift

在 Drag Interaction 中也包括了 Lift 的回调方法。可以说 Lift 过程是 Drag 过程的预备工作。Lift 过程包括了文件选择的动画、处于 Drag 时的显示状态等等。

func dragInteraction(_ interaction:UIDragInteraction, previewForLifting item:UIDragItem, session:UIDragSession) -> UITargetedDragPreview?

这个回调方法是用来做什么的呢?说白了,就定制 Lift 的动画及显示效果。系统默认的效果是当按住一个内容后,选中内容以块状的形式“提升”,来提示你现在可以拖动这些内容。此时预览图将是选中的内容本身,文本即为文本、图片即为图片快照等等。倘若你不喜欢它,你可以将预览图进行修改即可。在会议中 Kurt Revis 给出了一个示例:

func dragInteraction(_ interaction:UIDragInteraction, previewForLifting item:UIDragItem, session:UIDragSession) -> UITargetedDragPreview? {
    // 声明 imageView 实例
    let imageView = UIImageView(image: UIImage(named: "MyDragImage"))
    // 获取 interaction 层的 View
    let dragView = interaction.view!
    // 获取 dragView 的位置,从而确定手指的位置
    let dragPoint = session.location(in: dragView)
    // 使用视图与位置初始化成一个 Drag Preview Target
    let target = UIDragPreviewTarget(container: dragView, center: dragPoint)
    // UITargetedDragPreview 实例
    return UITargetedDragPreview(view: imageView, parameters:UIDragPreviewParameters(), target: target)
}
top Created with Sketch.