Android 7系统无障碍服务(六)输入事件拦截与 TouchExplorer

系列目录第一篇:全景图与架构概览 | [第二篇:AccessibilityManagerService 启动与初始化](#第二篇:AccessibilityManagerService 启动与初始化) | 第三篇:无障碍服务的注册与绑定 | [第四篇:AccessibilityEvent 的产生与投递](#第四篇:AccessibilityEvent 的产生与投递) | [第五篇:AccessibilityNodeInfo 的构建与查询](#第五篇:AccessibilityNodeInfo 的构建与查询) | [第六篇:输入事件拦截与 TouchExplorer](#第六篇:输入事件拦截与 TouchExplorer) | [第七篇:手势分发与 KeyEvent 处理](#第七篇:手势分发与 KeyEvent 处理) | 第八篇:放大镜与辅助功能特性


一、为什么输入事件拦截是 Touch Exploration 的地基

前面几篇分别梳理了无障碍服务的启动、注册绑定、事件投递与节点树查询。这一篇回答一个更底层的问题:

当用户用手指在屏幕上慢慢滑动时,系统如何判断这是「探索界面」而不是「划动列表」?

这个问题看似是产品逻辑,实际上是工程问题。Android 7 的屏幕阅读器(TalkBack 等)之所以能用「单指滑动探索、双击确认、双指拖拽滚动」三套手势共存,靠的是系统在主线程上插入了一个 InputFilter,在事件进入窗口分发之前,先把它交给一个状态机去解读。

这个状态机就是 TouchExplorer,而承载它的链路叫 EventStreamTransformation。本篇按这条链路展开:

  1. AMS 在什么时机创建并安装 AccessibilityInputFilter
  2. AccessibilityInputFilter 如何按输入源分发、按帧批处理事件
  3. EventStreamTransformation 责任链的组成与顺序
  4. TouchExplorer 的四个状态及其转换
  5. 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 投递到 MainHandlerMSG_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 框架会按事件类型回调 onInputEventAccessibilityInputFilter 在这一步做了三件事:按 InputEvent 来源分桶(触摸 / 鼠标 / 键盘)、校验 policyFlagsFLAG_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 时,onMotionEventonKeyEvent 的角色不是「处理」而是「出口」:

源码路径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 是这条链的接口定义。它规定了四个能力:处理触摸事件、处理按键事件、监听无障碍事件、清除状态。它还隐含了两条「责任链契约」:

  1. 消费即终止 :handler 处理完事件后若未调用 mNext,事件即被消费。
  2. 取消事件必须向下游广播 :如果 handler 决定取消当前手势,必须自己构造 ACTION_CANCELFLAG_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_DRAGGINGSTATE_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,而状态机拿 eventrawEvent 是未经过放大镜等变换的原始坐标,避免手势识别被「屏幕放大」干扰------这是 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 的标准交互。onDoubleTapAccessibilityGestureDetector.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 类结构

AccessibilityGestureDetectorTouchExplorer 的「前置过滤器」。它同时维护两套检测:

  • 快速手势 :用 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

recognizeGesturemStrokeBuffer 中的轨迹点构造 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(如 1210 等),这些 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 链入口,按输入源分发,按帧批处理 onInputEventbatchMotionEventprocessBatchedEventsenableFeatures
EventStreamTransformation.java 责任链接口定义 onMotionEventonKeyEventonAccessibilityEventclearEvents
TouchExplorer.java 触摸探索状态机,事件注入出口 onMotionEventhandleMotionEventStateTouchExploringonDoubleTapsendMotionEvent
AccessibilityGestureDetector.java 慢速/快速分流器 + 手势库匹配 onMotionEventrecognizeGestureonDoubleTap
ReceivedPointerTracker.java 跟踪原始输入指针 getPrimaryPointerIdgetLastReceivedEvent
InjectedPointerTracker.java 跟踪已注入到视图层的指针 isInjectedPointerDowngetLastInjectedHoverEventForClick

九、小结

输入事件拦截的完整数据流:

复制代码
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 在这条链中的处理路径(KeyboardInterceptorKeyEventDispatcher)。

相关推荐
天空之城--1 小时前
MT管理器Android逆向工程完全指南:从入门到实战
android
Anhty2 小时前
2026九月最新变声器测评:iOS安卓双端适配,低延迟运行更稳定
android·人工智能·功能测试·ios·智能手机
渡我白衣4 小时前
并查集:基础认识与模拟实现
android·java·javascript·数据结构·c++·算法·并查集
Android-Flutter6 小时前
flutter GetX 详解
android·flutter
MyFreeIT7 小时前
Android Manual
android
hai_android8 小时前
AIDL 实现 IPC 进程间通信
android·android studio
AFinalStone9 小时前
Android 7系统无障碍服务(五)AccessibilityNodeInfo 的构建与查询
android·无障碍服务
今天要早睡_10 小时前
C++ 核心语法速过:命名空间、引用、函数重载与 nullptr 深度解析
android·java·c++
啦啦啦~~~22211 小时前
PC端+安卓端阅读器推荐!开源本地小说阅读器软件,
android·论文阅读·windows·开源软件·福昕阅读器