Android 拖拽 API 的选择与使用
Android 官方提供了两套拖拽 API,部分情况下它们都能实现需求。今天不仅要讨论各自的基本用法,更重要的是搞清楚它们的区别与使用场景。
OnDragListener
OnDragListener
是 Android framework 提供的 API,不需要引用额外的库。它有几个默认的特性:
- 拖动时原始 View 照常在原位置显示。
- 拖动时会有一个半透明的浮层,全局置顶,甚至可以覆盖系统 UI(比如导航栏)。
与 OnDragListener
配套的是 View.startDrag()
方法或 ViewCompat.startDragAndDrop()
兼容版本的方法,调用它就能启动拖拽,系统会自动显示一个浮层跟随手指,非常省心。
通过 View.setOnDragListener()
注册监听就可以得到拖拽的回调。与点击监听不同,任意 View 被拖拽都会触发所有 View 的回调。拖拽回调的 View
参数并不是被拖拽的 View,而是注册了这个监听的 View。为什么这么设计?因为拖拽回调有 ACTION_DRAG_ENTERED
之类的事件,它在拖拽进入其他 View 的区域时触发。一个 View 很可能希望别的东西拖拽到自己身上时进行一些响应,因此拖拽回调不仅仅针对被拖拽的 View,而是针对所有注册了的 View。
那么问题来了,怎能知道哪个 View 被拖动了?看一下启动拖拽的方法签名:
public static boolean startDragAndDrop(@NonNull View v, ClipData data,
View.DragShadowBuilder shadowBuilder, Object localState, int flags)
其中有三个参数:
localState: Object
: 拖拽回调中DragEvent.localState
的值就是这个参数的值。利用这一点,如果把 View 自身作为参数,自然在回调中就能判断是哪一个 View 被拖动了。data: ClipData
: 只能在DragEvent.ACTION_DROP
事件中才能通过DrarEvent.clipData
取到。看起来和localState
类似,但它有个特点就是可以跨进程。也因此比较重,所以只能在特定的时候获取。shadowBuilder: View.DragShadowBuilder
: 决定了拖拽浮层的样式,默认是和原始 View 一样。
刚才提到这套 API 有个参数可以跨进程,这有什么用?类似于拖拽图片到聊天窗口直接发送的功能就是这么实现的!当然,这需要相册和聊天软件同时支持才可以。
ViewDragHelper
ViewDragHelper
是 AndroidX 内的一个类,用于实现 View 拖拽。它的特点如下:
- 不改变 View 的 z 轴顺序。
- 直接改变 View 的位置,没有额外的浮层。
ViewDragHelper
与 GestureDetectorCompat
类似,都是一个外挂的手势检测器,只不过除了检测手势之外,它内部直接就完成了 View 的移动操作。既然是手势检测器,那需要自定义 View 后才能使用,模板代码如下:
class DragHelperGridView(context: Context, attrs: AttributeSet?) : ViewGroup(context, attrs) {
private var dragHelper = ViewDragHelper.create(this, DragCallback())
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return dragHelper.shouldInterceptTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
dragHelper.processTouchEvent(event)
return true
}
}
ViewDragHelper.Callback()
有很多回调:
-
tryCaptureView()
: 返回 true 表示启动拖动,否则不拖动这个 View。 -
clampViewPositionXxxx()
: 用于限制拖动的范围,默认实现是不能拖动。参数会传入要拖动到的位置和本次手指的位移,返回允许拖动到的位置。若直接返回要拖动到的位置则相当于此方向上无限制。例如下面的实现:override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int { return left }
-
onViewCaptured()
: 开始拖动的回调,可做一些初始化工作。 -
onViewReleased()
: 释放的回调,可做一些收尾工作。
ViewDragHelper
也内置了「归位」的逻辑,通常在 onViewReleased()
回调中调用 ViewDragHelper.settleCapturedViewAt()
以动画的形式让当前被拖拽的 View 移动到指定位置。与 OverScroller
类似,我们得在每一帧手动计算一下实时位置,模板代码如下:
override fun computeScroll() {
if (dragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this)
}
}
对比
OnDragListener | ViewDragHelper | |
---|---|---|
使用 | 直接使用 | 需要自定义 ViewGroup |
效果 | 显示浮层,保留原 View | 直接移动原 View |
拖拽显示范围 | 全局 | ViewGroup 内部 |
可能有小伙伴对 OnDragListener
的设计很不满意,比如「为什么拖走了原始 View 还显示?」 因为 OnDragListener
API 关注的是数据,UI 只是一种触摸反馈。例如把图片从相册拖到聊天窗口,当然应该在相册中继续正常显示。它的语义是「把这个数据传递给某个 App」,而不是「把这个 View 移动过去」。使用 View.startDrag()
API 通常伴随着数据的传递,甚至是跨应用的传递,否则在一定程度上就不是一个好的选择。
ViewDragHelper
则相反,它关注的就是 UI,操作的也是 View,适用于单纯对界面组件的拖动场景。它的语义很纯粹:移动这个 View。