我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Android开发Input系统触摸事件分发

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Android开发Input系统触摸事件分发

引言

Input系统: InputReader 处理触摸事件 分析了 InputReader 对触摸事件的处理流程,最终的结果是把触摸事件包装成 NotifyMotionArgs,然后分发给下一环。根据 Input系统: InputManagerService的创建与启动 可知,下一环是 InputClassifier。然而系统目前并不支持 InputClassifier 的功能,因此事件会被直接发送到 InputDispatcher。

Input系统: 按键事件分发 分析了按键事件的分发流程,虽然分析的目标是按键事件,但是也从整体上,描绘了事件分发的框架。而本文分析触摸事件的分发流程,也会用到这个框架,因此重复内容不再赘述。

1. InputDispatcher 收到触摸事件

void InputDispatcher::notifyMotion(const NotifyMotionArgs* args) {
    if (!validateMotionEvent(args->action, args->actionButton, args->pointerCount,
                             args->pointerProperties)) {
        return;
    }
    uint32_t policyFlags = args->policyFlags;
    // 来自InputReader/InputClassifier的 motion 事件,都是受信任的
    policyFlags |= POLICY_FLAG_TRUSTED;
    android::base::Timer t;
    // 1. 对触摸事件执行截断策略
    // 触摸事件入队前,查询截断策略,查询的结果保存到参数 policyFlags
    mPolicy->interceptMotionBeforeQueueing(args->displayId, args->eventTime,  policyFlags);
    if (t.duration() > SLOW_INTERCEPTION_THRESHOLD) {
        ALOGW("Excessive delay in interceptMotionBeforeQueueing; took %s ms",
              std::to_string(t.duration().count()).c_str());
    }
    bool needWake;
    { // acquire lock
        mLock.lock();
        if (shouldSendMotionToInputFilterLocked(args)) {
            // ...
        }
        // 包装成 MotionEntry
        // Just enqueue a new motion event.
        std::unique_ptr<MotionEntry> newEntry =
                std::make_unique<MotionEntry>(args->id, args->eventTime, args->deviceId,
                                              args->source, args->displayId, policyFlags,
                                              args->action, args->actionButton, args->flags,
                                              args->metaState, args->buttonState,
                                              args->classification, args->edgeFlags,
                                              args->xPrecision, args->yPrecision,
                                              args->xCursorPosition, args->yCursorPosition,
                                              args->downTime, args->pointerCount,
                                              args->pointerProperties, args->pointerCoords, 0, 0);
        // 2. 把触摸事件加入收件箱
        needWake = enqueueInboundEventLocked(std::move(newEntry));
        mLock.unlock();
    } // release lock
    // 3. 如果有必要,唤醒线程处理触摸事件
    if (needWake) {
        mLooper->wake();
    }
}

InputDispatcher 收到触摸事件后的处理流程,与收到按键事件的处理流程非常相似

  • 对触摸事件进行截断策略查询。参考【1.1 截断策略查询】
  • 把触摸事件加入 InputDispatcher 收件箱,然后唤醒线程处理触摸事件。

1.1 截断策略查询

void NativeInputManager::interceptMotionBeforeQueueing(const int32_t displayId, nsecs_t when,
        uint32_t&amp; policyFlags) {
    bool interactive = mInteractive.load();
    if (interactive) {
        policyFlags |= POLICY_FLAG_INTERACTIVE;
    }
    // 受信任,并且是非注入的事件
    if ((policyFlags &amp; POLICY_FLAG_TRUSTED) &amp;&amp; !(policyFlags &amp; POLICY_FLAG_INJECTED)) {
        if (policyFlags &amp; POLICY_FLAG_INTERACTIVE) {
            // 设备处于交互状态下,受信任且非注入的事件,直接发送给用户,而不经过截断策略处理
            policyFlags |= POLICY_FLAG_PASS_TO_USER;
        } else {
            // 只有设备处于非交互状态,触摸事件才需要执行截断策略
            JNIEnv* env = jniEnv();
            jint wmActions = env-&gt;CallIntMethod(mServiceObj,
                        gServiceClassInfo.interceptMotionBeforeQueueingNonInteractive,
                        displayId, when, policyFlags);
            if (checkAndClearExceptionFromCallback(env,
                    "interceptMotionBeforeQueueingNonInteractive")) {
                wmActions = 0;
            }
            handleInterceptActions(wmActions, when,  policyFlags);
        }
    } else { // 注入事件,或者不受信任事件
        // 只有在交互状态下,才传递给用户
        // 注意,这里还有另外一层意思: 非交互状态下,不发送给用户
        if (interactive) {
            policyFlags |= POLICY_FLAG_PASS_TO_USER;
        }
    }
}
void NativeInputManager::handleInterceptActions(jint wmActions, nsecs_t when,
        uint32_t&amp; policyFlags) {
    if (wmActions &amp; WM_ACTION_PASS_TO_USER) {
        policyFlags |= POLICY_FLAG_PASS_TO_USER;
    }
}

