系列目录 :第一篇:全景图与架构概览 | [第二篇:AccessibilityManagerService 启动与初始化](#第二篇:AccessibilityManagerService 启动与初始化) | 第三篇:无障碍服务的注册与绑定 | [第四篇:AccessibilityEvent 的产生与投递](#第四篇:AccessibilityEvent 的产生与投递) | [第五篇:AccessibilityNodeInfo 的构建与查询](#第五篇:AccessibilityNodeInfo 的构建与查询) | [第六篇:输入事件拦截与 TouchExplorer](#第六篇:输入事件拦截与 TouchExplorer) | [第七篇:手势分发与 KeyEvent 处理](#第七篇:手势分发与 KeyEvent 处理) | 第八篇:放大镜与辅助功能特性
一、为什么输入事件拦截是 Touch Exploration 的地基
前面几篇分别梳理了无障碍服务的启动、注册绑定、事件投递与节点树查询。这一篇回答一个更底层的问题:
当用户用手指在屏幕上慢慢滑动时,系统如何判断这是「探索界面」而不是「划动列表」?
这个问题看似是产品逻辑,实际上是工程问题。Android 7 的屏幕阅读器(TalkBack 等)之所以能用「单指滑动探索、双击确认、双指拖拽滚动」三套手势共存,靠的是系统在主线程上插入了一个 InputFilter,在事件进入窗口分发之前,先把它交给一个状态机去解读。
这个状态机就是 TouchExplorer,而承载它的链路叫 EventStreamTransformation。本篇按这条链路展开:
- AMS 在什么时机创建并安装
AccessibilityInputFilter AccessibilityInputFilter如何按输入源分发、按帧批处理事件EventStreamTransformation责任链的组成与顺序TouchExplorer的四个状态及其转换AccessibilityGestureDetector如何把慢速滑动与快速手势区分开
二、InputFilter 的安装 ------ AMS 的 updateInputFilter
2.1 触发条件
AccessibilityInputFilter 不是一直存在的。只有当当前用户启用了至少一项需要拦截输入的功能时(触摸探索、屏幕放大镜、按键过滤、鼠标自动点击、注入 MotionEvent),AMS 才会把它注册到 WindowManagerService 的输入过滤器链。
2.2 AMS 的 updateInputFilter
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
java
public class AccessibilityManagerService extends IAccessibilityManager.Stub {
// ...
private AccessibilityInputFilter mInputFilter;
private boolean mHasInputFilter;
private final WindowManagerInternal mWindowManagerService;
private void updateInputFilter(UserState userState) {
boolean setInputFilter = false;
AccessibilityInputFilter inputFilter = null;
synchronized (mLock) {
int flags = 0;
if (userState.mIsDisplayMagnificationEnabled) {
flags |= AccessibilityInputFilter.FLAG_FEATURE_SCREEN_MAGNIFIER;
}
// ... 其余 FLAG 计算省略 ...
if (userState.mIsPerformGesturesEnabled) {
flags |= AccessibilityInputFilter.FLAG_FEATURE_INJECT_MOTION_EVENTS;
}
if (flags != 0) {
if (!mHasInputFilter) {
mHasInputFilter = true;
if (mInputFilter == null) {
mInputFilter = new AccessibilityInputFilter(mContext,
AccessibilityManagerService.this);
}
inputFilter = mInputFilter;
setInputFilter = true;
}
mInputFilter.setUserAndEnabledFeatures(userState.mUserId, flags);
} else {
if (mHasInputFilter) {
mHasInputFilter = false;
mInputFilter.setUserAndEnabledFeatures(userState.mUserId, 0);
inputFilter = null;
setInputFilter = true;
}
}
}
if (setInputFilter) {
mWindowManagerService.setInputFilter(inputFilter);
}
}
}
关键设计 :
updateInputFilter先在mLock内决定「是否注册」以及「打开哪些 feature flag」,但把setInputFilter()调用放到锁外------因为该方法内部会走WindowManagerService,再回头可能回调到 AMS,持锁调用会造成死锁。
2.3 何时被调用
updateInputFilter 不是同步调用,而是被 scheduleUpdateInputFilter 投递到 MainHandler 的 MSG_UPDATE_INPUT_FILTER 异步执行。触发点主要有两类:
- 用户在设置中启用了带
flagRequestTouchExplorationMode等标志的服务 UserState中相关布尔量(如mIsDisplayMagnificationEnabled)发生变化
关键 :InputFilter 的生命周期由 feature flag 集合驱动,而不是由单个服务的启用/禁用驱动。这是后文
enableFeatures能按位组合的根本原因。
三、AccessibilityInputFilter ------ 链的入口与批处理器
3.1 类结构
AccessibilityInputFilter 继承自 android.view.InputFilter(主线程的 Looper 构造),自身同时实现 EventStreamTransformation。它既是链的「起点」,也是链的「终点 sink」------当某个 handler 决定把事件继续往下传时,最终会调回 AccessibilityInputFilter.onMotionEvent(),由它调用 sendInputEvent() 把事件送进 WindowManagerService。
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
java
class AccessibilityInputFilter extends InputFilter implements EventStreamTransformation {
static final int FLAG_FEATURE_SCREEN_MAGNIFIER = 0x00000001;
static final int FLAG_FEATURE_TOUCH_EXPLORATION = 0x00000002;
static final int FLAG_FEATURE_FILTER_KEY_EVENTS = 0x00000004;
static final int FLAG_FEATURE_AUTOCLICK = 0x00000008;
static final int FLAG_FEATURE_INJECT_MOTION_EVENTS = 0x00000010;
static final int FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER = 0x00000020;
private TouchExplorer mTouchExplorer;
private MagnificationGestureHandler mMagnificationGestureHandler;
private MotionEventInjector mMotionEventInjector;
private AutoclickController mAutoclickController;
private KeyboardInterceptor mKeyboardInterceptor;
private EventStreamTransformation mEventHandler; // 链头
private MotionEventHolder mEventQueue; // 待批处理队列
private Choreographer mChoreographer;
AccessibilityInputFilter(Context context, AccessibilityManagerService service) {
super(context.getMainLooper()); // ← 绑定主线程 Looper
// ...
}
void setUserAndEnabledFeatures(int userId, int enabledFeatures) {
if (mEnabledFeatures == enabledFeatures && mUserId == userId) {
return;
}
if (mInstalled) {
disableFeatures();
}
mUserId = userId;
mEnabledFeatures = enabledFeatures;
if (mInstalled) {
enableFeatures();
}
}
private void enableFeatures() {
resetStreamState();
if ((mEnabledFeatures & FLAG_FEATURE_AUTOCLICK) != 0) {
mAutoclickController = new AutoclickController(mContext, mUserId);
addFirstEventHandler(mAutoclickController);
}
if ((mEnabledFeatures & FLAG_FEATURE_TOUCH_EXPLORATION) != 0) {
mTouchExplorer = new TouchExplorer(mContext, mAms);
addFirstEventHandler(mTouchExplorer);
}
if ((mEnabledFeatures & FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER) != 0
|| (mEnabledFeatures & FLAG_FEATURE_SCREEN_MAGNIFIER) != 0) {
final boolean detectControlGestures = (mEnabledFeatures
& FLAG_FEATURE_SCREEN_MAGNIFIER) != 0;
mMagnificationGestureHandler = new MagnificationGestureHandler(
mContext, mAms, detectControlGestures);
addFirstEventHandler(mMagnificationGestureHandler);
}
if ((mEnabledFeatures & FLAG_FEATURE_INJECT_MOTION_EVENTS) != 0) {
mMotionEventInjector = new MotionEventInjector(mContext.getMainLooper());
addFirstEventHandler(mMotionEventInjector);
mAms.setMotionEventInjector(mMotionEventInjector);
}
if ((mEnabledFeatures & FLAG_FEATURE_FILTER_KEY_EVENTS) != 0) {
mKeyboardInterceptor = new KeyboardInterceptor(mAms);
addFirstEventHandler(mKeyboardInterceptor);
}
}
private void addFirstEventHandler(EventStreamTransformation handler) {
if (mEventHandler != null) {
handler.setNext(mEventHandler);
} else {
handler.setNext(this); // ← 链尾回指 InputFilter 自身
}
mEventHandler = handler;
}
}
关键设计 :每个 feature 通过
addFirstEventHandler()加入链头,因此「后添加的 feature」先看到事件。源码中mAutoclickController先加、mTouchExplorer后加,意味着鼠标自动点击的转换会先于触摸探索生效。链尾始终指回AccessibilityInputFilter自身,最终由onMotionEvent()调sendInputEvent()把事件交还给WindowManagerService。
3.2 入口分发:onInputEvent
InputFilter 框架会按事件类型回调 onInputEvent。AccessibilityInputFilter 在这一步做了三件事:按 InputEvent 来源分桶(触摸 / 鼠标 / 键盘)、校验 policyFlags 的 FLAG_PASS_TO_USER、把 MotionEvent 入队批处理、把 KeyEvent 直接转发给链头。
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
java
class AccessibilityInputFilter extends InputFilter implements EventStreamTransformation {
// ...
private EventStreamState mMouseStreamState;
private EventStreamState mTouchScreenStreamState;
private EventStreamState mKeyboardStreamState;
@Override
public void onInputEvent(InputEvent event, int policyFlags) {
if (mEventHandler == null) {
super.onInputEvent(event, policyFlags); // 未启用任何 feature → 直接透传
return;
}
EventStreamState state = getEventStreamState(event);
if (state == null) {
super.onInputEvent(event, policyFlags); // 非键盘/触摸/鼠标 → 透传
return;
}
int eventSource = event.getSource();
if ((policyFlags & WindowManagerPolicy.FLAG_PASS_TO_USER) == 0) {
state.reset();
mEventHandler.clearEvents(eventSource);
super.onInputEvent(event, policyFlags);
return;
}
if (state.updateDeviceId(event.getDeviceId())) {
mEventHandler.clearEvents(eventSource); // 输入设备切换 → 清空状态机
}
if (!state.deviceIdValid()) {
super.onInputEvent(event, policyFlags);
return;
}
if (event instanceof MotionEvent) {
if ((mEnabledFeatures & FEATURES_AFFECTING_MOTION_EVENTS) != 0) {
MotionEvent motionEvent = (MotionEvent) event;
processMotionEvent(state, motionEvent, policyFlags);
return;
} else {
super.onInputEvent(event, policyFlags);
}
} else if (event instanceof KeyEvent) {
KeyEvent keyEvent = (KeyEvent) event;
processKeyEvent(state, keyEvent, policyFlags);
}
}
private void processKeyEvent(EventStreamState state, KeyEvent event, int policyFlags) {
if (!state.shouldProcessKeyEvent(event)) {
return;
}
mEventHandler.onKeyEvent(event, policyFlags);
}
}
关键 :
FLAG_PASS_TO_USER == 0表示事件被系统 policy 层吞掉(例如电源键),此时必须state.reset()并把clearEvents广播给整条链,让每个 handler 都退出自己可能残留的状态。这是责任链设计里最容易被忽略的「一致性协议」。
3.3 按帧批处理 MotionEvent
触摸事件频率远高于帧率。如果每个 MotionEvent 都触发一次状态机迁移,会出现大量中间态。AccessibilityInputFilter 因此把触摸事件暂存到 MotionEventHolder 链表,再借助 Choreographer.CALLBACK_INPUT 在帧回调里按「属于当前帧」为界批量消费:
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
java
class AccessibilityInputFilter extends InputFilter implements EventStreamTransformation {
// ...
private final Runnable mProcessBatchedEventsRunnable = new Runnable() {
@Override
public void run() {
final long frameTimeNanos = mChoreographer.getFrameTimeNanos();
processBatchedEvents(frameTimeNanos);
if (mEventQueue != null) {
scheduleProcessBatchedEvents();
}
}
};
private void scheduleProcessBatchedEvents() {
mChoreographer.postCallback(Choreographer.CALLBACK_INPUT,
mProcessBatchedEventsRunnable, null);
}
private void batchMotionEvent(MotionEvent event, int policyFlags) {
if (mEventQueue == null) {
mEventQueue = MotionEventHolder.obtain(event, policyFlags);
scheduleProcessBatchedEvents();
return;
}
if (mEventQueue.event.addBatch(event)) {
return;
}
MotionEventHolder holder = MotionEventHolder.obtain(event, policyFlags);
holder.next = mEventQueue;
mEventQueue.previous = holder;
mEventQueue = holder;
}
private void processBatchedEvents(long frameNanos) {
MotionEventHolder current = mEventQueue;
if (current == null) {
return;
}
while (current.next != null) {
current = current.next;
}
while (true) {
if (current == null) {
mEventQueue = null;
break;
}
if (current.event.getEventTimeNano() >= frameNanos) {
// 本帧已处理完,剩下的留给下一帧
current.next = null;
break;
}
handleMotionEvent(current.event, current.policyFlags);
MotionEventHolder prior = current;
current = current.previous;
prior.recycle();
}
}
private void handleMotionEvent(MotionEvent event, int policyFlags) {
if (mEventHandler != null) {
mPm.userActivity(event.getEventTime(), false);
MotionEvent transformedEvent = MotionEvent.obtain(event);
mEventHandler.onMotionEvent(transformedEvent, event, policyFlags);
transformedEvent.recycle();
}
}
}
关键设计 :批处理以
frameNanos为界------eventTime < frameNanos的事件在本帧处理,>= frameNanos的留给下一帧。addBatch利用了MotionEvent自身对「同类型 move 事件可合并」的能力,能进一步降低 handler 调用次数。
3.4 链尾 sink:onMotionEvent / onKeyEvent
AccessibilityInputFilter 自己实现 EventStreamTransformation 时,onMotionEvent 与 onKeyEvent 的角色不是「处理」而是「出口」:
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
java
class AccessibilityInputFilter extends InputFilter implements EventStreamTransformation {
// ...
@Override
public void onMotionEvent(MotionEvent transformedEvent, MotionEvent rawEvent,
int policyFlags) {
sendInputEvent(transformedEvent, policyFlags);
}
@Override
public void onKeyEvent(KeyEvent event, int policyFlags) {
sendInputEvent(event, policyFlags);
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
// TODO Implement this to inject the accessibility event
// into the accessibility manager service similarly
// to how this is done for input events.
}
@Override
public void setNext(EventStreamTransformation sink) {
/* do nothing */
}
@Override
public void clearEvents(int inputSource) {
/* do nothing */
}
}
关键 :链上每个 handler 处理完后,要么消费事件(不调
mNext),要么调用mNext.onMotionEvent(...)把(可能被修改的)事件往下传。最终到达AccessibilityInputFilter.onMotionEvent(),由sendInputEvent()把事件送进WindowManagerService。这构成了一个「入口 = 出口」的环形责任链。
四、EventStreamTransformation ------ 责任链的契约
EventStreamTransformation 是这条链的接口定义。它规定了四个能力:处理触摸事件、处理按键事件、监听无障碍事件、清除状态。它还隐含了两条「责任链契约」:
- 消费即终止 :handler 处理完事件后若未调用
mNext,事件即被消费。 - 取消事件必须向下游广播 :如果 handler 决定取消当前手势,必须自己构造
ACTION_CANCEL或FLAG_CANCELED事件往下发,否则下游状态机将停留在不一致状态。
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/EventStreamTransformation.java
java
interface EventStreamTransformation {
public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags);
public void onKeyEvent(KeyEvent event, int policyFlags);
public void onAccessibilityEvent(AccessibilityEvent event);
public void setNext(EventStreamTransformation next);
public void clearEvents(int inputSource);
public void onDestroy();
}
关键设计 :接口同时暴露「事件流(onMotionEvent/onKeyEvent)」与「无障碍流(onAccessibilityEvent)」两条路径,使得 handler 既能改写输入事件,也能根据 AMS 投递的
AccessibilityEvent同步自己的内部状态(例如TouchExplorer会监听TYPE_VIEW_HOVER_EXIT来保证「手势结束」事件严格晚于最后一个 hover exit)。
4.1 责任链结构
按 enableFeatures() 的添加顺序(后加者先看到事件),一个典型 TalkBack 场景下的链形如下:
WindowManagerService (InputDispatcher)
│ onInputEvent(event, policyFlags)
▼
AccessibilityInputFilter
│ onMotionEvent / onKeyEvent (sink)
▲
│ setNext 链
│
├── mAutoclickController (鼠标自动点击)
│ │
│ ▼
├── mTouchExplorer (触摸探索)
│ │
│ ▼
├── mMagnificationGestureHandler (放大镜控制手势)
│ │
│ ▼
├── mMotionEventInjector (注入 MotionEvent 出口)
│ │
│ ▼
└── mKeyboardInterceptor (按键过滤)
│
▼
AccessibilityInputFilter.onMotionEvent()
└── sendInputEvent() → WindowManagerService
关键 :
mTouchExplorer位于放大镜手势之前,因为触摸探索必须先把单指手势识别为「探索 / 拖拽 / 委托」三种意图之一,放大镜控制手势才有机会介入多指场景。顺序错乱会导致两种 feature 互斥冲突。
五、TouchExplorer ------ 触摸探索的状态机
5.1 状态定义
TouchExplorer 是整个链中最复杂的 handler,用一个四态机把单指触摸流解释为「探索 / 拖拽 / 委托 / 手势检测」四种意图。状态常量直接定义在类里:
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java
java
class TouchExplorer implements EventStreamTransformation,
AccessibilityGestureDetector.Listener {
private static final int STATE_TOUCH_EXPLORING = 0x00000001;
private static final int STATE_DRAGGING = 0x00000002;
private static final int STATE_DELEGATING = 0x00000004;
private static final int STATE_GESTURE_DETECTING = 0x00000005;
private static final int CLICK_LOCATION_NONE = 0;
private static final int CLICK_LOCATION_ACCESSIBILITY_FOCUS = 1;
private static final int CLICK_LOCATION_LAST_TOUCH_EXPLORED = 2;
private static final float MAX_DRAGGING_ANGLE_COS = 0.525321989f; // cos(pi/4)
private static final int MIN_POINTER_DISTANCE_TO_USE_MIDDLE_LOCATION_DIP = 200;
private static final int EXIT_GESTURE_DETECTION_TIMEOUT = 2000;
private int mCurrentState = STATE_TOUCH_EXPLORING;
private int mDraggingPointerId;
private Handler mHandler;
private SendHoverEnterAndMoveDelayed mSendHoverEnterAndMoveDelayed;
private SendHoverExitDelayed mSendHoverExitDelayed;
private SendAccessibilityEventDelayed mSendTouchExplorationEndDelayed;
private SendAccessibilityEventDelayed mSendTouchInteractionEndDelayed;
private ExitGestureDetectionModeDelayed mExitGestureDetectionModeDelayed;
private AccessibilityGestureDetector mGestureDetector;
private EventStreamTransformation mNext;
private ReceivedPointerTracker mReceivedPointerTracker;
private InjectedPointerTracker mInjectedPointerTracker;
private AccessibilityManagerService mAms;
private Context mContext;
private boolean mTouchExplorationInProgress;
private int mLastTouchedWindowId;
}
关键设计 :四个状态不是简单的「互斥位」,而是有进入条件的有限状态机。例如
STATE_DRAGGING与STATE_DELEGATING都只能通过「单指探索中按下第二指」进入;STATE_GESTURE_DETECTING则由AccessibilityGestureDetector回调触发。
5.2 状态入口:onMotionEvent
onMotionEvent 是状态机的调度点。它做三件事:把事件交给 mGestureDetector 试匹配手势;处理 ACTION_CANCEL;按当前状态分派到对应的处理函数。
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java
java
class TouchExplorer implements EventStreamTransformation,
AccessibilityGestureDetector.Listener {
// ...
@Override
public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
if (!event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)) {
if (mNext != null) {
mNext.onMotionEvent(event, rawEvent, policyFlags);
}
return;
}
mReceivedPointerTracker.onMotionEvent(rawEvent);
// 手势检测器关心的是物理空间中的移动,因此用 rawEvent
if (mGestureDetector.onMotionEvent(rawEvent, policyFlags)) {
return; // 已被手势检测器消费
}
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
clear(event, policyFlags);
return;
}
switch(mCurrentState) {
case STATE_TOUCH_EXPLORING:
handleMotionEventStateTouchExploring(event, rawEvent, policyFlags);
break;
case STATE_DRAGGING:
handleMotionEventStateDragging(event, policyFlags);
break;
case STATE_DELEGATING:
handleMotionEventStateDelegating(event, policyFlags);
break;
case STATE_GESTURE_DETECTING:
// 已由手势检测器处理
break;
default:
throw new IllegalStateException("Illegal state: " + mCurrentState);
}
}
}
关键 :
mGestureDetector永远拿rawEvent,而状态机拿event。rawEvent是未经过放大镜等变换的原始坐标,避免手势识别被「屏幕放大」干扰------这是 AOSP 7 中很典型的双事件源设计。
5.3 单指探索分支:handleMotionEventStateTouchExploring
这是状态机最复杂的分支。它要处理三种情况:单指(继续探索)、第二指按下(拖拽或委托)、抬手(结束)。下面展示其核心 switch,省略了「缓存事件等待意图判定」的细节:
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java
java
class TouchExplorer implements EventStreamTransformation,
AccessibilityGestureDetector.Listener {
// ...
private void handleMotionEventStateTouchExploring(MotionEvent event, MotionEvent rawEvent,
int policyFlags) {
ReceivedPointerTracker receivedTracker = mReceivedPointerTracker;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
mAms.onTouchInteractionStart();
sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_START);
// 取消未决的延迟回调,强制发出上一轮悬停结束
mSendHoverEnterAndMoveDelayed.cancel();
mSendHoverExitDelayed.cancel();
if (mSendTouchExplorationEndDelayed.isPending()) {
mSendTouchExplorationEndDelayed.forceSendAndRemove();
}
if (mSendTouchInteractionEndDelayed.isPending()) {
mSendTouchInteractionEndDelayed.forceSendAndRemove();
}
if (!mGestureDetector.firstTapDetected() && !mTouchExplorationInProgress) {
// 用延迟方式发送 hover enter,给「意图判定」留窗口
final int pointerId = receivedTracker.getPrimaryPointerId();
final int pointerIdBits = (1 << pointerId);
mSendHoverEnterAndMoveDelayed.post(event, true, pointerIdBits,
policyFlags);
}
break;
}
case MotionEvent.ACTION_POINTER_DOWN: {
// 第二指按下 → 取消已排队的 hover 投递
mSendHoverEnterAndMoveDelayed.cancel();
mSendHoverExitDelayed.cancel();
break;
}
case MotionEvent.ACTION_MOVE: {
final int pointerId = receivedTracker.getPrimaryPointerId();
final int pointerIndex = event.findPointerIndex(pointerId);
final int pointerIdBits = (1 << pointerId);
switch (event.getPointerCount()) {
case 1: {
if (mSendHoverEnterAndMoveDelayed.isPending()) {
mSendHoverEnterAndMoveDelayed.addEvent(event);
} else {
if (mTouchExplorationInProgress) {
sendTouchExplorationGestureStartAndHoverEnterIfNeeded(policyFlags);
sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE,
pointerIdBits, policyFlags);
}
}
break;
}
case 2: {
// 第二指加入 → 必须决定是拖拽还是委托
if (mSendHoverEnterAndMoveDelayed.isPending()) {
mSendHoverEnterAndMoveDelayed.cancel();
mSendHoverExitDelayed.cancel();
} else if (mTouchExplorationInProgress) {
// 双指双击确认点击的 slop 判断(基于 rawEvent 距离)
// 超过 slop 则视为拖拽/委托
sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags);
}
if (isDraggingGesture(event)) {
mCurrentState = STATE_DRAGGING;
mDraggingPointerId = pointerId;
event.setEdgeFlags(receivedTracker.getLastReceivedDownEdgeFlags());
sendMotionEvent(event, MotionEvent.ACTION_DOWN, pointerIdBits,
policyFlags);
} else {
mCurrentState = STATE_DELEGATING;
sendDownForAllNotInjectedPointers(event, policyFlags);
}
break;
}
default: {
// 三指及以上 → 一律委托给视图层
if (mSendHoverEnterAndMoveDelayed.isPending()) {
mSendHoverEnterAndMoveDelayed.cancel();
mSendHoverExitDelayed.cancel();
} else {
sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags);
}
mCurrentState = STATE_DELEGATING;
sendDownForAllNotInjectedPointers(event, policyFlags);
break;
}
}
break;
}
case MotionEvent.ACTION_UP: {
mAms.onTouchInteractionEnd();
final int pointerId = event.getPointerId(event.getActionIndex());
final int pointerIdBits = (1 << pointerId);
if (mSendHoverEnterAndMoveDelayed.isPending()) {
// 还没真正发过 hover → 调度一个延迟的 hover exit
mSendHoverExitDelayed.post(event, pointerIdBits, policyFlags);
} else {
sendHoverExitAndTouchExplorationGestureEndIfNeeded(policyFlags);
}
if (!mSendTouchInteractionEndDelayed.isPending()) {
mSendTouchInteractionEndDelayed.post();
}
break;
}
}
}
}
关键设计 :「探索」并不是手指按下立即生效。
ACTION_DOWN之后,系统先用mSendHoverEnterAndMoveDelayed把 hover 事件「延迟」一段时间(与AccessibilityGestureDetector的判定窗口对齐)。这段时间里如果检测到双击或快滑,则取消 hover,改走点击或手势路径。这就是「慢速 = 探索,快速 = 手势」的工程实现。
5.4 双击确认点击:onDoubleTap
用户先单指探索到某控件,再「双击任意位置」确认点击------这是 TalkBack 的标准交互。onDoubleTap 是 AccessibilityGestureDetector.Listener 的回调,被 TouchExplorer 实现。它从「最后一次 hover 事件」中取坐标,再按优先级决定点击落在「无障碍焦点控件」还是「最后一次探索的位置」:
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java
java
class TouchExplorer implements EventStreamTransformation,
AccessibilityGestureDetector.Listener {
// ...
@Override
public boolean onDoubleTap(MotionEvent event, int policyFlags) {
if (mCurrentState != STATE_TOUCH_EXPLORING) {
return false;
}
mSendHoverEnterAndMoveDelayed.cancel();
mSendHoverExitDelayed.cancel();
if (mSendTouchExplorationEndDelayed.isPending()) {
mSendTouchExplorationEndDelayed.forceSendAndRemove();
}
if (mSendTouchInteractionEndDelayed.isPending()) {
mSendTouchInteractionEndDelayed.forceSendAndRemove();
}
final int pointerIndex = event.getActionIndex();
final int pointerId = event.getPointerId(pointerIndex);
Point clickLocation = mTempPoint;
final int result = computeClickLocation(clickLocation);
if (result == CLICK_LOCATION_NONE) {
return true; // 手势被消费,但没有可点击位置
}
PointerProperties[] properties = new PointerProperties[1];
properties[0] = new PointerProperties();
event.getPointerProperties(pointerIndex, properties[0]);
PointerCoords[] coords = new PointerCoords[1];
coords[0] = new PointerCoords();
coords[0].x = clickLocation.x;
coords[0].y = clickLocation.y;
MotionEvent click_event = MotionEvent.obtain(event.getDownTime(),
event.getEventTime(), MotionEvent.ACTION_DOWN, 1, properties,
coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0,
event.getSource(), event.getFlags());
final boolean targetAccessibilityFocus =
(result == CLICK_LOCATION_ACCESSIBILITY_FOCUS);
sendActionDownAndUp(click_event, policyFlags, targetAccessibilityFocus);
click_event.recycle();
return true;
}
private int computeClickLocation(Point outLocation) {
MotionEvent lastExploreEvent =
mInjectedPointerTracker.getLastInjectedHoverEventForClick();
if (lastExploreEvent != null) {
final int lastExplorePointerIndex = lastExploreEvent.getActionIndex();
outLocation.x = (int) lastExploreEvent.getX(lastExplorePointerIndex);
outLocation.y = (int) lastExploreEvent.getY(lastExplorePointerIndex);
if (!mAms.accessibilityFocusOnlyInActiveWindow()
|| mLastTouchedWindowId == mAms.getActiveWindowId()) {
if (mAms.getAccessibilityFocusClickPointInScreen(outLocation)) {
return CLICK_LOCATION_ACCESSIBILITY_FOCUS;
} else {
return CLICK_LOCATION_LAST_TOUCH_EXPLORED;
}
}
}
if (mAms.getAccessibilityFocusClickPointInScreen(outLocation)) {
return CLICK_LOCATION_ACCESSIBILITY_FOCUS;
}
return CLICK_LOCATION_NONE;
}
}
关键:点击坐标并不是用户手指抬起的位置,而是「最后一次 hover 注入事件」的位置,并且优先落在「无障碍焦点」对应的屏幕坐标上。这保证了用户双击屏幕任意位置都能精确点击到 TalkBack 当前朗读的控件,而不是手指所在处的视图。
5.5 拖拽状态:handleMotionEventStateDragging
双指同时向同一方向滑动且距离超过 200dp 时,TouchExplorer 进入 STATE_DRAGGING,把事件「合成」成一个单指拖动事件传给视图层(用于在 TalkBack 模式下滚动列表)。如果双指分向滑动或第二指抬起,则退回 STATE_DELEGATING,把双指事件原样交给视图层(用于缩放等操作)。
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java
java
class TouchExplorer implements EventStreamTransformation,
AccessibilityGestureDetector.Listener {
// ...
private void handleMotionEventStateDragging(MotionEvent event, int policyFlags) {
int pointerIdBits = 0;
if (event.findPointerIndex(mDraggingPointerId) == -1) {
mDraggingPointerId = INVALID_POINTER_ID;
} else {
pointerIdBits = (1 << mDraggingPointerId);
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
throw new IllegalStateException("Dragging state can be reached only if two "
+ "pointers are already down");
case MotionEvent.ACTION_POINTER_DOWN: {
// 第三指加入 → 委托给视图层
mCurrentState = STATE_DELEGATING;
if (mDraggingPointerId != INVALID_POINTER_ID) {
sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags);
}
sendDownForAllNotInjectedPointers(event, policyFlags);
break;
}
case MotionEvent.ACTION_MOVE: {
if (mDraggingPointerId == INVALID_POINTER_ID) {
break;
}
switch (event.getPointerCount()) {
case 2: {
if (isDraggingGesture(event)) {
final float firstPtrX = event.getX(0);
final float firstPtrY = event.getY(0);
final float secondPtrX = event.getX(1);
final float secondPtrY = event.getY(1);
final float deltaX = firstPtrX - secondPtrX;
final float deltaY = firstPtrY - secondPtrY;
final double distance = Math.hypot(deltaX, deltaY);
if (distance > mScaledMinPointerDistanceToUseMiddleLocation) {
event.setLocation(deltaX / 2, deltaY / 2);
}
sendMotionEvent(event, MotionEvent.ACTION_MOVE, pointerIdBits,
policyFlags);
} else {
// 双指分向滑动 → 委托
mCurrentState = STATE_DELEGATING;
sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits,
policyFlags);
sendDownForAllNotInjectedPointers(event, policyFlags);
}
break;
}
// ... 省略三指及以上分支 ...
}
break;
}
case MotionEvent.ACTION_UP: {
mAms.onTouchInteractionEnd();
sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END);
final int pointerId = event.getPointerId(event.getActionIndex());
if (pointerId == mDraggingPointerId) {
mDraggingPointerId = INVALID_POINTER_ID;
sendMotionEvent(event, MotionEvent.ACTION_UP, pointerIdBits, policyFlags);
}
mCurrentState = STATE_TOUCH_EXPLORING;
break;
}
// ...
}
}
}
关键设计 :
event.setLocation(deltaX / 2, deltaY / 2)把双指事件「合成」为以中点为坐标的单指 move。这是 TalkBack 模式下「双指下滑 = 列表滚动」的核心------视图层只看到一个指针在动,方向与速度由双指合力决定。
5.6 手势检测分支:onGestureStarted / onGestureCompleted
当 AccessibilityGestureDetector 判定「这串触摸不是慢速探索」时,会回调 onGestureStarted(),TouchExplorer 把状态切到 STATE_GESTURE_DETECTING 并把后续事件交给手势库识别:
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java
java
class TouchExplorer implements EventStreamTransformation,
AccessibilityGestureDetector.Listener {
// ...
@Override
public boolean onGestureStarted() {
mCurrentState = STATE_GESTURE_DETECTING;
mSendHoverEnterAndMoveDelayed.cancel();
mSendHoverExitDelayed.cancel();
mExitGestureDetectionModeDelayed.post(); // 2s 超时退回
sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_START);
return false;
}
@Override
public boolean onGestureCompleted(int gestureId) {
if (mCurrentState != STATE_GESTURE_DETECTING) {
return false;
}
endGestureDetection();
mAms.onGesture(gestureId); // ← 转发给 AMS 再分发给各 Service
return true;
}
@Override
public boolean onGestureCancelled(MotionEvent event, int policyFlags) {
if (mCurrentState == STATE_GESTURE_DETECTING) {
endGestureDetection();
return true;
} else if (mCurrentState == STATE_TOUCH_EXPLORING) {
// 还在探索中但被判定不是手势 → 把 MOVE 事件恢复为探索
if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {
final int pointerId = mReceivedPointerTracker.getPrimaryPointerId();
final int pointerIdBits = (1 << pointerId);
mSendHoverEnterAndMoveDelayed.addEvent(event);
mSendHoverEnterAndMoveDelayed.forceSendAndRemove();
mSendHoverExitDelayed.cancel();
sendMotionEvent(event, MotionEvent.ACTION_HOVER_MOVE, pointerIdBits,
policyFlags);
return true;
}
}
return false;
}
private void endGestureDetection() {
mAms.onTouchInteractionEnd();
sendAccessibilityEvent(AccessibilityEvent.TYPE_GESTURE_DETECTION_END);
sendAccessibilityEvent(AccessibilityEvent.TYPE_TOUCH_INTERACTION_END);
mExitGestureDetectionModeDelayed.cancel();
mCurrentState = STATE_TOUCH_EXPLORING;
}
}
关键 :
mAms.onGesture(gestureId)把识别出的手势 id(来自R.raw.accessibility_gestures手势库)分发给已绑定的AccessibilityService.onGesture()。TalkBack 在这个回调里完成「双指左滑 = 返回」之类的语义。下一篇将深入分析这条分发链与KeyEvent处理。
5.7 事件注入出口:sendMotionEvent
状态机所有「向视图层发事件」的操作都汇聚到 sendMotionEvent。它做了四件事:按 pointerIdBits 拆分多指事件、设置 downTime、对长按指针做坐标偏移(避免误触相邻控件)、向下游链转发:
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/TouchExplorer.java
java
class TouchExplorer implements EventStreamTransformation,
AccessibilityGestureDetector.Listener {
// ...
private void sendMotionEvent(MotionEvent prototype, int action, int pointerIdBits,
int policyFlags) {
prototype.setAction(action);
MotionEvent event = null;
if (pointerIdBits == ALL_POINTER_ID_BITS) {
event = prototype;
} else {
event = prototype.split(pointerIdBits); // 按位拆分多指事件
}
if (action == MotionEvent.ACTION_DOWN) {
event.setDownTime(event.getEventTime());
} else {
event.setDownTime(mInjectedPointerTracker.getLastInjectedDownEventTime());
}
if (mLongPressingPointerId >= 0) {
event = offsetEvent(event, - mLongPressingPointerDeltaX,
- mLongPressingPointerDeltaY);
}
policyFlags |= WindowManagerPolicy.FLAG_PASS_TO_USER;
if (mNext != null) {
mNext.onMotionEvent(event, null, policyFlags);
}
mInjectedPointerTracker.onMotionEvent(event);
if (event != prototype) {
event.recycle();
}
}
}
关键设计 :
prototype.split(pointerIdBits)是MotionEvent原生的多指拆分 API。TouchExplorer利用它把「双指事件」裁剪成「单指拖动事件」传给视图层,而mInjectedPointerTracker同步记录已注入的指针,确保后续ACTION_UP时只给「之前注入过」的指针补发抬起事件------这是责任链一致性的关键不变量。
六、AccessibilityGestureDetector ------ 慢速探索与快速手势的分流器
6.1 类结构
AccessibilityGestureDetector 是 TouchExplorer 的「前置过滤器」。它同时维护两套检测:
- 快速手势 :用
GestureLibraries.fromRawResource(R.raw.accessibility_gestures)加载系统预定义的手势库,对快速滑动轨迹做模板匹配,命中后通过onGestureCompleted(gestureId)回调。 - 双击 / 双击长按 :用标准
GestureDetector+OnDoubleTapListener检测,命中后通过onDoubleTap/onDoubleTapAndHold回调。
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/AccessibilityGestureDetector.java
java
class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListener {
public interface Listener {
void onDoubleTapAndHold(MotionEvent event, int policyFlags);
boolean onDoubleTap(MotionEvent event, int policyFlags);
boolean onGestureStarted();
boolean onGestureCompleted(int gestureId);
boolean onGestureCancelled(MotionEvent event, int policyFlags);
}
private final Listener mListener;
private final GestureDetector mGestureDetector;
private final GestureLibrary mGestureLibrary;
private static final int TOUCH_TOLERANCE = 3;
private static final float MIN_PREDICTION_SCORE = 2.0f;
private static final int GESTURE_CONFIRM_MM = 10;
private static final long CANCEL_ON_PAUSE_THRESHOLD_NOT_STARTED_MS = 200;
private static final long CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS = 500;
private final float mGestureDetectionThreshold;
private final ArrayList<GesturePoint> mStrokeBuffer = new ArrayList<>(100);
AccessibilityGestureDetector(Context context, Listener listener) {
mListener = listener;
mGestureDetector = new GestureDetector(context, this);
mGestureDetector.setOnDoubleTapListener(this);
mGestureLibrary = GestureLibraries.fromRawResource(context,
R.raw.accessibility_gestures);
mGestureLibrary.setOrientationStyle(8 /* GestureStore.ORIENTATION_SENSITIVE_8 */);
mGestureLibrary.setSequenceType(GestureStore.SEQUENCE_SENSITIVE);
mGestureLibrary.load();
mGestureDetectionThreshold = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1,
context.getResources().getDisplayMetrics()) * GESTURE_CONFIRM_MM;
}
}
关键设计 :手势库是「方向敏感 + 顺序敏感」的(
ORIENTATION_SENSITIVE_8+SEQUENCE_SENSITIVE),意味着「双指左滑」与「双指右滑」会被识别为两个不同手势,且轨迹点的顺序参与匹配。R.raw.accessibility_gestures是 AOSP 7 系统预定义的手势库资源,ODM 可通过替换该资源或自定义onGesture()处理来扩展语义。
6.2 主入口:onMotionEvent
onMotionEvent 的判定逻辑:
ACTION_DOWN:初始化 stroke buffer、base 坐标、时间戳ACTION_MOVE:累计位移超过 10mm 阈值且耗时 < 200ms → 判定为手势开始(回调onGestureStarted);若位移未达阈值但静止超过 500ms → 取消手势(回调onGestureCancelled)ACTION_UP:若处于「手势已开始」状态 → 调用recognizeGesture()用模板匹配出 gestureId;否则若是双击 →finishDoubleTap()ACTION_POINTER_DOWN:第二指按下即取消手势(手势库只支持单指)
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/AccessibilityGestureDetector.java
java
class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListener {
// ...
public boolean onMotionEvent(MotionEvent event, int policyFlags) {
final float x = event.getX();
final float y = event.getY();
final long time = event.getEventTime();
mPolicyFlags = policyFlags;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mDoubleTapDetected = false;
mSecondFingerDoubleTap = false;
mRecognizingGesture = true;
mGestureStarted = false;
mPreviousGestureX = x;
mPreviousGestureY = y;
mStrokeBuffer.clear();
mStrokeBuffer.add(new GesturePoint(x, y, time));
mBaseX = x;
mBaseY = y;
mBaseTime = time;
break;
case MotionEvent.ACTION_MOVE:
if (mRecognizingGesture) {
final float deltaX = mBaseX - x;
final float deltaY = mBaseY - y;
final double moveDelta = Math.hypot(deltaX, deltaY);
if (moveDelta > mGestureDetectionThreshold) {
mBaseX = x;
mBaseY = y;
mBaseTime = time;
mFirstTapDetected = false;
mDoubleTapDetected = false;
if (!mGestureStarted) {
mGestureStarted = true;
return mListener.onGestureStarted();
}
} else if (!mFirstTapDetected) {
final long timeDelta = time - mBaseTime;
final long threshold = mGestureStarted ?
CANCEL_ON_PAUSE_THRESHOLD_STARTED_MS :
CANCEL_ON_PAUSE_THRESHOLD_NOT_STARTED_MS;
if (timeDelta > threshold) {
cancelGesture();
return mListener.onGestureCancelled(event, policyFlags);
}
}
final float dX = Math.abs(x - mPreviousGestureX);
final float dY = Math.abs(y - mPreviousGestureY);
if (dX >= TOUCH_TOLERANCE || dY >= TOUCH_TOLERANCE) {
mPreviousGestureX = x;
mPreviousGestureY = y;
mStrokeBuffer.add(new GesturePoint(x, y, time));
}
}
break;
case MotionEvent.ACTION_UP:
if (mDoubleTapDetected) {
return finishDoubleTap(event, policyFlags);
}
if (mGestureStarted) {
mStrokeBuffer.add(new GesturePoint(x, y, time));
return recognizeGesture(event, policyFlags);
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
// 第二指加入 → 必然不是单指手势
cancelGesture();
if (event.getPointerCount() == 2) {
mSecondFingerDoubleTap = true;
mSecondPointerDownTime = time;
} else {
mSecondFingerDoubleTap = false;
}
break;
// ...
}
// ...
}
}
关键设计:「10mm + 200ms」这两个阈值是用用户测试数据定的------快速手势的首段 10mm 移动几乎都在 100ms 内完成,而慢速探索的首段移动几乎都超过 200ms。这是 AOSP 在「准确性」与「响应延迟」之间权衡的产物,也是 TalkBack 模式下「手指稍微快点就变成手势、稍微慢点就变成探索」体验背后的工程参数。
6.3 模板匹配:recognizeGesture
recognizeGesture 用 mStrokeBuffer 中的轨迹点构造 Gesture,调用 mGestureLibrary.recognize() 拿到一个 Prediction 列表,取分数最高且 ≥ 2.0 的项,把其 name 解析为 int 形式的 gestureId 回调给 TouchExplorer:
源码路径 :frameworks/base/services/accessibility/java/com/android/server/accessibility/AccessibilityGestureDetector.java
java
class AccessibilityGestureDetector extends GestureDetector.SimpleOnGestureListener {
// ...
private boolean recognizeGesture(MotionEvent event, int policyFlags) {
Gesture gesture = new Gesture();
gesture.addStroke(new GestureStroke(mStrokeBuffer));
ArrayList<Prediction> predictions = mGestureLibrary.recognize(gesture);
if (!predictions.isEmpty()) {
Prediction bestPrediction = predictions.get(0);
if (bestPrediction.score >= MIN_PREDICTION_SCORE) {
try {
final int gestureId = Integer.parseInt(bestPrediction.name);
return mListener.onGestureCompleted(gestureId);
} catch (NumberFormatException nfe) {
Slog.w(LOG_TAG, "Non numeric gesture id:" + bestPrediction.name);
}
}
}
return mListener.onGestureCancelled(event, policyFlags);
}
}
关键 :手势库中每个模板的 name 必须能解析为 int(如
1、2、10等),这些 id 会原样传给AccessibilityService.onGesture(int gestureId)。OEM 若自定义手势库,需要同时更新 name 与 TalkBack 侧的语义映射表,否则Integer.parseInt会失败并走onGestureCancelled回退。
七、状态迁移总览
把上面四个状态与触发条件汇总,可以得到 TouchExplorer 的完整状态机:
┌────────────────────────────────────────────┐
│ │
▼ │
┌───────────────────────────────┐ │
│ STATE_TOUCH_EXPLORING │ │
│ (初始态 / 默认态) │ │
└───────────────────────────────┘ │
│ │ │ │
│ ACTION_DOWN │ 双指 MOVE │ 双指 MOVE │
│ (单指) │ 同向 + 距离≥200dp │ 分向 │
│ │ │ │
▼ ▼ ▼ │
延迟发 hover enter ┌────────────┐ ┌──────────────┐ │
│ │ DRAGGING │ │ DELEGATING │ │
│ └────────────┘ └──────────────┘ │
│ │ │ │
│ 双指 UP / 分向 │ 单指 UP │ │
│ 移动 │ │ │
└─────────────────┴────────────────────┘ │
回到 TOUCH_EXPLORING ──────────────────┘
▲
│
┌──────────────────────────┐ │
│ GESTURE_DETECTING │─────────┘
│ (onGestureStarted 进入) │ endGestureDetection()
└──────────────────────────┘
▲
│
快速滑动 ≥10mm 且 <200ms
(AccessibilityGestureDetector 判定)
关键 :所有状态最终都会回到
STATE_TOUCH_EXPLORING,这是状态机的「吸引子」。任何异常路径(ACTION_CANCEL、超时、第二指加入)都会通过clear(event, policyFlags)强制回到初始态,并发送TYPE_TOUCH_INTERACTION_END让无障碍服务同步。
八、关键源码文件索引
| 文件 | 角色 | 关键方法 |
|---|---|---|
AccessibilityManagerService.java |
决定 InputFilter 的创建/销毁/feature 开关 | updateInputFilter |
AccessibilityInputFilter.java |
链入口,按输入源分发,按帧批处理 | onInputEvent、batchMotionEvent、processBatchedEvents、enableFeatures |
EventStreamTransformation.java |
责任链接口定义 | onMotionEvent、onKeyEvent、onAccessibilityEvent、clearEvents |
TouchExplorer.java |
触摸探索状态机,事件注入出口 | onMotionEvent、handleMotionEventStateTouchExploring、onDoubleTap、sendMotionEvent |
AccessibilityGestureDetector.java |
慢速/快速分流器 + 手势库匹配 | onMotionEvent、recognizeGesture、onDoubleTap |
ReceivedPointerTracker.java |
跟踪原始输入指针 | getPrimaryPointerId、getLastReceivedEvent |
InjectedPointerTracker.java |
跟踪已注入到视图层的指针 | isInjectedPointerDown、getLastInjectedHoverEventForClick |
九、小结
输入事件拦截的完整数据流:
InputDispatcher (WindowManagerService)
│ onInputEvent(event, policyFlags)
▼
AccessibilityInputFilter.onInputEvent()
│ ├─ 按 source 分桶 (Touch / Mouse / Keyboard)
│ ├─ 校验 FLAG_PASS_TO_USER
│ ├─ 设备切换 → clearEvents 广播
│ └─ MotionEvent → 批处理队列
│ KeyEvent → 直接转发
▼
(Choreographer 帧回调) processBatchedEvents()
│
▼
mEventHandler (链头,按 feature 组合)
│ Autoclick → TouchExplorer → Magnification → MotionInjector → Keyboard
▼
TouchExplorer.onMotionEvent()
│ ├─ 慢速单指 → STATE_TOUCH_EXPLORING (hover enter/move)
│ ├─ 双指同向 → STATE_DRAGGING (合成单指 move)
│ ├─ 双指分向 / 三指 → STATE_DELEGATING (原样透传)
│ └─ 快速单指 → STATE_GESTURE_DETECTING (模板匹配)
▼
AccessibilityGestureDetector (前置)
│ ├─ 慢速 → onGestureCancelled → 恢复探索
│ └─ 快速 → onGestureStarted → recognizeGesture
▼
AMA.onGesture(gestureId) → AccessibilityService.onGesture()
TouchExplorer 的四态机 + AccessibilityGestureDetector 的「10mm/200ms」分流器,共同构成了 TalkBack 等屏幕阅读器「慢速探索、双击确认、双指拖拽、快速手势」四套交互的工程基础。
下一篇将分析手势识别完成后 AMA.onGesture() 如何把 gestureId 分发给各个 AccessibilityService.onGesture(),以及 KeyEvent 在这条链中的处理路径(KeyboardInterceptor 与 KeyEventDispatcher)。