說明:終于寫到了事件分發機制的最後一篇,如果還沒看過Android學習筆記之事件分發機制(一)和Android學習筆記之事件分發機制(二)的話可以先看看,再結合源碼會有助于了解。
前言
第一篇主要講了
dispatchTouchEvent
、
onTouch
、
onTouchEvent
和
onClick
之間的關系。
第二篇主要講了事件的分發路徑: Activity -> ViewGroup -> View。
這兩篇都還有一些東西講得不是很清楚,是以這篇會順帶把之前一些難以了解的地方給講明白。
源碼版本
Android 22
其他版本的源碼可能會有一些不同,但大概的思路都是一樣的。
說明:為了節省篇幅和複雜性,源碼我隻提取了其中有用到的。具體的源碼請大家自己檢視。
主線一
首先,我們先來看這一條主線:
dispatchTouchEvent
、
onTouch
、
onTouchEvent
和
onClick
之間的關系。
找到View.java中的
dispatchTouchEvent
:
/**
* Pass the touch screen motion event down to the target view, or this
* view if it is the target.
*
* @param event The motion event to be dispatched.
* @return True if the event was handled by the view, false otherwise.
*/
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
先來看下注釋:
将點選螢幕事件分發到指定的View。
當事件被目前的View處理(消費)時傳回true,否則傳回false。
然後跳到第13行。if中有四個判斷條件。第一個和第二個我們可以直接認為是true了,因為
onTouch
能被執行也就意味着前兩個條件為true,不用去追蹤源碼了。看一下第三個條件吧,這裡的意思是判斷目前的View是否是Enable的,Button預設是Enable的,是以第三個條件也為true。也就是說,
onTouch
決定了result的值。假設
onTouch
傳回了true,result的值變為true。來到19行,第一個條件為false,是以直接跳出判斷,後面的
onTouchEvent
是不會被執行的。
是以第一個結論來了,目前View為Enable的前提下,隻有當
onTouch
傳回false時,
onTouchEvent
才會被執行。
假設我們将
onTouch
傳回false,再進入
onTouchEvent
中探個究竟。有點長,是以隻挑重點來講。
public boolean onTouchEvent(MotionEvent event) {
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != ) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != ;
if ((mPrivateFlags & PFLAG_PRESSED) != || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
break;
case MotionEvent.ACTION_DOWN:
...
case MotionEvent.ACTION_CANCEL:
...
case MotionEvent.ACTION_MOVE:
...
}
return true;
}
return false;
}
先看2-10行。如果目前View是被disable但仍然可以點選的,傳回true,即目前View消費掉此次事件,但沒有對它們做出反應。
從12行開始了一個很長的if塊,一直到74行(中間省略了很多代碼)。先不管if裡面是什麼,隻看12行和73行。如果目前View是可以點選的,最後會傳回true消費掉該事件。如果不可點選,直接傳回false。
好了,再來看12行到73行之間的代碼,在46行找到了
performClick();
看一下源碼
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
看到了很熟悉的
onClick
有沒有!執行完
onClick
後會傳回true,即消費掉該事件。
先來總結一下主線一吧。對于一個View來說,事件首先會到達
dispatchTouchEvent
,然後在該方法裡面會先執行
onTouch
,接着如果
onTouch
傳回false的話就去執行
onTouchEvent
,然後
onClick
方法在
onTouchEvent
中被調用。
onTouch
和
onTouchEvent
結合起來得到的最後結果會作為
dispatchTouchEvent
的傳回值。
看到這裡,希望你能看得明白。如果可以的話,那麼接下來的也會很好了解了,不過我更希望你順着這個思路自己分析Activity和ViewGroup的源碼。
主線二
主線二是: Activity -> ViewGroup -> View
先看Activity.java
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
結合之前第二篇的實驗結果,
onTouchEvent
最開始是沒有被執行的,也就是說,事件分發發生在這裡面。
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
當事件被消費的時候,
getWindow().superDispatchTouchEvent(ev)
傳回true,進而讓Activity的
dispatchTouchEvent
傳回true。
再來看
onTouchEvent
的源碼
/**
* Called when a touch screen event was not handled by any of the views
* under it. This is most useful to process touch events that happen
* outside of your window bounds, where there is no view to receive it.
*
* @param event The touch screen event being processed.
*
* @return Return true if you have consumed the event, false if you haven't.
* The default implementation always returns false.
*/
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
看一下注釋就可以了,當事件沒有被任何View處理的時候,事件會傳回給Activity處理。這也就可以解釋為什麼
onTouchEvent
有時候會被執行有時候沒被執行了。
接着看ViewGroup.java,隻看一個方法就好了。
/**
* Implement this method to intercept all touch screen motion events. This
* allows you to watch events as they are dispatched to your children, and
* take ownership of the current gesture at any point.
*
* <p>Using this function takes some care, as it has a fairly complicated
* interaction with {@link View#onTouchEvent(MotionEvent)
* View.onTouchEvent(MotionEvent)}, and using it requires implementing
* that method as well as this one in the correct way. Events will be
* received in the following order:
*
* <ol>
* <li> You will receive the down event here.
* <li> The down event will be handled either by a child of this view
* group, or given to your own onTouchEvent() method to handle; this means
* you should implement onTouchEvent() to return true, so you will
* continue to see the rest of the gesture (instead of looking for
* a parent view to handle it). Also, by returning true from
* onTouchEvent(), you will not receive any following
* events in onInterceptTouchEvent() and all touch processing must
* happen in onTouchEvent() like normal.
* <li> For as long as you return false from this function, each following
* event (up to and including the final up) will be delivered first here
* and then to the target's onTouchEvent().
* <li> If you return true from here, you will not receive any
* following events: the target view will receive the same event but
* with the action {@link MotionEvent#ACTION_CANCEL}, and all further
* events will be delivered to your onTouchEvent() method and no longer
* appear here.
* </ol>
*
* @param ev The motion event being dispatched down the hierarchy.
* @return Return true to steal motion events from the children and have
* them dispatched to this ViewGroup through onTouchEvent().
* The current target will receive an ACTION_CANCEL event, and no further
* messages will be delivered here.
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
在第二篇中已經講過了這個方法,現在是想來看下它的注釋。
大緻意思是說:實作這個方法來截獲所有的觸摸螢幕事件,可以在事件發給你(ViewGroup,下同)的孩子之前監聽到事件,并接管這些事件,進而使你的孩子無法收到這些觸摸事件。
使用這個方法需要小心點,它和
View.onTouchEvent
有着複雜的互動,使用這個方法需要同時也重寫
onTouchEvent
。事件會以下面的順序接收到:
- 你會在這裡收到
事件ACTION_DOWN
-
事件會被你的孩子處理或者你自己的ACTION_DOWN
處理。這意味着你必須在onTouchEvent
中傳回true,進而你才能繼續看到其餘的事件(而不是尋找你的父節點去處理)。還有,在onTouchEvent
中傳回true的話,在onTouchEvent
中你不會再接收到剩下的任何事件,所有的事件會像正常情況一樣在onInterceptTouchEvent
中被處理。onTouchEvent
- 如果在該方法中傳回false的話,接下來的事件會先被分發到這裡,然後到達目标View的
。onTouchEvent
- 如果在該方法中傳回true的話,目标View會接收到
。進一步的事件将不會出現在這裡而是直接到達你的ACTION_CANCEL
onTouchEvent
方法。
傳回true會從你的子節點中偷走事件,然後将事件分發給自己的
處理,目标View會收到onTouchEvent
事件,進一步的消息将不會出現在ACTION_CANCEL
中。onInterceptTouchEvent
講了一大段,其實講得有點啰嗦。大緻意思就是如果在這個方法中傳回true的話,事件會被自己的
onTouchEvent
方法處理,不會傳遞到孩子節點中。同時,在
onTouchEvent
中要傳回true,否則系統就會去尋找父節點處理該事件。
主線一和主線二就講到這裡了,希望大家能自己分析一下源碼,再自己寫一寫效果會加倍的.
在第二篇的最後還遺留了一個問題,看完這篇,分析起來就很清晰了.
當
onInterceptTouchEvent
傳回true的時候, CustomLayout自己的
onTouchEvent
會被調用,最後傳回
super.onTouchEvent(event)
,而這裡的結果最後又會作為
dispatchTouchEvent
的傳回值,進而判斷是否消費了該事件.為了友善大家檢視,我再貼一下圖
從結果來看,
super.onTouchEvent(event)
的值為false.不信?自己試試看呗.傳回false後,該事件沒有被任何View消費(注意:該事件是不會分發給CustomButton的),最後回傳給了MainActivity自己處理,由于CustomLayout沒有消費該事件,是以
ACTION_DOWN
在MainActivity中又被處理了一次.
後來,我們讓CustomLayout中的
onTouchEvent
傳回true,即CustomLayout消費了該事件,是以才有了後面的事件.
The End
安卓的事件分發機制寫到這裡總算完了,希望這幾篇博文能讓你對事件分發機制有進一步的了解.