一个触摸事件,必须满足下面三种情况,才执行截断策略

  • 触摸事件是受信任的。来自输入设备的触摸事件都是受信任的。
  • 触摸事件是非注入的。monkey 的原理就是注入触摸事件,因此它的事件是不需要经过截断策略处理的。
  • 设备处于非交互状态。一般来说,非交互状态指的就是显示屏处于灭屏状态。

另外还需要关注的是,事件在什么时候是不需要经过截断策略,有两种情况

  • 对于受信任且非注入的触摸事件,如果设备处于交互状态,直接发送给用户。 也就是说,如果显示屏处于亮屏状态,输入设备产生的触摸事件一定会发送给窗口。
  • 对于不受信任,或者注入的触摸事件,如果设备处于交互状态,也是直接发送给用户。也就是说,如果显示屏处于亮屏状态,monkey 注入的触摸事件,也是直接发送给窗口的。

最后还要注意一件事,如果一个触摸事件是不受信任的事件,或者是注入事件,当设备处于非交互状态下(通常指灭屏),那么它不经过截断策略,也不会发送给用户,也就是会被丢弃。

在实际工作中处理的触摸事件,通常都是来自输入设备,它肯定是受信任的,而且非注入的,因此它只有在设备处于非交互状态下(一般指灭屏)下,非会执行截断策略,而如果设备处于交互状态(通常指亮屏),会被直接分发给窗口。

现在来看下截断策略的具体实现

// PhoneWindowManager.java
    public int interceptMotionBeforeQueueingNonInteractive(int displayId, long whenNanos,
            int policyFlags) {
        // 1. 如果策略要求唤醒屏幕,那么截断这个触摸事件
        // 一般来说,唤醒屏幕的策略取决于设备的配置文件
        if ((policyFlags &amp; FLAG_WAKE) != 0) {
            if (wakeUp(whenNanos / 1000000, mAllowTheaterModeWakeFromMotion,
                    PowerManager.WAKE_REASON_WAKE_MOTION, "android.policy:MOTION")) {
                // 返回 0,表示截断触摸事件
                return 0;
            }
        }
        // 2. 判断非交互状态下,是否截断事件
        if (shouldDispatchInputWhenNonInteractive(displayId, KEYCODE_UNKNOWN)) {
            // 返回这个值,表示不截断事件,也就是事件分发给用户
            return ACTION_PASS_TO_USER;
        }
        // 忽略 theater mode
        if (isTheaterModeEnabled() &amp;&amp; (policyFlags &amp; FLAG_WAKE) != 0) {
            wakeUp(whenNanos / 1000000, mAllowTheaterModeWakeFromMotionWhenNotDreaming,
                    PowerManager.WAKE_REASON_WAKE_MOTION, "android.policy:MOTION");
        }
        // 3. 默认截断触摸事件
        // 返回0,表示截断事件
        return 0;
    }
    private boolean shouldDispatchInputWhenNonInteractive(int displayId, int keyCode) {
        // Apply the default display policy to unknown displays as well.
        final boolean isDefaultDisplay = displayId == DEFAULT_DISPLAY
                || displayId == INVALID_DISPLAY;
        final Display display = isDefaultDisplay
                ? mDefaultDisplay
                : mDisplayManager.getDisplay(displayId);
        final boolean displayOff = (display == null
                || display.getState() == STATE_OFF);
        if (displayOff &amp;&amp; !mHasFeatureWatch) {
            return false;
        }
        // displayOff 表示屏幕处于 off 状态,但是非 off 状态,并不表示一定是亮屏状态
        // 对于 doze 状态,屏幕处于 on 状态,但是屏幕可能仍然是黑的
        // 因此,只要屏幕处于 on 状态,并且显示了锁屏,触摸事件不会截断
        if (isKeyguardShowingAndNotOccluded() &amp;&amp; !displayOff) {
            return true;
        }
        // 对于触摸事件,keyCode 的值为 KEYCODE_UNKNOWN
        if (mHasFeatureWatch &amp;&amp; (keyCode == KeyEvent.KEYCODE_BACK
                || keyCode == KeyEvent.KEYCODE_STEM_PRIMARY
                || keyCode == KeyEvent.KEYCODE_STEM_1
                || keyCode == KeyEvent.KEYCODE_STEM_2
                || keyCode == KeyEvent.KEYCODE_STEM_3)) {
            return false;
        }
        // 对于默认屏幕,如果设备处于梦境状态,那么触摸事件不截断
        // 因为 doze 组件需要接收触摸事件,可能会唤醒屏幕
        if (isDefaultDisplay) {
            IDreamManager dreamManager = getDreamManager();
            try {
                if (dreamManager != null &amp;&amp; dreamManager.isDreaming()) {
                    return true;
                }
            } catch (RemoteException e) {
                Slog.e(TAG, "RemoteException when checking if dreaming", e);
            }
        }
        // Otherwise, consume events since the user can't see what is being
        // interacted with.
        return false;
    }    

截断策略是否截断触摸事件,取决于策略的返回值,有两种情况

  • 返回 0,表示截断触摸事件。
  • 返回 ACTION_PASS_TO_USER ,表示不截断触摸事件,也就是把触摸事件分发给用户/窗口。

