10、手势与触摸反馈:Launcher的触摸事件分发、长按、滑动、双击等手势识别、GestureListener与TouchController
各位好,今天我们来聊聊Launcher里最“接地气”的部分——触摸和手势。你想想看,用户每天在桌面上划来划去、点按图标、长按进入编辑模式,这些操作背后,是一整套事件分发和手势识别的机制在支撑。
我个人习惯把触摸系统比作一个“接力赛”。手指触碰屏幕的那一刻,事件就从底层驱动一路往上跑,经过ViewGroup、View,最后落到我们写的代码里。如果中间哪个环节断了,用户就会觉得“这手机怎么不跟手?”——嗯,这其实是个挺严重的问题。
10.1 触摸事件分发:从屏幕到Launcher
Android的触摸事件分发,核心就是三个方法:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。Launcher作为一个顶级容器,它的Workspace和DragLayer是事件分发的主战场。
我简单画了个流程图,帮你理清这个关系:
这里有个关键点:DragLayer是Launcher里最高优先级的触摸拦截器。为什么?因为拖拽操作(比如长按图标后移动)必须优先于其他所有手势。我在项目中遇到过一个问题:用户在桌面上快速滑动时,偶尔会触发长按。排查下来,就是DragLayer的onInterceptTouchEvent里对滑动距离的判断阈值设得太小了。
核心原则:Launcher的触摸分发遵循“谁先拦截,谁先处理”的规则。DragLayer拦截拖拽,Workspace拦截滑动翻页,CellLayout处理点击和长按。
10.2 手势识别:长按、滑动、双击
Launcher里用到的手势,说白了就几种:点击、长按、滑动、双击。但每种手势的识别时机和触发条件,都有讲究。
10.2.1 长按手势
长按是Launcher最核心的手势之一——它触发编辑模式、拖拽、卸载等操作。Launcher用的是LongPressHandler,它内部维护了一个CheckForLongPress的Runnable。
// 伪代码,展示长按检测逻辑
public class LongPressHandler {
private static final int LONG_PRESS_TIMEOUT = 500; // 毫秒
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 启动一个延迟500ms的Runnable
postCheckForLongPress(ev);
break;
case MotionEvent.ACTION_MOVE:
// 如果手指移动超过阈值,取消长按
if (distanceExceedsSlop(ev)) {
removeCallbacks(mCheckForLongPress);
}
break;
case MotionEvent.ACTION_UP:
// 手指抬起,取消长按
removeCallbacks(mCheckForLongPress);
break;
}
return false;
}
}
我曾经踩过一个坑:在某个定制ROM里,长按弹出菜单后,手指稍微动了一下,菜单就消失了。原因是ACTION_MOVE的滑动阈值设得太小,稍微抖一下就触发了取消。后来我把TouchSlop从系统默认的8dp改成了12dp,问题就解决了。
小技巧:长按的延迟时间不要硬编码。用ViewConfiguration.getLongPressTimeout()获取系统值,这样能适配不同用户的无障碍设置。
10.2.2 滑动翻页
Workspace的滑动翻页,用的是PagedView的机制。它本质上是一个水平滑动的ViewPager,但Launcher自己实现了一套,没有直接用官方的ViewPager。
为什么?因为Launcher需要更精细的控制——比如拖拽图标到边缘时自动翻页,或者编辑模式下禁止滑动。这些需求,官方的ViewPager很难满足。
// Workspace滑动翻页的核心逻辑
public class Workspace extends PagedView {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 编辑模式下不拦截触摸
if (mIsInEditMode) {
return false;
}
// 如果正在拖拽,也不拦截(交给DragLayer处理)
if (mDragController.isDragging()) {
return false;
}
return super.onInterceptTouchEvent(ev);
}
}
10.2.3 双击手势
双击在Launcher里用得不多,但有些Launcher用它来快速进入设置或锁屏。识别双击其实很简单:记录两次ACTION_DOWN的时间戳,如果间隔小于300ms,就认为是双击。
注意:双击和单击是互斥的。如果检测到双击,就不能再触发单击事件。所以通常的做法是:在单击的onClick里加一个延迟,等300ms确认没有第二次点击再执行。
10.3 GestureListener与TouchController
Launcher里有两个重要的角色:GestureListener和TouchController。它们分工明确,各司其职。
| 组件 | 职责 | 典型实现 |
|---|---|---|
| GestureListener | 监听具体的手势事件(长按、滑动、双击) | LongPressHandler、SwipeDetector |
| TouchController | 管理触摸事件的整体流程,协调多个手势 | DragController、WorkspaceTouchController |
TouchController是一个接口,Launcher里有很多实现类:
- DragController:处理拖拽相关的触摸事件
- WorkspaceTouchController:处理Workspace的滑动和点击
- FolderTouchController:处理文件夹内的触摸事件
每个TouchController都有一个onInterceptTouchEvent和onTouchEvent方法。Launcher在DragLayer里维护了一个TouchController列表,事件到来时,按优先级依次询问每个Controller是否要处理。
// DragLayer中的事件分发
public class DragLayer extends ViewGroup {
private List<TouchController> mControllers = new ArrayList<>();
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
for (TouchController controller : mControllers) {
if (controller.onInterceptTouchEvent(ev)) {
// 某个Controller说“我要处理这个事件”
mActiveController = controller;
return true;
}
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mActiveController != null) {
return mActiveController.onTouchEvent(ev);
}
return false;
}
}
这种设计的好处是:解耦。每个Controller只关心自己负责的手势,互不干扰。我后来在做一个车载Launcher时,也借鉴了这种模式——加了一个VoiceTouchController来处理语音唤醒的手势,非常方便。
总结一下:Launcher的触摸系统,核心就是“分层拦截 + 控制器模式”。事件从DragLayer开始,经过Workspace,最后落到具体的View上。每个手势都有专门的Handler或Controller来识别,互不干扰。这种设计,既保证了流畅性,又方便扩展。
嗯,关于手势和触摸反馈,今天就聊这么多。下一章我们会深入拖拽框架的实现,看看图标是怎么从桌面上“飘”起来的。