源碼跟蹤分析View的事件分發機制

hv0377 7年前發布 | 5K 次閱讀 安卓開發 Android開發 移動開發

想要寫好Android的界面,解決View的滑動沖突是十分重要的,因此需要對Android的事件分發機制有一定了解和認識。之前校招面試的時候自己也被問過相關問題,加上自己最近在寫一個小Demo遇到的一些玄學Bug,索性就系統的學習了一下。下面分享一下學習成果,同時也希望有人能指出我的錯誤和不足。

寫在前面的話

  • 1.dispatchTouchEvent和onTouchEvent返回true則表示當前View要消耗點擊事件,就不再向下分發,返回false則開始向上回溯;
  • 2.ViewGroup是繼承自View類的,只不過ViewGroup是容器它可以有子View而View沒有;
  • 3.同一個事件序列是指從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結束,在這個過程中所產生的一系列事件,這個事件序列以down事件開始,中間夾雜著許多個move事件,最終以up事件結束;
  • 4.一個MotionEvent對象即封裝了一個點擊事件。

先介紹點擊事件分發過程中3個比較重要的方法:

public boolean dispatchTouchEvent(MotionEvent ev)

這個方法的作用是用來分發點擊事件的,如果點擊事件傳遞到了當前 View ,那么該方法一點會被調用,返回結果受當前 View 的onTouchEvent和下級 View 的dispatchTouchEvent的返回值的影響,表示是否消耗當前事件。如果所有 View 都不消耗,那么該點擊事件最后會交給 Activity 的onTouchEvent方法處理。

public boolean onInterceptEvent(MotionEvent ev)

在上述方法內部調用,返回結果表示是否攔截事件。如果當前View攔截了某個事件,那么在同一個事件序列中此方法不會被再次調用。

public boolean onTouchEvent(MotionEvent ev)

在dispatchTouchEvent中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前 View 無法再次接收到事件。

下面這幅圖是參照 圖解 Android 事件分發機制 這篇博客畫的一幅流程圖:

事件分發流程圖.png

從左上角開始,點擊事件總是先傳遞給Activity,再由activity向下分發。下面就按照圖中1-9的順序從源碼角度跟蹤點擊事件的去向。

1. Activity#dispatchTouchEvent -> ViewGroup#dispatchTouchEvent

當一個點擊事件發生后首先會傳遞給Activity,由Activity的dispatchTouchEvent(MotionEvent ev)來接收并進行分發,首先看一下Activity的dispatchTouchEvent方法源碼:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

onUserInteraction()方法是一個空方法,第一個if可以不看。接著看第二個if,即如果getWindow().superDispatchTouchEvent(ev)返回true,則Activity的dispatchTouchEvent返回true。

這時候點擊事件就從Activity傳遞到getWindow()返回的Window了,Android里面的Window類是一個抽象類,Window類的唯一一個實現類是PhoneWindow,接著看看PhoneWindow的superDispatchTouchEvent方法:

public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

先來看一下mDecor這個對象,這是一個DecorView類對象,這個DecorView是整個應用窗口的根View:

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker

看一下它的superDispatchTouchEvent方法:

public boolean superDispatchTouchEvent(MotionEvent event){    
    return super.dispatchTouchEvent(event);
}

因為DecorView是繼承自FrameLayout的,即間接繼承自GroupView,所以上述super.dispatchTouchEvent方法調用的即是GroupView的dispatchTouchEvent,自此點擊事件就傳遞到了GroupView了。

2.ViewGroup#dispatchTouchEvent -> Activity#onTouchEvent

回過頭看Activity的dispatchTouchEvent方法,如果getWindow().superDispatchTouchEvent(ev)返回了false即ViewGroup的dispatchTouchEvent返回false,則return onTouchEvent(ev)這時候點擊事件就交給Activity的onTouchEvent去處理了。

3. ViewGroup#dispatchTouchEvent -> ViewGroup#onInterceptTouchEvent

來看一下ViewGroup的dispatchTouchEvent 中是否攔截相關邏輯:

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); 
        } else {        
            intercepted = false;    
        }
    } else {    
        intercepted = true;
    }
    ...
}

先看第一個if的判斷條件,事件類型為ACTION_DOWN或者mFirstTouchTarget != null。這個mFirstTouchTarget 在當前ViewGroup的子View決定攔截處理事件時會被賦值,這時候mFirstTouchTarget != null成立。當ACTION_MOVE和ACTION_UP過來,actionMasked == MotionEvent.ACTION_DOWN不成立,這時候如果mFirstTouchTarget == null說明沒有子View要處理這次點擊事件,則intercepted = true, 即當前ViewGroup要攔截這次事件(因為沒有子View處理,而且事件能傳遞到當前ViewGroup,所以當前ViewGroup攔截)。如果mFirstTouchTarget != null則點擊事件再繼續往下傳遞。

如果第一個if條件成立,進入到if內部。mGroupFlags和FLAG_DISALLOW_INTERCEPT是2個標志位。mGroupFlags這個標志位我不知道是用來做什么的,也希望知道的能給我說明下。FLAG_DISALLOW_INTERCEPT是通過requestDisallowInterceptTouchEvent來設置,一般是通過在子View調用該方法來設置該標志,該標志一旦設置,ViewGroup將無法攔截除了ACTION_DOWN之外的其他點擊事件,因為ACTION_DOWN事件會重置FLAG_DISALLOW_INTERCEPT標志位。