下面列举触摸事件截断与否的情况,但是要注意一个前提,设备处于非交互状态(一般就是指灭屏状态)

  • 事件会被传递给用户,也就是不截断,情况如下
    • 有锁屏,并且显示屏处于非 off 状态。注意,非 off 状态,并不是表示屏幕处于 on(亮屏) 状态,也可能是 doze 状态(屏幕处于低电量状态),doze 状态屏幕也是黑的。
    • 梦境状态。因为梦境状态下会运行 doze 组件。
  • 事件被截断,情况如下
    • 策略标志位包含 FLAG_WAKE ,它会导致屏幕被唤醒,因此需要截断触摸事件。FLAG_WAKE 一般来自于输入设备的配置文件。
    • 没有锁屏,没有梦境,也没有 FLAG_WAKE,默认就会截断。

从上面的分析可以总结出了两条结论

  • 如果系统有组件在运行,例如,锁屏、doze组件,那么触摸事件需要分发到这些组件,因此不会被截断。
  • 如果没有组件运行,触摸事件都会被截断。触摸事件由于需要唤醒屏幕,而导致被截断,只是其中一个特例。

2. InputDispatcher 分发触摸事件

由 Input系统: InputManagerService的创建与启动 可知,InputDispatcher 通过线程循环来处理收件箱中的事件,而且一次循环只能处理一个事件

void InputDispatcher::dispatchOnce() {
    nsecs_t nextWakeupTime = LONG_LONG_MAX;
    { // acquire lock
        std::scoped_lock _l(mLock);
        mDispatcherIsAlive.notify_all();
        if (!haveCommandsLocked()) {
            // 1. 分发一个触摸事件
            dispatchOnceInnerLocked(&amp;nextWakeupTime);
        }
        // 触摸事件的分发过程不会产生命令
        if (runCommandsLockedInterruptible()) {
            nextWakeupTime = LONG_LONG_MIN;
        }
        // 2. 计算线程下次唤醒的时间点,以便处理 anr
        const nsecs_t nextAnrCheck = processAnrsLocked();
        nextWakeupTime = std::min(nextWakeupTime, nextAnrCheck);
        if (nextWakeupTime == LONG_LONG_MAX) {
            mDispatcherEnteredIdle.notify_all();
        }
    } // release lock
    // 3. 线程休眠指定的时长
    nsecs_t currentTime = now();
    int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);
    mLooper-&gt;pollOnce(timeoutMillis);
}

一次线程循环处理触摸事件的过程如下

  • 分发一个触摸事件。
  • 当事件分发给窗口后,会计算一个窗口反馈的超时时间,利用这个时间,计算线程下次唤醒的时间点。
  • 利用上一步计算出的线程唤醒的时间点,计算出线程最终需要休眠多长时间。当线程被唤醒后,会检查接收触摸时间的窗口,是否反馈超时,如果超时,会引发 ANR。

现在来看看如何分发一个触摸事件

void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    nsecs_t currentTime = now();
    if (!mDispatchEnabled) {
        resetKeyRepeatLocked();
    }
    if (mDispatchFrozen) {
        return;
    }
    // 这里是优化 app 切换的延迟
    // mAppSwitchDueTime 是 app 切换的超时时间,如果小于当前时间,那么表明app切换超时了
    // 如果app切换超时,那么在app切换按键事件之前的未处理的事件,都将会被丢弃
    bool isAppSwitchDue = mAppSwitchDueTime <= currentTime;
    if (mAppSwitchDueTime < *nextWakeupTime) {
        *nextWakeupTime = mAppSwitchDueTime;
    }
    // mPendingEvent 表示正在处理的事件
    if (!mPendingEvent) {
        if (mInboundQueue.empty()) {
            // ...
        } else {
            // 1. 从收件箱队列中取出事件
            mPendingEvent = mInboundQueue.front();
            mInboundQueue.pop_front();
            traceInboundQueueLengthLocked();
        }
        // 如果这个事件需要传递给用户,那么需要同上层的 PowerManagerService,此时有用户行为,这个作用就是延长亮屏的时间
        if (mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER) {
            pokeUserActivityLocked(*mPendingEvent);
        }
    }
    ALOG_ASSERT(mPendingEvent != nullptr);
    bool done = false;
    // 检测丢弃事件的原因
    DropReason dropReason = DropReason::NOT_DROPPED;
    if (!(mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER)) {
        // 被截断策略截断
        dropReason = DropReason::POLICY;
    } else if (!mDispatchEnabled) {
        // 一般是由于系统正在系统或者正在关闭
        dropReason = DropReason::DISABLED;
    }
    if (mNextUnblockedEvent == mPendingEvent) {
        mNextUnblockedEvent = nullptr;
    }
    switch (mPendingEvent->type) {
        // ....
        case EventEntry::Type::MOTION: {
            std::shared_ptr<MotionEntry> motionEntry =
                    std::static_pointer_cast<MotionEntry>(mPendingEvent);
            if (dropReason == DropReason::NOT_DROPPED && isAppSwitchDue) {
                // app 切换超时,导致触摸事件被丢弃
                dropReason = DropReason::APP_SWITCH;
            }
            if (dropReason == DropReason::NOT_DROPPED && isStaleEvent(currentTime, *motionEntry)) {
                // 10s 之前的事件,已经过期
                dropReason = DropReason::STALE;
            }
            // 这里是优化应用无响应的一个措施,会丢弃mNextUnblockedEvent之前的所有触摸事件
            if (dropReason == DropReason::NOT_DROPPED && mNextUnblockedEvent) {
                dropReason = DropReason::BLOCKED;
            }
            // 2. 分发触摸事件
            done = dispatchMotionLocked(currentTime, motionEntry, &dropReason, nextWakeupTime);
            break;
        }
        // ...
    }
    // 3. 如果事件被处理,重置一些状态,例如 mPendingEvent
    // 返回 true,就表示已经处理了事件
    // 事件被丢弃,或者发送完毕,都会返回 true
    // 返回 false,表示暂时不知道如何处理事件,因此线程会休眠
    // 然后,线程再次被唤醒时,再来处理这个事件
    if (done) {
        if (dropReason != DropReason::NOT_DROPPED) {
            dropInboundEventLocked(*mPendingEvent, dropReason);
        }
        mLastDropReason = dropReason;
        // 重置 mPendingEvent
        releasePendingEventLocked();
        // 立即唤醒,处理下一个事件
        *nextWakeupTime = LONG_LONG_MIN; // force next poll to wake up immediately
    }
}

