Android View事件分發(fā)機制詳解
準備了一陣子,一直想寫一篇事件分發(fā)的文章總結(jié)一下,這個知識點實在是太重要了。
一個應(yīng)用的布局是豐富的,有TextView,ImageView,Button等,這些子View的外層還有ViewGroup,如RelativeLayout,LinearLayout。作為一個開發(fā)者,我們會思考,當點擊一個按鈕,Android系統(tǒng)是怎樣確定我點的就是按鈕而不是TextView的?然后還正確的響應(yīng)了按鈕的點擊事件。內(nèi)部經(jīng)過了一系列什么過程呢?
先鋪墊一些知識能更加清晰的理解事件分發(fā)機制:
1. 通過setContentView設(shè)置的View就是DecorView的子view,即DecorView是父容器。
2. 點擊屏幕時,在手指按下和抬起間,會產(chǎn)生很多事件,down…move…move…up,中間會有很多的move事件,這一系列的事件為一個事件序列
3. dispatchTouchEvent方法用于分發(fā)事件
4. onInterceptTouchEvent方法用于攔截事件
5. onTouchEvent方法用于處理事件
當一個點擊事件(MotionEvent)產(chǎn)生后,事件最先傳遞給當前的界面(Activity),這點是很好理解的。 Activity再將事件傳遞給窗口(Window),然后Window將事件傳遞給頂級View(DecorView)。此時,事件已經(jīng)到達了View了。之后頂級View就會按照事件分發(fā)機制去分發(fā)事件。具體是這樣的:
對于一個根ViewGroup來說,點擊事件產(chǎn)生后,首先會傳遞給它,這時它的 dispatchTouchEvent 方法就會被調(diào)用,如果這個ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前事件,接著事件就會交給這個ViewGroup處理,即它的onTouchEvent方法就會被調(diào)用。如果這個ViewGroup的onInterceptTouchEvent方法返回false,就表示它不攔截當前事件,這時當前事件就會繼續(xù)傳遞給它的子元素,接著子元素的dispatchTouchEvent方法就會被調(diào)用,如此反復(fù)直到事件被最終處理。
如果一個View的onTouchEvent方法返回false,那么它的父容器的onTouchEvent方法會被調(diào)用,如果它的父容器的onTouchEvent方法還是返回false,那就繼續(xù)往上拋,當所有的元素都不處理這個事件,那么這個事件會最終傳遞給Activity處理,即Activity的onTouchEvent方法會被調(diào)用。
好了,現(xiàn)在已經(jīng)鋪墊了基礎(chǔ),那么接下來就從源碼的角度來分析事件分發(fā)機制。
當然是從Activity的dispatchTouchEvent方法開始分析。源碼如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
如果當前事件是down的話,就調(diào)用onUserInteraction方法,onUserInteraction是一個空方法,我們可以暫時不搭理。然后調(diào)用getWindow方法獲取到當前Activity關(guān)聯(lián)的Window,Window再調(diào)用superDispatchTouchEvent方法將事件傳入進行分發(fā)。
如果superDispatchTouchEvent方法返回true的話, view已經(jīng)處理了事件。整個事件循環(huán)結(jié)束。如果返回false,沒有view處理這個事件。事件往上拋,那就Activity自己處理了,即Activity的onTouchEvent方法會被調(diào)用。
因為想要知道事件的整個分發(fā)過程,現(xiàn)在關(guān)注的是Window的superDispatchTouchEvent方法,那么就跟進去看看:
public abstract boolean superDispatchTouchEvent(MotionEvent event);
Window是一個抽象類,superDispatchTouchEvent是一個抽象的方法,那么我們必須要找到window的實現(xiàn)類才行,可是茫茫人海怎么找呢?看到window類的說明就明白了
* <p>The only existing implementation of this abstract class is * android.view.PhoneWindow, which you should instantiate when needing a * Window. */ public abstract class Window
意思是Window存在唯一的實現(xiàn)是android.view.PhoneWindow
那么PhoneWindow里的superDispatchTouchEvent方法就是我們要找的信息,如下:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
直接將事件傳遞給了DecorView。這時事件已經(jīng)是到達View了哦。
那么跟進DecorView的superDispatchTouchEvent方法看看,如下:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
內(nèi)部調(diào)用了父類的dispatchTouchEvent方法,那么DecorView的父類是什么呢?DecorView肯定是View的,那么剛才開篇提到,我們通過setContentView設(shè)置的View,是DecorView的子View。那么更加準確的說DecorView是一個ViewGroup。
private final class DecorView extends FrameLayout implements RootViewSurfaceTaker
可以看到DecorView是繼承自FrameLayout,F(xiàn)rameLayout是ViewGroup,也就是說DecorView是一個ViewGroup。
那么現(xiàn)在只需要關(guān)注ViewGroup的dispatchTouchEvent方法。繼續(xù)前進
ViewGroup的事件分發(fā)
ViewGroup的dispatchTouchEvent方法如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//代碼省略
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
//代碼省略
if (!canceled && !intercepted) {
//代碼省略
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildOrderedChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder
? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
//代碼省略
return handled;
}
代碼比較長,一點一點分析,先看到一開始的判斷
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)
mFirstTouchTarget != null的意義是ViewGroup不攔截事件并將事件交由子元素處理,先這樣記著,這從后面的addTouchTarget方法可以得出結(jié)論的。
然后又會來到這個if判斷。
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
}
那我們看看disallowIntercept。而disallowIntercept的賦值過程中,有一個 FLAG_DISALLOW_INTERCEPT 標記位
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
這個 FLAG_DISALLOW_INTERCEPT 標記位是可以通過requestDisallowInterceptTouchEvent方法來設(shè)置的。
回到if (!disallowIntercept)的判斷,進入這個if判斷后,就會來到
intercepted = onInterceptTouchEvent(ev);
調(diào)用onInterceptTouchEvent方法,詢問ViewGroup是否攔截事件。
讀到這里,可以回憶下開篇時鋪墊的結(jié)論,對于ViewGroup,點擊事件產(chǎn)生后,首先會傳遞給它,這時它的 dispatchTouchEvent 方法就會被調(diào)用,接著會調(diào)用它的onInterceptTouchEvent方法,如果這個ViewGroup的onInterceptTouchEvent方法返回true就表示它要攔截當前事件,接著事件就會交給這個ViewGroup處理,即它的onTouchEvent方法就會被調(diào)用。如果返回false表示不攔截,通常ViewGroup也是不攔截事件的。
那現(xiàn)在先分析不攔截的情況,不攔截那就好辦了的。經(jīng)過一系列的判斷,就會來到一個for循環(huán)遍歷。
for (int i = childrenCount - 1; i >= 0; i--)
這時ViewGroup開始分發(fā)傳遞事件,遍歷子元素了。
首先肯定需要過濾掉一些無關(guān)點擊事件的子元素的,判斷子元素是否能夠接收點擊事件,點擊事件的坐標是否落在子元素區(qū)域內(nèi)。
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
如果不能夠接收點擊事件或者點擊事件的坐標沒有落在子元素區(qū)域,就會跳出當前循環(huán),繼續(xù)遍歷下一個子元素。這下就知道了Android系統(tǒng)為什么能夠知道點擊的是Button而不是TextView,其實內(nèi)部就只是做了一個判斷嘛。
那么繼續(xù)分析,子元素符合以上兩個條件后,就將事件傳遞給這個子元素。會來到了這個判斷。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
執(zhí)行dispatchTransformedTouchEvent方法,將子元素傳進去。這個方法很重要,那么跟進看看
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
//代碼省略
}
我們看到child!=null的情況,如果子元素不為空,調(diào)用子元素的dispatchTouchEvent方法繼續(xù)分發(fā)事件,同時返回處理結(jié)果布爾值,這時就將事件傳遞到了子View處理。完成了一輪的事件分發(fā)。這個方法先到這里就好。
再看回ViewGroup的dispatchTouchEvent方法,如果dispatchTransformedTouchEvent方法返回true的話,這時事件已經(jīng)傳遞給子元素處理,ViewGroup已經(jīng)不管這個事件了。
那么就會進入if語句,最后會來到addTouchTarget方法,這個方法之前是提到過的,用于mFirstTouchTarget標記位的賦值。
那跟進這個方法看看
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
其實就是讓mFirstTouchTarget指向子元素。
執(zhí)行完這個addTouchTarget方法后,最終會到break語句,那么就會跳出整個for循環(huán)體。ViewGroup結(jié)束分發(fā)過程!
又回到dispatchTransformedTouchEvent方法,如果dispatchTransformedTouchEvent方法返回false,那么if語句的一大段代碼都不執(zhí)行了,而是回到for循環(huán)繼續(xù)遍歷子元素進行分發(fā)。如此重復(fù)完成事件的傳遞過程。
現(xiàn)在分析ViewGroup攔截事件的情況,如果ViewGroup攔截事件的話,那么就會進入以下這個判斷
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
注意到dispatchTransformedTouchEvent方法的第三個參數(shù)child傳入的是null,那么就是在dispatchTransformedTouchEvent方法中走以下的語句
if (child == null) {
handled = super.dispatchTouchEvent(event);
}
而ViewGroup是繼承自View的,那么就是ViewGroup自己處理事件了。這點我們以下分析了View的事件分發(fā)過程就能搞明白了。
以上就是ViewGroup的事件分發(fā)
那么現(xiàn)在分析已經(jīng)將事件傳遞給了子View的情況,View繼續(xù)調(diào)用dispatchTouchEvent方法,那我們看看View的dispatchTouchEvent方法。
View的事件分發(fā)
View的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) {
//代碼省略
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;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
相比于ViewGroup的dispatchTouchEvent方法,View的dispatchTouchEvent方法代碼量少了。也相對簡單些了。
首先會來到如下判斷:
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event))
li變量在哪里被賦值的呢?通常是在setOnClickListener方法或setOnTouchListener方法的時候。
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnTouchListener(OnTouchListener l) {
getListenerInfo().mOnTouchListener = l;
}
而這個getListenerInfo()如下:
ListenerInfo getListenerInfo() {
if (mListenerInfo != null) {
return mListenerInfo;
}
mListenerInfo = new ListenerInfo();
return mListenerInfo;
}
ListenerInfo是一個內(nèi)部類,里面存放的是各種監(jiān)聽事件的引用。
之后會判斷如下條件:
li.mOnTouchListener != null
同理只要setOnTouchListener方法設(shè)置了,這個引用就不空。這些都是好理解的。那關(guān)鍵到了,
li.mOnTouchListener.onTouch(this, event)
到了最后一個條件。這個onTouch方法是我們?nèi)崿F(xiàn)的,它也返回一個布爾值,如果返回true的話,那么就會進入這個if判斷最終返回true,跳出整個方法,那么我們可以看到接下來的onTouchEvent方法是不會得到執(zhí)行的。
也就是onTouch的執(zhí)行在onTouchEvent之前。那么如果我們也調(diào)用了setOnClickListener方法監(jiān)聽點擊事件的話,onClick方法是在哪里調(diào)用的呢?我們有理由相信是在onTouchEvent方法里調(diào)用的。那么就跟進看看。
public boolean onTouchEvent(MotionEvent event) {
//代碼省略
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
//代碼省略
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();
}
}
}
//代碼省略
break;
}
return true;
}
return false;
}
只要CLICKABLE或LONG_CLICKABLE不空, 就會處理這個事件,然而怎么保證CLICKABLE或LONG_CLICKABLE不空呢?其實細心的你會發(fā)現(xiàn),剛才上面貼出的setOnClickListener源代碼中,會將CLICKABL屬性設(shè)置會true
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
這樣就能進入if判斷去處理這個事件了,之后就會來到performClick()方法,應(yīng)該就是它了,跟進去看看吧。
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;
}
熟悉接口回調(diào)機制的你,一定也讀懂了performClick()方法的源碼,
li.mOnClickListener.onClick(this);
是在執(zhí)行這行代碼時,調(diào)用了我們熟悉的onClick方法
以上就是View的事件分發(fā)機制。
此時已經(jīng)將事件分發(fā)機制分析完了,由于我的技術(shù)的原因,駕馭的不好,有些關(guān)鍵點還是沒分析清楚,但我相信學完了這篇文章能讓我和你都對事件分發(fā)機制的實現(xiàn)有一個大致的認識,有這個已經(jīng)可以了,之后還可以一點點去強化鍛煉,深入理解事件分發(fā)機制。才能為自定義控件鋪墊良好的基礎(chǔ)。
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
OpenGL Shader實現(xiàn)光照發(fā)光體特效
這篇文章主要介紹了如何通過OpenGL Shader實現(xiàn)光照發(fā)光體特效,不同于陰影遮蓋,它是利用圓形繪制向內(nèi)部。感興趣的小伙伴可以了解一下2022-02-02
Android開發(fā)之圖形圖像與動畫(五)LayoutAnimationController詳解
LayoutAnimationController用于為一個layout里面的控件,或者是一個ViewGroup,里面的控件設(shè)置動畫效果,感興趣的朋友可以了解下啊,希望本文對你有所幫助2013-01-01
Android實現(xiàn)app應(yīng)用多語言切換功能
這篇文章主要為大家詳細介紹了Android實現(xiàn)app應(yīng)用多語言切換功能的相關(guān)資料,類似于微信的語言切換,感興趣的小伙伴們可以參考一下2016-08-08
利用smsmanager實現(xiàn)后臺發(fā)送短信示例
這篇文章主要介紹了android利用SmsManager可以實現(xiàn)后臺發(fā)送短信的方法,最近有使用說明,大家可以參考使用2014-01-01
Android 實現(xiàn)監(jiān)聽的四種方法詳解實例代碼
這篇文章主要介紹了Android 實現(xiàn)監(jiān)聽的方法詳解實例代碼的相關(guān)資料,這里整理了四種方法,需要的朋友可以參考下2016-10-10
自定義一個theme在不同的sdk環(huán)境下繼承不同的值
可能很多在高版本下編繹apk的同學,可能都曾有和我一樣的困惑,就是如何讓低版本的用戶也能有高版本的體驗?zāi)?/div> 2013-01-01最新評論