第二個if的條件,如果disallowIntercept為false即允許攔截,intercepted = onInterceptTouchEvent(ev)這句執行,此時點擊事件就傳遞到了onInterceptTouchEvent。而onInterceptTouchEvent是默認返回false的,即不攔截。

public boolean onInterceptTouchEvent(MotionEvent ev) {
    return false;
}

4. ViewGroup#onInterceptTouchEvent -> ViewGroup#onTouchEvent

看一下ViewGroup點擊事件向下分發源碼

public boolean dispatchTouchEvent(MotionEvent ev) {
    ...
    final View[] children = mChildren;

    //遍歷所有子View
    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);   

        //判斷子View是否能接收點擊事件
        if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
            continue;
        }

        //判斷當前view是否已存在mFirstTouchTarget隊列中,當多點觸控時,多次點擊同一個view只保存一個
        newTouchTarget = getTouchTarget(child);    
        if (newTouchTarget != null) {
            newTouchTarget.pointerIdBits |= idBitsToAssign;        
            break;    
        }    

        resetCancelNextUpFlag(child);

        //事件已傳遞到子View
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){        
            mLastTouchDownTime = ev.getDownTime();        
            if (preorderedList != null) {            
                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;    
        }    
    }
    ...
}

上面的dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)里面調用的就是就是子View的dispatchTouchEvent,該方法中有如下一段代碼:

if (child == null) {
    handled = super.dispatchTouchEvent(event);
} else {
    handled = child.dispatchTouchEvent(event);
}

這里有2種情況。第一種情況,如果child == null,則handled = super.dispatchTouchEvent(event),此時會調用ViewGroup的父類View的dispatchTouchEvent,先來看一下該類中的部分方法:

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false ;
    ...
    if (onFilterTouchEventForSecurity(event)) {
        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 ;
}

ListenerInfo 在OnTouchListener 被設置的時候會初始化,onFilterTouchEventForSecurity做的是一些標志位判斷,所以直接來看這句

if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event))

當OnTouchListener 被設置后不為null并且OnTouchListener的onTouch返回了true,則result = true。那么下一個if判斷!result為false,根據&&運算符特性則onTouchEvent不會被調用。即只要設置了OnTouchListener則onTouchEvent就不會被調用,可見OnTouchListener的優先級高于onTouchEvent。到這里點擊事件就傳遞給onTouchEvent了,這里的onTouchEvent是GroupView的onTouchEvent,先記住這點。

5.ViewGroup#onTouchEvent -> Activity#onTouchEvent

如果onTouchEvent方法return false,將間接導致ViewGroup的dispatchTouchEvent返回false,則發生前面 2 中 的情況,即點擊事件傳遞到Activity的onTouchEvent。

6.ViewGroup#onInterceptTouchEvent -> View#dispatchTouchEvent

4中提到的2種情況,當child == null不成立時,此時會調用child.dispatchTouchEvent方法,這樣點擊事件就傳遞到了子View的dispatchTouchEvent方法了。

7.View#dispatchTouchEvent -> View#onTouchEvent

參考4中View的dispatchTouchEvent 方法的部分代碼,如果View設置了OnTouchListener,onTouchEvent就不會調用;反之,onTouchEvent會被調用,此時點擊事件就傳遞到View的onTouchEvent了。

8.View#diapatchTouchEvent -> ViewGroup#onTouchEvent

如果View的diapatchTouchEvent返回false說明當前View不準備消費點擊事件,那么前面提到的mFirstTouchTarget就為null ,因為前面說過只有子View明確表示要消費點擊事件,它才會被賦值。如果mFirstTouchTarget == null,ViewGroup就會自己處理點擊事件:

if (mFirstTouchTarget == null) {
    handled = dispatchTransformedTouchEvent(ev, canceled, null,  TouchTarget.ALL_POINTER_IDS);
}

dispatchTransformedTouchEvent這個方法在前面提到過,只不過當時第三個參數是child不是null,這樣前面child == null就直接成立,所以handled = super.dispatchTouchEvent(event)會被調用,從而調用onTouchEvent。

9.View#onTouchEvent -> ViewGroup#onTouchEvent

當點擊事件傳遞到View的onTouchEvent,但是該方法返回了false,間接導致View的dispatchTouchEvent返回了false,從而出現8當中的情況,從而ViewGroup的onTouchEvent被調用。

總結:

  • 1.點擊事件從Activity中的dispatchTouchEvent開始向下分發,向上回溯是決定getWindow().superDispatchTouchEvent(ev)的返回值;
  • 2.onInterceptTouchEvent方法不是每一次都會被調用,只有當傳遞過來的為ACTION_DOWN事件或者mFirstTouchTarget != null;
  • 3.View的dispatchTouchEvent方法和onTouchEvent方法返回了false,那么它的父容器View(如果有的話)的onTouchEvent方法一定被調用,因為此時child為null(參考8和9);
  • 4.子View的onTouchEvent要在父View的onTouchEvent之前被調用;
  • 5.onTouchListener的優先級高于onTouchEvent,如果前者被賦值,后者將被屏蔽;
  • 6.可以通過調用requestDisallowInterceptTouchEvent設置FLAG_DISALLOW_INTERCEPT這個標志位來攔截除了ACTION_DOWN以外的其他點擊事件,因為來ACTION_DOWN到來時會重置標志位。

參考鏈接:

圖解 Android 事件分發機制

參考書籍:

《Android開發藝術探索》

 

來自:http://www.jianshu.com/p/5597d6295130

 

 本文由用戶 hv0377 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!