Input系统: 按键事件分发 已经分析过 InputDispatcher 的线程循环。而对于触摸事件,是通过 InputDispatcher::dispatchMotionLocked() 进行分发

bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, std::shared_ptr<MotionEntry> entry,
                                           DropReason* dropReason, nsecs_t* nextWakeupTime) {
    if (!entry->dispatchInProgress) {
        entry->dispatchInProgress = true;
    }
    // 1. 触摸事件有原因需要丢弃,那么不走后面的分发流程
    if (*dropReason != DropReason::NOT_DROPPED) {
        setInjectionResult(*entry,
                           *dropReason == DropReason::POLICY ? InputEventInjectionResult::SUCCEEDED
                                                             : InputEventInjectionResult::FAILED);
        return true;
    }
    bool isPointerEvent = entry->source & AINPUT_SOURCE_CLASS_POINTER;
    std::vector<InputTarget> inputTargets;
    bool conflictingPointerActions = false;
    InputEventInjectionResult injectionResult;
    if (isPointerEvent) {
        // 寻找触摸的窗口,窗口保存到 inputTargets
        // 2. 为触摸事件,寻找触摸的窗口
        // 触摸的窗口保存到 inputTargets 中
        injectionResult =
                findTouchedWindowTargetsLocked(currentTime, *entry, inputTargets, nextWakeupTime,
                                               &conflictingPointerActions);
    } else {
        // ...
    }
    if (injectionResult == InputEventInjectionResult::PENDING) {
        // 返回 false,表示暂时不知道如何处理这个事件,这会导致线程休眠
        // 等线程下次被唤醒时,再来处理这个事件
        return false;
    }
    // 走到这里,表示触摸事件已经被处理,因此保存处理的结果
    // 只要返回的不是 InputEventInjectionResult::PENDING
    // 都表示事件被处理,无论是权限拒绝还是失败,或是成功
    setInjectionResult(*entry, injectionResult);
    if (injectionResult == InputEventInjectionResult::PERMISSION_DENIED) {
        ALOGW("Permission denied, dropping the motion (isPointer=%s)", toString(isPointerEvent));
        return true;
    }
    if (injectionResult != InputEventInjectionResult::SUCCEEDED) {
        CancelationOptions::Mode mode(isPointerEvent
                                              ? CancelationOptions::CANCEL_POINTER_EVENTS
                                              : CancelationOptions::CANCEL_NON_POINTER_EVENTS);
        CancelationOptions options(mode, "input event injection failed");
        synthesizeCancelationEventsForMonitorsLocked(options);
        return true;
    }
    // 走到这里,表示触摸事件已经成功找到触摸的窗口
    // Add monitor channels from event's or focused display.
    // 3. 触摸事件找到了触摸窗口,在分发给窗口前,保存 global monitor 到 inputTargets 中
    // 开发者选项中的 Show taps 和 Pointer location,利用的 global monitor
    addGlobalMonitoringTargetsLocked(inputTargets, getTargetDisplayId(*entry));
    if (isPointerEvent) {
        // ... 省略 portal window 处理的代码
    }
    if (conflictingPointerActions) {
        // ...
    }
    // 4. 分发事件给 inputTargets 中的所有窗口
    dispatchEventLocked(currentTime, entry, inputTargets);
    return true;
}

一个触摸事件的分发过程,可以大致总结为以下几个过程

  • 如果有原因表明触摸事件需要被丢弃,那么触摸事件不会走后面的分发流程,即被丢弃。
  • 通常触摸事件是发送给窗口的,因此需要为触摸事件寻找触摸窗口。窗口最终被保存到 inputTargets 中。参考【2.1 寻找触摸的窗口】
  • inputTargets 保存触摸窗口后,还要保存 global monitor 窗口。例如开发者选项中的 Show taps 和 Pointer location,就是利用这个窗口实现的。
  • 启动分发循环,把触摸事件分发给 inputTargets 保存的窗口。 由于 Input系统: 按键事件分发 已经分发过这个过程,本文不再分析。

