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方法

搜尋結果
可以從搜尋結果中看到,這裡就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這個類到底有什麼作用:

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的事件轉發,並且根據狀態去處理。上面的註釋挺齊全的,這裡需要注意:

  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的哪個方法呢?還是跟著程式碼看一看吧:

呼叫位置
看下程式碼:

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事件的分發。
Android觸控事件(上)——事件的由來
順便貼一張debug下方法呼叫圖,整個過程如下面所示:
Android觸控事件(上)——事件的由來
關於為什麼貼這張圖?因為今天再寫下面的一篇文章的時候,搜到了一些關於這方面的知識。結果,有人連整個分發過程還不清楚。。不想多說。
Android觸控事件(上)——事件的由來

相關文章