Android觸控事件(上)——事件的由來

ksuu發表於2017-12-15

從接觸Android開發以來,貌似Android的事件就一直伴隨著我。從一開始的setOnclickListener到後來的setOnTouchListener以及各種手勢的事件,關於Android的事件傳遞機制,我覺得很多人都看了不止一遍了。藉著這次大總結,我覺得有必要對這部分進行一下總結了。
之前寫了關於View的測量、佈局和繪製的過程,在繪製完成後,介面的元素就已經展示出來了。光有花裡胡哨的頁面對於一個完整的App是不夠的,因為我們需要的不僅僅是頁面展示,還包括了頁面互動,不是還有個職位叫UE(互動設計師)嘛。
說起互動,那麼可以說的就很多了。比如我點選這個按鈕會怎樣?長按又如何?這裡面有很多東西需要深入探究,我們需要做到知其然並且知其所以然,這裡我就假裝一次小白(咳咳,現在是大白。。)一點點分析Android的觸控事件。

作為小白,我只知道在View是所有子View的爸爸(這不廢話嘛),所以我肯定知道觸控事件在View中一定有實現。翻開原始碼找一找,果不其然:

public boolean dispatchTouchEvent(MotionEvent event) {
    ......
    // 具體實現暫時不看
}

找到關於觸控事件的分發了,那麼問題來了:這個事件分發是在何時呼叫的呢?我們知道View是一個基類,如果手機接收到觸控事件時肯定直接或間接呼叫的是基類的dispatchTouchEvent方法,所以需要查詢下有那個類呼叫了dispatchTouchEvent方法。不查不知道,一查嚇一跳,N多個類都有呼叫這個dispatchTouchEvent方法。定睛一看,原來都是View的子類(虛驚一場)。既然外面沒有,那就找View當前有沒有方法呼叫吧。別說,還真有一個——dispatchPointerEvent:

// 程式碼簡單,我喜歡。。
public final boolean dispatchPointerEvent(MotionEvent event) {
    // 判斷是不是觸控事件
    if (event.isTouchEvent()) {
        return dispatchTouchEvent(event);
    } else {
        return dispatchGenericMotionEvent(event);
    }
}

這個方法程式碼很少,我這種小白也是能夠理解的:通過判斷當前的事件是不是觸控事件,如果是的話則需要將觸控事件分發。好了,現在知道了dispatchPointerEvent方法呼叫了dispatchTouchEvent方法。所以現在我們需要找到誰呼叫了dispatchPointerEvent方法

img_b404fc900b46ed33f2d075c39913624d.png
搜尋結果

可以從搜尋結果中看到,這裡就ViewRootImpl呼叫了這個方法。從這裡我就可以知道,觸控事件的分發肯定是通過ViewRootImpl進行分發的。那麼,先去看下這個方法:

private int processPointerEvent(QueuedInputEvent q) {
    // 獲取輸入事件,並將其轉換成MotionEvent
    final MotionEvent event = (MotionEvent)q.mEvent;

    mAttachInfo.mUnbufferedDispatchRequested = false;
    final View eventTarget =
            (event.isFromSource(InputDevice.SOURCE_MOUSE) && mCapturingView != null) ?
                    mCapturingView : mView;
    mAttachInfo.mHandlingPointerEvent = true;
    // 呼叫dispatchPointerEvent去處理這次事件
    boolean handled = eventTarget.dispatchPointerEvent(event);
    maybeUpdatePointerIcon(event);
    mAttachInfo.mHandlingPointerEvent = false;
    if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
        mUnbufferedInputDispatch = true;
        if (mConsumeBatchedInputScheduled) {
            scheduleConsumeBatchedInputImmediately();
        }
    }
        // 如果被處理返回FINISH_HANDLED ,否則返回轉發狀態
    return handled ? FINISH_HANDLED : FORWARD;
}

可以看到這個方法是ViewRootImpl的內部類ViewPostImeInputStage的方法,並且這個方法被onProcess呼叫,接下來需要看下ViewPostImeInputStage這個類到底有什麼作用:

img_8510823cfb3107ad2b4e33df49922fde.png
ViewPostImeInputStage類

看註釋的意思是:將後期輸入事件傳遞給檢視層次結構。那麼就可以理解為輸入事件的傳遞了。那麼,先看下這個類是何時建立並在何處呼叫了onProcess方法的。

img_ee440a3ba77d751eb651d91c97797249.png
建立

可以看到,這個建立過程是在ViewRootImpl的setView方法中,在之前寫的Activity顯示到Window的過程中講到setView是在Activity顯示的時候呼叫的方法,通過WindowManagerGlobal的addView方法呼叫了ViewRootImpl的setView方法。在這個方法裡面建立了ViewPostImeInputStage物件,在這裡也可以看到一個很有趣的現象:前面建立好的物件又當作引數傳入了下一個建立的物件,所以這邊需要看下這裡面到底有什麼玄機。這裡我們看下這些類的父類InputStage

abstract class InputStage {
    private final InputStage mNext;

    protected static final int FORWARD = 0;
    protected static final int FINISH_HANDLED = 1;
    protected static final int FINISH_NOT_HANDLED = 2;
    
    // 構造方法裡面傳入了InputStage作為下一個將要轉發的InputStage
    /**
     * Creates an input stage.
     * @param next The next stage to which events should be forwarded.
     */
    public InputStage(InputStage next) {
        mNext = next;
    }

    /**
     * Delivers an event to be processed.
     * 提供要被處理的事件。
     */
    public final void deliver(QueuedInputEvent q) {
        // 如果事件的flag是完成了,則轉發事件
        if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
            forward(q);
        //如果需要丟棄這個事件
        } else if (shouldDropInputEvent(q)) {
            // 完成,但是傳入的是false
            finish(q, false);
        } else {
            // 應用處理,裡面呼叫了onProcess即在過程中執行,當初次進入時應該會進入這個方法
            apply(q, onProcess(q));
        }
    }

    /**
     * Marks the the input event as finished then forwards it to the next stage.
     */
    protected void finish(QueuedInputEvent q, boolean handled) {
        q.mFlags |= QueuedInputEvent.FLAG_FINISHED;
        if (handled) {
            q.mFlags |= QueuedInputEvent.FLAG_FINISHED_HANDLED;
        }
        forward(q);
    }

    /**
     * Forwards the event to the next stage.
     * 轉發事件到下一個階段
     */
    protected void forward(QueuedInputEvent q) {
        onDeliverToNext(q);
    }

    /**
     * Applies a result code from {@link #onProcess} to the specified event.
     */
    protected void apply(QueuedInputEvent q, int result) {
        if (result == FORWARD) {
            forward(q);
        } else if (result == FINISH_HANDLED) {
            finish(q, true);
        } else if (result == FINISH_NOT_HANDLED) {
            finish(q, false);
        } else {
            throw new IllegalArgumentException("Invalid result: " + result);
        }
    }

    /**
     * Called when an event is ready to be processed.
     * 返回這個處理事件的程式碼 
     * 如FORWARD(轉發)FINISH_HANDLED(完成,已經處理)FINISH_NOT_HANDLED(完成,沒有處理)
     * 這個方法具體實現應當由其子類實現
     * @return A result code indicating how the event was handled.
     */
    protected int onProcess(QueuedInputEvent q) {
        return FORWARD;
    }

    /**
     * Called when an event is being delivered to the next stage.
     * 傳遞到下一個InputState
     */
    protected void onDeliverToNext(QueuedInputEvent q) {
        if (DEBUG_INPUT_STAGES) {
            Log.v(mTag, "Done with " + getClass().getSimpleName() + ". " + q);
        }
        // 如果下一個不為空,則呼叫deliver去處理;否則,完成這次輸入事件的處理
        if (mNext != null) {
            mNext.deliver(q);
        } else {
            finishInputEvent(q);
        }
    }

    protected boolean shouldDropInputEvent(QueuedInputEvent q) {
        // 如果當前傳入的View為空或者沒有被新增,此時應該拋棄調輸入事件
        if (mView == null || !mAdded) {
            Slog.w(mTag, "Dropping event due to root view being removed: " + q.mEvent);
            return true;
        } else if ((!mAttachInfo.mHasWindowFocus
                && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) || mStopped
                || (mIsAmbientMode && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON))
                || (mPausedForTransition && !isBack(q.mEvent))) {
            // This is a focus event and the window doesn`t currently have input focus or
            // has stopped. This could be an event that came back from the previous stage
            // but the window has lost focus or stopped in the meantime.
            // 這是一個焦點事件,視窗當前沒有輸入焦點或已經停止。 
            // 這可能是一個事件,從前一個階段回來,但視窗失去了重點或停止在此期間。
            if (isTerminalInputEvent(q.mEvent)) {
                // Don`t drop terminal input events, however mark them as canceled.
                // 不要丟棄終端輸入事件,但將它們標記為已取消。
                q.mEvent.cancel();
                Slog.w(mTag, "Cancelling event due to no window focus: " + q.mEvent);
                return false;
            }

            // Drop non-terminal input events.
            Slog.w(mTag, "Dropping event due to no window focus: " + q.mEvent);
            return true;
        }
        return false;
    }

    void dump(String prefix, PrintWriter writer) {
        if (mNext != null) {
            mNext.dump(prefix, writer);
        }
    }

    private boolean isBack(InputEvent event) {
        if (event instanceof KeyEvent) {
            return ((KeyEvent) event).getKeyCode() == KeyEvent.KEYCODE_BACK;
        } else {
            return false;
        }
    }
}

從上面的程式碼可以看出,InputStage是單向連結串列結構,從上到下依次處理,並將根據處理結果賦值給事件然後轉發到下一個狀態處理。當全部處理完成後,會呼叫finishInputEvent這個方法去完成這次輸入事件的處理。現在可以解釋剛才的那個現象了:我們傳入的Stage作為接收上層Stage的事件轉發,並且根據狀態去處理。上面的註釋挺齊全的,這裡需要注意:

  1. deliver方法傳遞事件是會根據QueuedInputEvent的mFlags 屬性來判斷是forward還是finish或者apply,這個屬性除了在一開始生成的時候賦值,其他修改的地方就是在finish方法中
  2. onProcess在InputStage中只是返回了一個FORWARD狀態碼,其子類會根據自身處理返回相應的狀態碼
    看完InputStage後,我們需要看下ViewPostImeInputStage的onProcess方法
@Override
protected int onProcess(QueuedInputEvent q) {
    // 按鍵事件處理
    if (q.mEvent instanceof KeyEvent) {
        return processKeyEvent(q);
    } else {
        // 獲取事件的源頭
        final int source = q.mEvent.getSource();
        // 觸控
        if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
            return processPointerEvent(q);
        // 安卓軌跡球類似與滑鼠等輸入
        } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
            return processTrackballEvent(q);
        } else {
            // 其他的輸入
            return processGenericMotionEvent(q);
        }
    }
}

關於processPointerEvent我們在上面已經寫過,這裡不贅述。
好了,現在知道輸入事件是在各種InputStage中處理的,那麼到底是哪裡呼叫了InputStage的哪個方法呢?還是跟著程式碼看一看吧:

img_967f2685613098f729f33fcd48199fdc.png
呼叫位置

看下程式碼:

private void deliverInputEvent(QueuedInputEvent q) {
    Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
            q.mEvent.getSequenceNumber());
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
    }

    InputStage stage;
    // 如果true,則使用最後一個InputStage
    if (q.shouldSendToSynthesizer()) {
        stage = mSyntheticInputStage;
    } else {
        // 根據shouldSkipIme來判斷使用哪個InputStage
        stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
    }
    
    // 呼叫deliver方法
    if (stage != null) {
        stage.deliver(q);
    } else {
        finishInputEvent(q);
    }
}
// 根據mFlags是否為FLAG_UNHANDLED來判斷
public boolean shouldSendToSynthesizer() {
    if ((mFlags & FLAG_UNHANDLED) != 0) {
        return true;
    }
    return false;
}

下面需要查詢哪裡呼叫了deliverInputEvent方法(方法呼叫較多):

void doProcessInputEvents() {
    // Deliver all pending input events in the queue.
    while (mPendingInputEventHead != null) {
        // 從頭部得到當前的事件,頭部指向next
        QueuedInputEvent q = mPendingInputEventHead;
        mPendingInputEventHead = q.mNext;
        if (mPendingInputEventHead == null) {
            mPendingInputEventTail = null;
        }
        q.mNext = null;
        // 事件數量-1
        mPendingInputEventCount -= 1;
        Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
                mPendingInputEventCount);

        long eventTime = q.mEvent.getEventTimeNano();
        long oldestEventTime = eventTime;
        if (q.mEvent instanceof MotionEvent) {
            MotionEvent me = (MotionEvent)q.mEvent;
            if (me.getHistorySize() > 0) {
                oldestEventTime = me.getHistoricalEventTimeNano(0);
            }
        }
        mChoreographer.mFrameInfo.updateInputEventTime(eventTime, oldestEventTime);
        // 傳遞事件
        deliverInputEvent(q);
    }

    // We are done processing all input events that we can process right now
    // so we can clear the pending flag immediately.
    if (mProcessInputEventsScheduled) {
        mProcessInputEventsScheduled = false;
        mHandler.removeMessages(MSG_PROCESS_INPUT_EVENTS);
    }
}