2.1 寻找触摸的窗口

InputEventInjectionResult InputDispatcher::findTouchedWindowTargetsLocked(
        nsecs_t currentTime, const MotionEntry& entry, std::vector<InputTarget>& inputTargets,
        nsecs_t* nextWakeupTime, bool* outConflictingPointerActions) {
    // ...
    // 6. 对于非 DOWN 事件,获取已经 DOWN 事件保存的 TouchState
    // TouchState 保存了接收 DOWN 事件的窗口
    const TouchState* oldState = nullptr;
    TouchState tempTouchState;
    std::unordered_map<int32_t, TouchState>::iterator oldStateIt =
            mTouchStatesByDisplay.find(displayId);
    if (oldStateIt != mTouchStatesByDisplay.end()) {
        oldState = &(oldStateIt->second);
        tempTouchState.copyFrom(*oldState);
    }
    // ...
    // 第一个条件 newGesture 表示第一个手指按下
    // 后面一个条件,表示当前窗口支持 split motion,并且此时有另外一个手指按下
    if (newGesture || (isSplit && maskedAction == AMOTION_EVENT_ACTION_POINTER_DOWN)) {
        
        // 触摸点的获取 x, y 坐标
        int32_t x;
        int32_t y;
        int32_t pointerIndex = getMotionEventActionPointerIndex(action);
        if (isFromMouse) {
            // ...
        } else {
            x = int32_t(entry.pointerCoords[pointerIndex].getAxisValue(AMOTION_EVENT_AXIS_X));
            y = int32_t(entry.pointerCoords[pointerIndex].getAxisValue(AMOTION_EVENT_AXIS_Y));
        }
        // 这里检测是否是第一个手指按下
        bool isDown = maskedAction == AMOTION_EVENT_ACTION_DOWN;
        // 1. 对于 DOWN 事件,根据触摸事件的x,y坐标,寻找触摸窗口
        // 参数 addOutsideTargets 表示,只有在第一个手指按下时,如果没有找到触摸的窗口,
        // 那么需要保存那些可以接受 OUTSIZE 事件的窗口到 tempTouchState
        newTouchedWindowHandle =
                findTouchedWindowAtLocked(displayId, x, y, &tempTouchState,
                                          isDown , true );
        // 省略 ... 处理窗口异常的情况 ...
        // 2. 获取所有的 getsture monitor
        const std::vector<TouchedMonitor> newGestureMonitors = isDown
                ? selectResponsiveMonitorsLocked(
                          findTouchedGestureMonitorsLocked(displayId, tempTouchState.portalWindows))
                : tempTouchState.gestureMonitors;
        // 既没有找到触摸点所在的窗口,也没有找到 gesture monitor,那么此次寻找触摸窗口的任务就失败了
        if (newTouchedWindowHandle == nullptr && newGestureMonitors.empty()) {
            ALOGI("Dropping event because there is no touchable window or gesture monitor at "
                  "(%d, %d) in display %" PRId32 ".",
                  x, y, displayId);
            injectionResult = InputEventInjectionResult::FAILED;
            goto Failed;
        }
        // 走到这里,表示找到了触摸的窗口,或者找到 gesture monitor
        if (newTouchedWindowHandle != nullptr) {
            // 马上要保存窗口了,现在获取窗口的 flag
            int32_t targetFlags = InputTarget::FLAG_FOREGROUND | InputTarget::FLAG_DISPATCH_AS_IS;
            if (isSplit) {
                targetFlags |= InputTarget::FLAG_SPLIT;
            }
            if (isWindowObscuredAtPointLocked(newTouchedWindowHandle, x, y)) {
                targetFlags |= InputTarget::FLAG_WINDOW_IS_OBSCURED;
            } else if (isWindowObscuredLocked(newTouchedWindowHandle)) {
                targetFlags |= InputTarget::FLAG_WINDOW_IS_PARTIALLY_OBSCURED;
            }
            // Update hover state.
            if (maskedAction == AMOTION_EVENT_ACTION_HOVER_EXIT) {
                newHoverWindowHandle = nullptr;
            } else if (isHoverAction) {
                newHoverWindowHandle = newTouchedWindowHandle;
            }
            // Update the temporary touch state.
            // 如果窗口支持 split,那么用 tempTouchState 保存窗口的时候,要特别保存 pointer id
            BitSet32 pointerIds;
            if (isSplit) {
                uint32_t pointerId = entry.pointerProperties[pointerIndex].id;
                pointerIds.markBit(pointerId);
            }
            // 3. tempTouchState 保存找到的触摸的窗口
            // 如果是真的找到的触摸窗口,那么这里就是保存,如果是找到可以接受 OUTSIDE 的窗口,那么这里是更新
            tempTouchState.addOrUpdateWindow(newTouchedWindowHandle, targetFlags, pointerIds);
        } else if (tempTouchState.windows.empty()) {
            // If no window is touched, set split to true. This will allow the next pointer down to
            // be delivered to a new window which supports split touch.
            tempTouchState.split = true;
        }
        if (isDown) {
            // tempTouchState 保存所有的 gesture monitor
            // 4. 第一个手指按下时,tempTouchState 保存 gesture monitor
            tempTouchState.addGestureMonitors(newGestureMonitors);
        }
    } else {
        // ...
    }
    if (newHoverWindowHandle != mLastHoverWindowHandle) {
        // ....
    }
    {
        // 权限检测 ...
    }
    // 保存接收 AMOTION_EVENT_ACTION_OUTSIDE 的窗口
    if (maskedAction == AMOTION_EVENT_ACTION_DOWN) {
        // ...
    }
    // 第一个手指按下时,保存壁纸窗口
    if (maskedAction == AMOTION_EVENT_ACTION_DOWN) { // 
        // ...
    }
    // 走到这里,表示没有异常情况了
    injectionResult = InputEventInjectionResult::SUCCEEDED;
    // 5. 把 tempTouchState 保存了触摸窗口和gesture monitor,保存到 inputTargets 中
    for (const TouchedWindow& touchedWindow : tempTouchState.windows) {
        addWindowTargetLocked(touchedWindow.windowHandle, touchedWindow.targetFlags,
                              touchedWindow.pointerIds, inputTargets);
    }
    for (const TouchedMonitor& touchedMonitor : tempTouchState.gestureMonitors) {
        addMonitoringTargetLocked(touchedMonitor.monitor, touchedMonitor.xOffset,
                                  touchedMonitor.yOffset, inputTargets);
    }
    // Drop the outside or hover touch windows since we will not care about them
    // in the next iteration.
    tempTouchState.filterNonAsIsTouchWindows();
Failed:
    // ...
    // 6. 缓存 tempTouchState
    if (maskedAction != AMOTION_EVENT_ACTION_SCROLL) {
        if (tempTouchState.displayId >= 0) {
            mTouchStatesByDisplay[displayId] = tempTouchState;
        } else {
            mTouchStatesByDisplay.erase(displayId);
        }
    } 
    return injectionResult;
}

