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

  1. 消费即终止 :handler 处理完事件后若未调用 mNext,事件即被消费。
  2. 取消事件必须向下游广播 :如果 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)。

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui