Android觸控事件(上)——事件的由來
從接觸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方法。
可以從搜尋結果中看到,這裡就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
這個類到底有什麼作用:
看註釋的意思是:將後期輸入事件傳遞給檢視層次結構。那麼就可以理解為輸入事件的傳遞了。那麼,先看下這個類是何時建立並在何處呼叫了onProcess
方法的。
可以看到,這個建立過程是在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的事件轉發,並且根據狀態去處理。上面的註釋挺齊全的,這裡需要注意:
- deliver方法傳遞事件是會根據QueuedInputEvent的mFlags 屬性來判斷是forward還是finish或者apply,這個屬性除了在一開始生成的時候賦值,其他修改的地方就是在finish方法中
- 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的哪個方法呢?還是跟著程式碼看一看吧:
看下程式碼:
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);
}
上面是排序圖(靈魂畫家。。),通過上面的程式碼可以看到,我們的事件是native層呼叫InputEventReceiver中的dispatchInputEvent方法進行傳遞的,在傳遞過程中需要對事件進行排序處理,即先來先處理。
好了,到了這裡基本上就是我們找到的整個觸控事件的“根源”了。當然,這裡肯定不是最初的起點,但是起點可能太深了,需要一點點挖掘,量力而為。
整個過程已經完成了,我覺得有必要從後面到前面在梳理一遍:
到此為止,事件從java層的事件產生到傳遞的整個流程就已經全部展示完成,下面會分析一個嚼爛了的點——Android事件的分發。
順便貼一張debug下方法呼叫圖,整個過程如下面所示:
關於為什麼貼這張圖?因為今天再寫下面的一篇文章的時候,搜到了一些關於這方面的知識。結果,有人連整個分發過程還不清楚。。不想多說。
相關文章
- Android觸控事件(下)——事件的分發Android事件
- Android觸控事件的應用Android事件
- 觸控事件事件
- Android觸控事件(續)——點選長按事件Android事件
- JS觸控事件JS事件
- 觸控事件02事件
- android 管理ViewGroup中的觸控事件AndroidView事件
- Android觸控事件全過程分析:由產生到Activity.dispatchTouchEvent()Android事件
- Android觸控事件傳遞機制Android事件
- Android中TouchEvent觸控事件機制Android事件
- Android 觸控事件處理機制Android事件
- ScrollView 觸控事件View事件
- Android中觸控事件的傳遞機制Android事件
- android 觸控(Touch)事件、點選(Click)事件的區別(詳細解析)Android事件
- 安卓觸控事件與單擊事件的區別安卓事件
- 初識Android觸控事件傳遞機制Android事件
- Unity觸控式螢幕觸控事件定義Unity事件
- Flutter:如何響應觸控事件Flutter事件
- 觸控事件獲取座標事件
- android觸控事件分發機制,曾困惑你我的地方Android事件
- 【Android Developers Training】 67. 響應觸控事件AndroidDeveloperAI事件
- 大領導給小明安排任務——Android觸控事件Android事件
- 十分鐘瞭解Android觸控事件原理(InputManagerService)Android事件
- 微信小程式之觸控事件(六)微信小程式事件
- Android觸控事件的酸甜苦辣以及詳細介紹Android事件
- 通過程式碼控制View的觸控事件被觸發View事件
- Android中的視窗座標體系和螢幕的觸控事件Android事件
- iOS開發系列--觸控事件、手勢識別、搖晃事件、耳機線控iOS事件
- 大領導又給小明安排任務——Android觸控事件Android事件
- 【透鏡系列】看穿 > 觸控事件分發 >事件
- iOS學習筆記05 觸控事件iOS筆記事件
- [翻譯]整合滑鼠、觸控 和觸控筆事件的Html5 Pointer Event Api事件HTMLAPI
- Android事件傳遞、多點觸控及滑動衝突的處理Android事件
- 那些你曾不知道的觸控事件—Android分發機制完全解析事件Android
- H5觸控事件判斷滑動方向H5事件
- 觸控事件分發核心機制優化吸收事件優化
- HTML5觸控事件(touchstart、touchmove和touchend) (轉)HTML事件
- 取消事件觸發事件