为触摸事件寻找触摸窗口的过程,极其复杂。虽然这段代码被我省略了很多过程,但是我估计读者也会看得头晕。

对于 DOWN 事件

  • 根据 x,y 坐标寻找触摸的窗口。参考【2.1.1 根据坐标找到触摸窗口】
  • 获取所有的 gesture monitor 窗口 。
  • 把触摸窗口保存到 tempTouchState 中。
  • 把所有的 gesture monitor 窗口保存到 tempTouchState 中。
  • 为 tempTouchState 保存所有窗口,创建 InputTarget 对象,并保存到参数 inputTargets 中。参考【2.1.2 保存窗口】
  • 使用 mTouchStatesByDisplay 缓存 tempTouchState。

gesture monitor 是为了实现手势功能而添加的一个窗口。什么是手势功能? 例如在屏幕的左边/右边,向屏幕中央滑动,会触发返回手势。这个手势功能用来替代导航键。在下一篇文章中,我会剖析这个手势功能的原理。

对于非 DOWN 事件,一般为 MOVE, UP 事件

  • 获取 DOWN 事件缓存的 tempTouchState。 因为 tempTouchState 保存了处理 DOWN 事件的触摸窗口和 gesture monitor,非 DOWN 事件,也会发送给这些窗口。
  • 重复 DOWN 事件的第5步。

当分析的代码量很大的时候,我们需要有一个整体的观念。为触摸事件寻找触摸窗口,最终的结果就是把找到的窗口保存到参数 inputTargets 中,后面会把事件分发给 inputTargets 保存的窗口。

2.1.1 根据坐标找到触摸窗口

