4. 拖拽框架(DragLayer):层级与事件分发、接口设计、动画反馈
拖拽,是桌面系统里最核心的交互之一。你想想看,用户长按一个图标,把它拖到另一个位置,或者拖进文件夹,甚至拖到屏幕边缘触发某个功能——这一连串动作背后,需要一个强大的框架来支撑。
这个框架,就是 DragLayer。我把它比作桌面系统的“物流中枢”。今天我们就来拆解它。
4.1 DragLayer 的层级地位
先搞清楚一件事:DragLayer 在窗口体系里处于什么位置?
在 Android 的 WindowManager 里,每个窗口都有个层级(Layer)。普通应用窗口在某个层级,状态栏在更高层,而 DragLayer——它必须是最顶层。
为什么?因为拖拽过程中,那个被拖起来的图标要浮在所有界面之上。如果它被别的窗口盖住,用户就看不见了,交互就断了。
核心原则:DragLayer 的 WindowManager.LayoutParams.type 通常设置为 TYPE_APPLICATION_OVERLAY,并且它的 z-order 要高于所有普通窗口。
我早期做的一个项目里,就因为 DragLayer 层级设低了,结果拖拽时图标被通知栏遮住了一半。用户反馈说“图标消失了”,排查了半天才发现是层级问题。从那以后,我每次都会在代码里显式检查 DragLayer 的层级。
4.2 事件分发:谁该响应触摸?
拖拽的本质,是触摸事件的拦截与重定向。DragLayer 需要做两件事:
- 拦截长按事件——当用户在桌面上长按一个图标时,触发拖拽。
- 接管后续触摸——拖拽开始后,所有触摸事件都交给 DragLayer 处理,不再分发给下面的桌面。
具体实现上,我习惯在 DragLayer 的 onInterceptTouchEvent() 里做判断:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录触摸点,判断是否在可拖拽区域
mDownX = ev.getX();
mDownY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
// 如果移动距离超过阈值,且当前处于拖拽状态,则拦截
if (isDragging && isDragDistanceExceeded(ev)) {
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 结束拖拽
break;
}
return super.onInterceptTouchEvent(ev);
}
这里有个细节:onInterceptTouchEvent 返回 true 后,后续事件会直接交给 DragLayer 的 onTouchEvent()。所以你要在 onTouchEvent() 里处理拖拽的移动逻辑。
小技巧:我建议在 DragLayer 里维护一个 mDragState 枚举(IDLE, DRAGGING, ANIMATING),这样事件分发的逻辑会清晰很多。
4.3 DragSource 与 DropTarget 接口设计
拖拽不是 DragLayer 一个人的事。它需要知道:
- 谁发起了拖拽?(DragSource)
- 拖到哪里去?(DropTarget)
这两个接口,是拖拽框架的“协议”。
DragSource 接口
DragSource 代表拖拽的发起者。比如桌面上的一个图标、文件夹里的一个应用、或者 Dock 栏上的快捷方式。
public interface DragSource {
/**
* 开始拖拽时调用
* @param dragInfo 拖拽数据
* @return 拖拽的视图(通常是一个克隆的图标)
*/
View onDragStart(DragInfo dragInfo);
/**
* 拖拽结束时调用
* @param success 是否成功放置
*/
void onDragEnd(boolean success);
}
我遇到过一个问题:onDragStart() 返回的 View 如果直接使用原图标,会导致原位置图标消失。所以一定要克隆一个 View,或者创建一个 Bitmap 来显示。
DropTarget 接口
DropTarget 代表可以接收拖拽的目标。比如桌面网格、文件夹、或者屏幕边缘的“删除”区域。
public interface DropTarget {
/**
* 判断是否接受这个拖拽
*/
boolean acceptDrop(DragInfo dragInfo);
/**
* 拖拽进入目标区域
*/
void onDragEnter(DragInfo dragInfo);
/**
* 拖拽离开目标区域
*/
void onDragExit(DragInfo dragInfo);
/**
* 拖拽在目标区域内移动
*/
void onDragOver(DragInfo dragInfo);
/**
* 放置完成
*/
void onDrop(DragInfo dragInfo);
}
你看,这个接口设计得很像“鼠标事件”——进入、离开、移动、放下。每个 DropTarget 都可以根据这些回调来更新自己的 UI。
设计要点:DragInfo 是一个数据类,包含被拖拽对象的类型、位置、附加数据等。我建议把它设计成可序列化的,方便跨进程传递。
4.4 拖拽动画与视觉反馈
没有动画的拖拽,就像没有声音的电影——能用,但体验很差。
拖拽动画主要分三块:
1. 拖拽过程中的跟随动画
手指移动时,被拖拽的图标要平滑跟随。这个很简单,在 onTouchEvent() 里更新 View 的 translationX/Y 就行。
// 在 onTouchEvent 的 ACTION_MOVE 中
mDragView.setTranslationX(ev.getRawX() - mDragOffsetX);
mDragView.setTranslationY(ev.getRawY() - mDragOffsetY);
但直接设置位置会显得很生硬。我习惯加一个 spring 效果:
// 使用 SpringAnimation 实现弹性跟随
SpringAnimation animX = new SpringAnimation(mDragView, SpringAnimation.TRANSLATION_X, targetX);
animX.getSpring().setStiffness(500f);
animX.getSpring().setDampingRatio(0.8f);
animX.start();
这样拖拽时图标会有一点点“滞后感”,反而更自然。
2. 目标区域的反馈动画
当拖拽进入某个 DropTarget 时,目标区域应该给出视觉反馈。比如:
- 桌面网格高亮显示插入位置
- 文件夹图标放大并打开
- 删除区域变红
这些反馈通常在 onDragEnter() 和 onDragExit() 里触发。
注意:反馈动画不要做得太夸张。我曾经见过一个项目,拖拽进文件夹时文件夹弹跳了3次,用户反馈说“太晕了”。适度的缩放(1.0 -> 1.1)就够了。
3. 放置成功/失败的动画
放置成功时,图标应该“归位”到目标位置,并有一个轻微的弹入效果。放置失败时(比如拖到了无效区域),图标应该“飞回”原位置。
飞回动画我通常用 ValueAnimator 配合 PathInterpolator 实现:
// 飞回动画
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.addUpdateListener(animation -> {
float fraction = animation.getAnimatedFraction();
float x = startX + (endX - startX) * fraction;
float y = startY + (endY - startY) * fraction;
mDragView.setTranslationX(x);
mDragView.setTranslationY(y);
});
anim.setInterpolator(new PathInterpolator(0.4f, 0f, 0.2f, 1f));
anim.setDuration(300);
anim.start();
这个 PathInterpolator 会让动画先快后慢,看起来更自然。
4.5 整体架构图
下面这张图展示了 DragLayer 框架的核心组件和交互流程:
4.6 避坑指南
最后,分享几个我踩过的坑:
- 触摸事件被消费了——如果 DragLayer 的子 View 也处理了触摸事件,记得在
onInterceptTouchEvent里返回 true,否则拖拽会失效。 - 动画卡顿——拖拽过程中如果频繁触发布局,会导致掉帧。我建议用
setLayerType(View.LAYER_TYPE_HARDWARE, null)开启硬件加速。 - 多指触控——用户可能用两根手指操作。记得处理
ACTION_POINTER_DOWN和ACTION_POINTER_UP,避免拖拽被意外中断。
嗯,拖拽框架的内容差不多就这些了。核心就是:层级要够高、事件要拦截、接口要清晰、动画要流畅。把这四点做好,你的桌面系统拖拽体验就不会差。
公众号:蓝海资料掘金营,微信deep3321