// 給輸入事件排序
void enqueueInputEvent(InputEvent event,
        InputEventReceiver receiver, int flags, boolean processImmediately) {
    adjustInputEventForCompatibility(event);
    // 生成輸入事件,這個通過池來實現
    QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);

    // Always enqueue the input event in order, regardless of its time stamp.
    // We do this because the application or the IME may inject key events
    // in response to touch events and we want to ensure that the injected keys
    // are processed in the order they were received and we cannot trust that
    // the time stamp of injected events are monotonic.
    // 始終按順序排列輸入事件,而不管其時間戳。 
    // 我們這樣做是因為應用程式或IME可能會響應觸控事件而注入關鍵事件,
    // 並且我們希望確保注入的鍵以接收到的順序進行處理,並且我們不能相信注入事件的時間戳是單調的。
    // 主要意思是說還是需要排序,下面是排序過程(後面會有圖解釋下)
    QueuedInputEvent last = mPendingInputEventTail;
    // 設定最後一個事件等於事件的尾部
    if (last == null) {
        // 如果為空則頭部和尾部都等於這個事件
        mPendingInputEventHead = q;
        mPendingInputEventTail = q;
    } else {
        // 否則的話讓事件的尾部的next等於當前這個事件,並將mPendingInputEventTail指向最後一個事件
        last.mNext = q;
        mPendingInputEventTail = q;
    }
    // 資料數量+1
    mPendingInputEventCount += 1;
    Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
            mPendingInputEventCount);

    // 如果是立即執行(這裡只看這個)
    if (processImmediately) {
        doProcessInputEvents();
    } else {
        scheduleProcessInputEvents();
    }
}

// 從名字上可以看出這是一個關於輸入事件的接收者
final class WindowInputEventReceiver extends InputEventReceiver {
    public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
        super(inputChannel, looper);
    }

    @Override
    public void onInputEvent(InputEvent event) {
        enqueueInputEvent(event, this, 0, true);
    }

    @Override
    public void onBatchedInputEventPending() {
        if (mUnbufferedInputDispatch) {
            super.onBatchedInputEventPending();
        } else {
            scheduleConsumeBatchedInput();
        }
    }

    @Override
    public void dispose() {
        unscheduleConsumeBatchedInput();
        super.dispose();
    }
}
InputEventReceiver.java
// 看註釋可以知道這個方法被native層呼叫,所以我覺得到這裡就應該算是java層的起點了
// Called from native code.
@SuppressWarnings("unused")
private void dispatchInputEvent(int seq, InputEvent event) {
    mSeqMap.put(event.getSequenceNumber(), seq);
    onInputEvent(event);
}
img_72b5ebd6e5a9bbc2d3c50bad9354767e.png
排序圖

上面是排序圖(靈魂畫家。。),通過上面的程式碼可以看到,我們的事件是native層呼叫InputEventReceiver中的dispatchInputEvent方法進行傳遞的,在傳遞過程中需要對事件進行排序處理,即先來先處理
好了,到了這裡基本上就是我們找到的整個觸控事件的“根源”了。當然,這裡肯定不是最初的起點,但是起點可能太深了,需要一點點挖掘,量力而為。
整個過程已經完成了,我覺得有必要從後面到前面在梳理一遍:

img_6b4dfba7601fde494b01c7a785b845f9.png
流程圖

到此為止,事件從java層的事件產生到傳遞的整個流程就已經全部展示完成,下面會分析一個嚼爛了的點——Android事件的分發。

img_3815a54d0837b9046404e3c728feccb8.png

順便貼一張debug下方法呼叫圖,整個過程如下面所示:

img_edc578c68c8a9509bd99218a57c861d4.png

關於為什麼貼這張圖?因為今天再寫下面的一篇文章的時候,搜到了一些關於這方面的知識。結果,有人連整個分發過程還不清楚。。不想多說。

img_7253b454e50b81acd09838694e11dd6c.png


相關文章