// addOutsideTargets 在第一个手指按下是为 true
// addPortalWindows 值为 true
// ignoreDragWindow 默认为 false
sp<InputWindowHandle> InputDispatcher::findTouchedWindowAtLocked(int32_t displayId, int32_t x,
                                                                 int32_t y, TouchState* touchState,
                                                                 bool addOutsideTargets,
                                                                 bool addPortalWindows,
                                                                 bool ignoreDragWindow) {
    if ((addPortalWindows || addOutsideTargets) && touchState == nullptr) {
        LOG_ALWAYS_FATAL(
                "Must provide a valid touch state if adding portal windows or outside targets");
    }
    // Traverse windows from front to back to find touched window.
    // 从前到后,遍历窗口
    const std::vector<sp<InputWindowHandle>>& windowHandles = getWindowHandlesLocked(displayId);
    for (const sp<InputWindowHandle>& windowHandle : windowHandles) {
        // ignoreDragWindow 默认为 false
        if (ignoreDragWindow && haveSameToken(windowHandle, mDragState->dragWindow)) {
            continue;
        }
        // 获取窗口信息
        const InputWindowInfo* windowInfo = windowHandle->getInfo();
        // 匹配属于特定屏幕的窗口
        if (windowInfo->displayId == displayId) {
            auto flags = windowInfo->flags;
            // 窗口要可见
            if (windowInfo->visible) {
                // 窗口要可触摸
                if (!flags.test(InputWindowInfo::Flag::NOT_TOUCHABLE)) {
                    // 检测是否为触摸模型: 可获取焦点,并且不允许窗口之外的触摸事件发送到它后面的窗口
                    bool isTouchModal = !flags.test(InputWindowInfo::Flag::NOT_FOCUSABLE) &&
                            !flags.test(InputWindowInfo::Flag::NOT_TOUCH_MODAL);
                    // 窗口是触摸模型,或者触摸的坐标点落在窗口上
                    if (isTouchModal || windowInfo->touchableRegionContainsPoint(x, y)) {
                        int32_t portalToDisplayId = windowInfo->portalToDisplayId;
                        // 如果是 portal window
                        if (portalToDisplayId != ADISPLAY_ID_NONE &&
                            portalToDisplayId != displayId) {
                            if (addPortalWindows) {
                                // For the monitoring channels of the display.
                                // touchState 保存 portal window
                                touchState->addPortalWindow(windowHandle);
                            }
                            // 递归调用,获取 portal display id 下的触摸窗口
                            return findTouchedWindowAtLocked(portalToDisplayId, x, y, touchState,
                                                             addOutsideTargets, addPortalWindows);
                        }
                        // 不是 portal window,直接返回找到的窗口
                        return windowHandle;
                    }
                }
                // 走到这里,表示没有找到触摸窗口。也就是说,既没有找到触摸模型的窗口,也没有找到包含触摸点的窗口
                // 当第一个手指按下是,addOutsideTargets 值为 true
                // NOT_TOUCH_MODAL 和 WATCH_OUTSIDE_TOUCH 一起使用,当第一个手指按下时,如果落在窗口之外
                // 窗口会收到 MotionEvent.ACTION_OUTSIDE 事件
                if (addOutsideTargets && flags.test(InputWindowInfo::Flag::WATCH_OUTSIDE_TOUCH)) {
                    touchState->addOrUpdateWindow(windowHandle,
                                                  InputTarget::FLAG_DISPATCH_AS_OUTSIDE,
                                                  BitSet32(0));
                }
            }
        }
    }
    return nullptr;
}

这里涉及一个 portal window 的概念,由于我没有找到具体使用的地方,我大致猜测它的意思就是,设备外接一个屏幕,然后在主屏幕上显示一个窗口来操作这个外接屏幕。后面的分析,我将略过 portal window 的部分。当然,触摸掌握了触摸事件的分发流程,以后遇到了 portal window 的事情,再来分析,应该没问题的。

寻找触摸点所在的窗口,其实就是从上到下遍历所有窗口,然后找到满足条件的窗口。

窗口首先要满足前置条件

  • 窗口要在指定屏幕上。
  • 窗口要可见。
  • 窗口要可触摸。

满足了所有的前置条件后,只要满足以下任意一个条件,那么就找到了触摸点所在的窗口

  • 是触摸模型的窗口: 可获取焦点,并且不允许窗口之外的触摸事件发送到它后面的窗口。
  • 触摸点的 x,y 坐标落在窗口坐标系中。

2.1.2 保存窗口

// InputDispatcher 保存触摸窗口
void InputDispatcher::addWindowTargetLocked(const sp<InputWindowHandle>& windowHandle,
                                            int32_t targetFlags, BitSet32 pointerIds,
                                            std::vector<InputTarget>& inputTargets) {
    std::vector<InputTarget>::iterator it =
            std::find_if(inputTargets.begin(), inputTargets.end(),
                         [&windowHandle](const InputTarget& inputTarget) {
                             return inputTarget.inputChannel->getConnectionToken() ==
                                     windowHandle->getToken();
                         });
    const InputWindowInfo* windowInfo = windowHandle->getInfo();
    // 创建 InputTarget,并保存到参数 inputTargets
    if (it == inputTargets.end()) {
        InputTarget inputTarget;
        std::shared_ptr<InputChannel> inputChannel =
                getInputChannelLocked(windowHandle->getToken());
        if (inputChannel == nullptr) {
            ALOGW("Window %s already unregistered input channel", windowHandle->getName().c_str());
            return;
        }
        inputTarget.inputChannel = inputChannel;
        inputTarget.flags = targetFlags;
        inputTarget.globalScaleFactor = windowInfo->globalScaleFactor;
        inputTarget.displaySize =
                int2(windowHandle->getInfo()->displayWidth, windowHandle->getInfo()->displayHeight);
        inputTargets.push_back(inputTarget);
        it = inputTargets.end() - 1;
    }
    ALOG_ASSERT(it->flags == targetFlags);
    ALOG_ASSERT(it->globalScaleFactor == windowInfo->globalScaleFactor);
    // 保存 InputTarget 后,在保存窗口的坐标转换参数,
    // 这个参数可以把显示屏的坐标,转换为窗口的坐标
    it->addPointers(pointerIds, windowInfo->transform);
}
// InputDispatcher 保存 gesture monitor
void InputDispatcher::addMonitoringTargetLocked(const Monitor& monitor, float xOffset,
                                                float yOffset,
                                                std::vector<InputTarget>& inputTargets) {
    InputTarget target;
    target.inputChannel = monitor.inputChannel;
    target.flags = InputTarget::FLAG_DISPATCH_AS_IS;
    ui::Transform t;
    t.set(xOffset, yOffset);
    target.setDefaultPointerTransform(t);
    inputTargets.push_back(target);
}

