首頁 > 軟體

Android開發Input系統觸控事件分發

2023-03-02 18:01:20

引言

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, /*byref*/ 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, /*byref*/ 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)) {
        /* Case 1: New splittable pointer going down, or need target for hover or scroll. */
        // 觸控點的獲取 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 /*addOutsideTargets*/, true /*addPortalWindows*/);
        // 省略 ... 處理視窗異常的情況 ...
        // 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觸控事件分發的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com