对于触摸事件,无论是触摸窗口,还是 gesture monitor,都会被转化为 InputTarget,然后保存到参数 inputTargets 中。当后面启动分发循环后,触摸事件就会发送到 inputTargets 保存的窗口中。

结束

本文从整体上分析了触摸事件的分发过程,很多细节并没有深入去分析,例如,当窗口无响应时,如何优化事件分发。但是,只要你掌握了基本的流程,这些细节你可以自行分析。

本文的某些分析过程,跨度可能很大,那是因为这些知识已经在前面的文章中讲过,如果你阅读本文,感觉有点困难,那么请先阅读前面的文章,打好基础。

理论的文章总有一些枯燥,但是不妨碍我继续向前,下一篇文章,将以此为基础,分析那个代替系统导航栏的手势功能是如何实现的,这也将作为 Input 系统的收官之作。

以上就是Android开发Input系统触摸事件分发的详细内容,更多关于Android Input触摸事件分发的资料请关注编程网其它相关文章!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Android开发Input系统触摸事件分发

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Android开发Input系统触摸事件分发

这篇文章主要为大家介绍了Android开发Input系统触摸事件分发示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-03-02

Android怎么开发Input系统触摸事件分发

本篇内容介绍了“Android怎么开发Input系统触摸事件分发”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!引言Input系统: Inpu
2023-07-05

Input系统之InputReader处理触摸事件案例

这篇文章主要为大家介绍了Input系统之InputReader处理触摸事件案例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-02-14

如何实现Android触摸事件分发的原理分析

如何实现Android触摸事件分发的原理分析,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。一:前言最近在学Android的触摸事件分发,我觉得网上说的太杂太乱,而且有很多博客都
2023-06-26

HarmonyOS自定义控件之触摸事件与事件分发

事件分发是一套比较重要同时也比较复杂的机制,如果不熟悉这套机制,那么在遇到稍微复杂的滑动失效问题就会觉得手足无措。在这里通过打印日志的方式来摸索HarmonyOS上的事件的传递机制。

iOS开发中的touchesBegan触摸事件怎么实现

在iOS开发中,可以通过重写`touchesBegan`方法来处理触摸事件。`touchesBegan`方法会在用户触摸屏幕时被调用,你可以在该方法中编写代码来响应触摸事件。以下是一个示例代码,演示如何在视图中实现`touchesBegan
2023-09-14

利用DrawerLayout和触摸事件分发实现抽屉侧滑效果

本文实例为大家分享了DrawerLayout和触摸事件分发实现抽屉侧滑效果的具体代码,供大家参考,具体内容如下效果展示 还是看代码实在,直接上菜了。 1.MainActivity的代码:public class MainActivity e
2023-05-30

android多屏触摸相关的详解方案-安卓framework开发手机车载车机系统开发课程

背景 直播免费视频课程地址:https://www.bilibili.com/video/BV1hN4y1R7t2/ 在做双屏相关需求开发过程中,经常会有对两个屏幕都要求可以正确触摸的场景。但是目前我们模拟器默认创建的双屏其实是没有办法进行
2023-08-30

Android开发系列三之窗口的常用事件

相关阅读: Android开发系列二之窗口Activity的生命周期 Android开发系列一用按钮实现显示时间设置窗口标题事件和在Activity之间跳转。 新建一个项目,新建两个Activity:MainActivity,TitleAc
2022-06-06

Python开发注意事项:处理并发和分布式系统时的注意事项

Python开发注意事项:处理并发和分布式系统时的注意事项随着互联网的快速发展,越来越多的软件系统需要处理并发性和分布式计算。Python作为一种灵活且易于使用的编程语言,被广泛应用于这些领域。然而,处理并发和分布式系统时,Python开发
Python开发注意事项:处理并发和分布式系统时的注意事项
2023-11-23

Android开发中怎样调用系统Email发送邮件(多种调用方式)

我们都知道,在Android中调用其他程序进行相关处理,几乎都是使用的Intent,所以,Email也不例外。 在Android中,调用Email有三种类型的Intent: Intent.ACTION_SENDTO 无附件的发送 Inten
2022-06-06

android开发教程之系统资源的使用方法 android资源文件

一、颜色资源1、颜色XML文件格式 代码如下: //resources根元素 #000000
2022-06-06

Android 13 将默认采用华为开发的只读文件系统

据报道,Android 13 最新的测试版中包括两个新功能的声明,声明内容显示 Google 正计划将 EROFS 引入 Android 13,使其成为默认的只读文件系统。

javascript内容将要从浏览者的系统剪贴板传送[粘贴]到页面中时触发此事件使用什么函数,详细讲解

当内容即将从浏览器剪贴板粘贴到页面时,可以使用navigator.clipboard.readText()函数触发事件。此函数允许读取剪贴板的文本内容。它需要浏览器的支持和用户的权限,可以通过async方法请求。若权限被授予,则函数返回一个Promise,包含读取的文本内容。支持因浏览器而异,因此需要检查兼容性。用户可以拒绝访问权限,处理此可能性很重要。
javascript内容将要从浏览者的系统剪贴板传送[粘贴]到页面中时触发此事件使用什么函数,详细讲解
2024-04-02

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录