Android事件分發原始碼歸納

大棋發表於2019-01-22

前提須知:

       1、DecorView是activity的根佈局。
       2、activity的window是PhoneWindow例項。
       3、DecorView是一個FrameLayout。
       4、ViewGroup繼承自View。沒有過載View#onTouchEvent()方法。
       5、原始碼版本為android 9.0。

一圖勝千言(PhoneWindow、DecorView和Activity佈局的關係)(ps:盜用下輝哥的圖):

Android事件分發原始碼歸納

簡單介紹:

ViewGroup:
       1、ViewGroup#dispatchTouchEvent()    對ViewGroup的事件進行分發。
       2、ViewGroup#onInterceptTouchEvent()    對ViewGroup的事件進行攔截。
       3、ViewGroup#onTouchEvent()    對ViewGroup的事件進行處理(消費)。

View:
       1、View#dispatchTouchEvent()    對View的事件進行排程。
       2、View#onTouchEvent()    對View的事件進行處理(消費)。

MotionEvent:
       1、MotionEvent.ACTION_DOWN    Down事件 使用者手指按下時觸發。
       2、MotionEvent.ACTION_MOVE    Move事件 使用者手指在螢幕上移動時觸發
       3、MotionEvent.ACTION_UP    Up事件 使用者手指抬起,離開螢幕時觸發
       4、MotionEvent.ACTION_CANCEL    Cancel事件

完整的MotionEvent = MotionEvent.ACTION_DOWN + n*MotionEvent.ACTION_MOVE + MotionEvent.ACTION_UP

其他:
      1、onTouchEvent()返回true時,表示該View或者ViewGroup消費此事件;返回false時,表示該View或者ViewGroup沒有消費事件。
      2、可對View或ViewGroup設定View.OnTouchListener進行觸控事件監聽。OnTouchListener#onTouch()可以對事件進行監聽並消費事件。

測試Demo:

Android事件分發原始碼歸納
      定義一個繼承View的MyButton類,過載dispatchTouchEvent()和onTouchEvent(),並新增列印輸出。

      定義一個繼承View的MyView類,過載dispatchTouchEvent()和onTouchEvent(),並新增列印輸出。

      定義一個繼承FrameLayout的MyViewGroup類,過載dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent(),並新增列印輸出。(繼承LinearLayout不需要過載onLayout()方法擺放子View)

注意:
      不可繼承自Button,否則會預設消費事件。

#MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        daqiBtn.setOnTouchListener(object :View.OnTouchListener{
            override fun onTouch(v: View?, event: MotionEvent?): Boolean {
                when(event?.action){
                    MotionEvent.ACTION_DOWN -> {
                        Log.d("daqia","View OnTouchListener ACTION_DOWN")
                    }
                    MotionEvent.ACTION_MOVE -> {
                        Log.d("daqia","View OnTouchListener ACTION_MOVE")
                    }
                    MotionEvent.ACTION_UP -> {
                        Log.d("daqia","View OnTouchListener ACTION_UP")
                    }
                }
                return false
            }
        })
    }
}
複製程式碼
#activity_main.xml
<com.daqi.daqitouchevent.MyViewGroup
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.daqi.daqitouchevent.MyButton
        android:id="@+id/daqiBtn"
        android:layout_width="120dp"
        android:layout_height="50dp"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="10dp"
        android:background="@color/colorAccent"/>
</com.daqi.daqitouchevent.MyViewGroup>
複製程式碼

Activity:

      當手指觸碰螢幕時,觸控事件最先達到Activity#dispatchTouchEvent()。所以我們先來看看Activity的dispatchTouchEvent()。

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        //空方法
        onUserInteraction();
    }
    //其實就是呼叫PhoneWindow的superDispatchTouchEvent()
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}
複製程式碼

      1、Down事件會執行onUserInteraction()方法。Activity#onUserInteraction()是一個空方法,而且翻閱了FragmentActivity和AppCompatActivity都沒有發現過載onUserInteraction()方法。所以預設當初空方法處理。

      2、Activity中的window是PhoneWindow例項,所以getWindow().superDispatchTouchEvent(ev)其實是呼叫PhoneWindow的superDispatchTouchEvent()方法。

      跳轉到PhoneWindow#superDispatchTouchEvent(),發現呼叫的是mDecor的superDispatchTouchEvent()方法,而mDecor就是DecorView的例項。

#PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}
複製程式碼

      3、跳轉到DecorView#superDispatchTouchEvent(),看到呼叫父類的方法super.dispatchTouchEvent(event)。

      DecorView繼承自FrameLayout,但FrameLayout沒有過載dispatchTouchEvent(event)方法,即super.dispatchTouchEvent(event)呼叫的是ViewGroup#dispatchTouchEvent(event)

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}
複製程式碼

      所以,當DecorView#dispatchTouchEvent()返回false時,會呼叫Activity#onTouchEvent()處理(消費)該事件。否則表示佈局中已有元件處理(消費)了事件。

View:

      View作為最底層的"葉子",並不涉及太多的事件分發且沒有攔截機制,所以先從View開始研究事件分發。

View#dispatchTouchEvent

//只顯示主要程式碼,其他程式碼用//....遮蔽掉
public boolean dispatchTouchEvent(MotionEvent event) {
        //....
        boolean result = false;
        
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            //1、down事件下來先停止巢狀滑動
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            //....
            
            //ListenerInfo中定義了一堆監聽事件類變數,我們設定的view監聽事件,都是儲存到這裡。
            //2、比如OnClickListener、OnLongClickListener等
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            
            //3、如果沒有定義onTouch監聽事件,則result為false,並直接執行onTouchEvent(event)
            //如果定義了onTouch監聽事件,當mOnTouchListener.onTouch返回true時,result則為true,導致下面!result為false,onTouchEvent(event)無法被執行。
            //(mViewFlags & ENABLED_MASK) == ENABLED 判斷元件是否為啟動狀態
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        //....
        //4、
        return result;
    }
複製程式碼

      1、先停止巢狀滑動

      2、ListenerInfo中定義了很多View的監聽事件例項,可從ListenerInfo中獲取OnTouchListener例項,回撥onTouch()方法。呼叫setOnTouchListener其實是將傳進來的View.OnTouchListener賦值到ListenerInfo的OnTouchListener變數中。

      3、當li.mOnTouchListener != null時,會回撥View.OnTouchListener#onTouch(),如果我們在onTouch()方法中返回true,表示消費此事件交由onTouch()處理(消費),不會呼叫View#onTouchEvent()來處理(消費)事件;反之,在onTouch()中返回false,表示不消費此事件,則事件交由onTouchEvent()處理(消費)。

      當沒有設定View.OnTouchListener監聽時,result預設為false,事件預設交由View#onTouchEvent()處理(消費)。

      4、當View#onTouchEvent()或者OnTouchListener#onTouch()返回true時(兩者只呼叫一個),View#dispatchTouchEvent()整體返回true。反之,View#dispatchTouchEvent()整體返回false。

View#onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
    //1、獲取view是否可以點選(點選,長按等)
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE 
                               || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    //2、判斷當前元件是否處於非啟動(禁用)狀態,即enabled是否為false
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        // A disabled view that is clickable still consumes the touch
        // events, it just doesn‘t respond to them.
        // 如果為禁用狀態且元件設定了點選事件,則會消費該事件,但不作響應。
        return clickable;
    }
    //android有效觸控的範圍是48dp,當元件太小,而又因為佈局問題不能設定padding時。
    //3、可對父元件設定TouchDelegate,以增大點選面積。
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    //4、當元件可點選時,響應點選事件。
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
           case MotionEvent.ACTION_UP:
                //..
                break;
           case MotionEvent.ACTION_DOWN:
                //..
                break;
           case MotionEvent.ACTION_CANCEL:
                //..
                break;
           case MotionEvent.ACTION_MOVE:
                //..
                break;
        }
        //只要元件是可點選的,必定消費事件
        return true;
    }
    //5、元件不可點選的,則不會消費事件。
    return false
}
複製程式碼

      1、依據元件的設定點選監聽和長按監聽的情況,獲取表示元件是否可點選的布林值clickable。

      2、當元件處於禁用(DISABLED)狀態,返回clickable。如果元件可點選或長按,則會返回true消費事件,但不做處理。(該View處於非啟動(禁用)狀態,enabled為false)

      3、當元件設定了TouchDelegate時,則會直接消費事件。(子View自身體積太小不利於點選,但又限制於佈局無法增大自身大小,可對父元件設定TouchDelegate,以增大點選面積。TouchDelegate不在討論範圍)

      4、如果View設定了點選監聽或長按監聽,則會依據事件進行不同的處理,但最後還是會return true消費事件。(switch語句結束後返回true)

      5、否則View沒有設定點選監聽或長按監聽,則return false不消費事件。

      注意:
            1、onTouchLinstener在View#dispatchTouchEvent()中回撥,如果過載dispatchTouchEvent,而不呼叫父類的dispatchToucEvent,則不會有onTouch()回撥。
            2、onLongClickListener和onClickListener在View#onTouchEvent()中回撥,同理,不呼叫父類的onTouchEvent(),則不會有onClick()和onLongClick()回撥。

ViewGroup:

      ViewGroup中涉及到更多事件分發和事件攔截機制,會比較難懂。先看一遍原始碼,再依據demo中的現象在原始碼中尋找答案,從而更好的學習和加深對原始碼的認識。

ViewGroup#dispatchTouchEvent()

#ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    //....

    // Handle an initial down.
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        //1、在開始新的觸控手勢時丟棄所有先前的狀態,重置Touch狀態標識
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }
    
    //標記是否攔截事件
    final boolean intercepted;
    //2、down事件或已經有處理Touch事件的目標了
    //當子控制元件沒有消費事件或者被攔截mFirstTouchTarget為空
    if (actionMasked == MotionEvent.ACTION_DOWN
        || mFirstTouchTarget != null) {
        //3、標記是否禁止攔截
        //因為在其他地方可能呼叫了requestDisallowInterceptTouchEvent(boolean disallowIntercept)
        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 {
        //4、
        intercepted = true;
    }
    
    //...
}
複製程式碼

      1、Down事件是整套MotionEvent事件的開始,先丟棄所有先前的狀態,重置Touch狀態標識。

      2、actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null    事件為down事件時,等式成立,進入if語句。

      3、獲取一個布林值disallowIntercept(字面意思:不允許攔截),該布林值決定著調不呼叫ViewGroup#onInterceptTouchEvent(),而ViewGroup#onInterceptTouchEvent()正是用來攔截事件的。

      disallowIntercept賦值true還是false,由當前ViewGroup中的子View決定。
      當子View呼叫requestDisallowInterceptTouchEvent(boolean)方法請求父類不要攔截時,該disallowIntercept為true,標記是否攔截的布林值intercepted寫死為false,表示不攔截。
      當沒有子View呼叫requestDisallowInterceptTouchEvent(boolean)方法時,該disallowIntercept為false,回撥ViewGroup#onInterceptTouchEvent()。ViewGroup可以在onInterceptTouchEvent()中對事件進行攔截。

      4、當不是Down事件 和 mFirstTouchTarget為null時,intercepted = true。先記住,後面用到。(重點mFirstTouchTarget為null)

#ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
        
    //...
    
    //每次分發事件前,將newTouchTarget置空
    TouchTarget newTouchTarget = null;
    boolean alreadyDispatchedToNewTouchTarget = false;
    //5、不是取消狀態且沒攔截的情況下
    if (!canceled && !intercepted) {
        //....
        
        //6、判斷(x,y)在該View的可視範圍內。true則繼續往下執行。
        if (!canViewReceivePointerEvents(child)
            || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }
        //....
        
        final View[] children = mChildren;
        
        //7、遍歷子view
        for (int i = childrenCount - 1; i >= 0; i--) {
            //....
            
            //通過dispatchTransformedTouchEvent()將事件分發下去
            //dispatchTransformedTouchEvent返回true時,表示子view消費了事件
            //當第三引數傳child(View例項)時,呼叫該ViewGroup的子View的dispatchTouchEvent()
            //當第三引數傳null時,呼叫父類(即View)的dispatchTouchEvent()
            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();
                //8、對mFirstTouchTarget進行賦值
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                //標記已派發事件給新的觸控物件
                alreadyDispatchedToNewTouchTarget = true;
                break;
            }
            
            //...
        }
        
        //9、如果遍歷完子View,newTouchTarget沒有被賦值,即本次沒有消費該事件的子View,但之前有子View的消費事件記錄(mFirstTouchTarget 不為空)
        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;
            //從連結串列表頭觸控物件開始遍歷,尋找下一個觸控物件,指定找到最後的(第一個)觸控物件為止,將其賦值給newTouchTarget變數
            while (newTouchTarget.next != null) {
                newTouchTarget = newTouchTarget.next;
            }
            newTouchTarget.pointerIdBits |= idBitsToAssign;
        }
    }
    
    //....
}
複製程式碼

      5、上面對定義的是否攔截的布林值intercepted,在判斷語句 !canceled && !intercepted 用上了。if(!canceled && !intercepted)內會對事件進行事件分發。intercepted為true時,表明事件被該ViewGroup攔截了,不會將事件分發給該ViewGroup的子View。

      6、ViewGroup#isTransformedTouchPointInView()用於判斷該事件的座標(x,y)是否在子View可視範圍內。符合條件才繼續往下執行。

      7、遍歷自己的子View,通過dispatchTransformedTouchEvent()將事件分發出去。dispatchTransformedTouchEvent返回true時,表示子view消費了事件。(第三個引數傳遞了當前遍歷的子View)

      子View的遍歷是倒序來得,其實是先選擇點選範圍內的頂層View,再一層一層將事件分發下去。

#ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        
    //....

    // Perform any necessary transformations and dispatch.
    //child為空 呼叫super.dispatchTouchEvent(),即呼叫View#dispatchTouchEvent(),而View#dispatchTouchEvent()中會呼叫View#onTouchEvent()
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }
        
        //呼叫子View的dispatchTouchEvent(),將事件分發下去。
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    //....
}
複製程式碼

      當第三引數傳 child(View例項) 時,呼叫該子View的dispatchTouchEvent()。

      當第三引數傳 null 時,呼叫父類(即View)的dispatchTouchEvent()。而View#dispatchTouchEvent中,會呼叫View#onTouchEvent()。即呼叫自身的onTouchEvent(),因為ViewGroup沒有過載onTouchEvent()。

      8、dispatchTransformedTouchEvent()的返回值作為if語句的判斷條件。當if語句為true時,說明子View消費事件。然後呼叫addTouchTarget(),將子View封裝成TouchTarget,並將其賦值到mFirstTouchTarget。break退出迴圈,不再分發Move事件。(也就是說只把事件分發給一個消費該事件的子View)

#ViewGroup.java
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    //對mFirstTouchTarget進行賦值
    mFirstTouchTarget = target;
    return target;
}
複製程式碼

      TouchTarget是一個連結串列解構,可以看到將target最終會賦值到mFirstTouchTarget,而上一步是將target的下一個索引指向mFirstTouchTarget,也就是說target的下一個索引指向的是上一個消費事件的target,而本次消費事件的target作為連結串列的第一個元素。

      然後將alreadyDispatchedToNewTouchTarget設為true,標記已分發事件給新的TouchTarget

      9、遍歷完子View,newTouchTarget仍然沒有被賦值,即本次子View沒有消費事件。但mFirstTouchTarget 不為空,表示之前有子View的消費事件的記錄。則遍歷連結串列,找出第一個消費事件的target,將其賦值給newTouchTarget物件。

#ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

    //....
        
    if (mFirstTouchTarget == null) {
        // No touch targets so treat this as an ordinary view.(沒有觸控目標,所以將其視為普通檢視。)
        //10、即ViewGroup呼叫父類(View)的dispatchTouchEvent()
        // 如果View的dispatchTouchEvent中返回true,消費了此事件,則handled會為true,ViewGroup#dispatchTouchEvent()返回true。表示告訴該ViewGroup的父檢視,該檢視消費了事件。
        // 否則,無子View消費事件,自身也不消費事件的情況下,則該ViewGroup#dispatchTouchEvent()返回false,表示不消費事件
        // 因為模版設計模式,通過View的dispatchTouchEvent進行對ViewGroup的onTouch() 和 onTouchEvent()進行呼叫。 
        handled = dispatchTransformedTouchEvent(ev, canceled, null,
                                                TouchTarget.ALL_POINTER_IDS);
    } else {
    
        TouchTarget predecessor = null;
        TouchTarget target = mFirstTouchTarget;
        //11、遍歷TouchTargt樹,分發事件
        while (target != null) {
            final TouchTarget next = target.next;
            //如果新的觸控目標依據派發過事件 且當前觸控目標等於新的觸控目標。
            //alreadyDispatchedToNewTouchTarget在上面子View消費會進行賦值為true
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                //既然子View依據消費了事件,則ViewGroup#dispatchEvent()必定返回true
                handled = true;
            } else {
                //標記是否讓child取消處理事件
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
                //派發事件
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                                                  target.child, target.pointerIdBits)) {
                    //ViewGroup#dispatchEvent()返回true
                    handled = true;
                }
                //12、cancelChild為true時,將當前target從TouchTarget連結串列中移除
                if (cancelChild) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            predecessor = target;
            target = next;
        }
    }
}
複製程式碼

      10、如果mFirstTouchTaget沒有被賦值,則表示本次包括之前的,沒有子View消費事件。呼叫ViewGroup#dispatchTransformedTouchEvent(),第三個引數傳遞null,呼叫自身的onTouch()和onTouchEvent()。自己嘗試消費事件,如果自身不消費事件,則ViewGroup#dispatchTouchEvent()返回false。

      11、如果mFirstTouchTaget不為空,則遍歷TouchTaget連結串列。

      alreadyDispatchedToNewTouchTarget為true只有在本次事件被子View消費的情況下不為空。即呼叫addTouchTaget()方法後設定為true。

      但newTouchTarget存在兩種情況不為空:
            一種是本次事件被子View消費了,呼叫了addTouchTaget();
            一種是本次事件沒有被消費,但之前有子View消費事件(可見ViewGroup#dispatchTouchTaget分析第9條)

      當本次事件被子View消費時,alreadyDispatchedToNewTouchTarget為true,且newTouchTarget等於mFirstTouchTarget。
alreadyDispatchedToNewTouchTarget && target == newTouchTarget條件成立,因為target初始賦值是mFirstTouchTarget。則直接將ViewGroup#dispatchTouchEvent()返回值設定為true。

      alreadyDispatchedToNewTouchTarget && target == newTouchTarget條件不成立時,獲取一個標記是否讓child取消處理事件的布林值cancelChild,同時以第二個引數傳入ViewGroup#dispatchTransformedTouchEvent()。

      cancelChild的值由resetCancelNextUpFlag(target.child) || intercepted 決定。resetCancelNextUpFlag()看不懂,但當ViewGroup攔截事件時,intercepted = true,則cancelChild = true。

      當布林值cancel為true時,也就是cancelChild為true時,會將事件修改為MotionEvent.ACTION_CANCEL,在分發給子View。然後復原MotionEvent。並將target從TouchTarget連結串列中移除。

      當布林值cancel為false時,也就是cancelChild為false時,會將事件原封不動的分發給子View。走正常分發的流程。

#ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
    final boolean handled;
    
    //標記當前屬於的MotionEvent型別
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        //將事件改為MotionEvent.ACTION_CANCEL,並分發給子View
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        //復原當前的MotionEvent型別
        event.setAction(oldAction);
        return handled;
    }
    
    //....
}
複製程式碼

總得來說就是:
      遍歷TouchTarget連結串列,當本次事件已經被子View消費時,則將返回值設為true,表示本ViewGroup嘗試消費該事件。
      判斷當前是否傳送MotionEvent.ACTION_CANCEL事件,如果intercepted為true導致cancelChild為true時,將事件改成ACTION_CANCEL事件,傳送給連結串列每一個元素,同時將其從TouchTarget連結串列中刪除。否則cancelChild為false時,將事件原封不動的傳送給連結串列中的元素。
      當連結串列中的元素消費事件時,則將ViewGroup#dispatchTouchEvent()的返回值改為true。

示例1、

      正常的過載ViewGroup和View關於事件分發的方法,不做任何改動,但對事件進行列印輸出。

Android事件分發原始碼歸納
Down和Up事件傳遞的順序是:
            ViewGroup#dispatchTouchEvent() -> ViewGroup#onInterceptTouchEvent() -> View#dispatchTouchEvent -> View#onTouchEvent() -> ViewGroup#onTouchEvent()

現象:
      手指按下去後,無論怎麼移動,然後抬起都是隻有這5個列印。從列印看只觸發了Down事件,沒有Move事件和Up事件。並傳遞給自己的子View。

解析:
      dowm事件時,actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null條件是成立的。所以intercepted依據ViewGroup#onInterceptTouchEvent()返回值來賦值,ViewGroup#onInterceptTouchEvent()預設返回false。

      條件!canceled && !intercepted成立時,遍歷子View,通過dispatchTransformedTouchEvent(child)對事件進行分發。即呼叫子View的dispatchTouchEvent()方法和View#onTouchEvent()方法。

      當事件沒有子View消費時,if(dispatchTransformedTouchEvent(child))條件不成立,導致mFirstTouchTarget依舊為空。

      繼續往下執行,到if (mFirstTouchTarget == null)時,條件成立。則呼叫dispatchTransformedTouchEvent(null)。傳入的子View為null,呼叫父類也就是View的dispatchTouchEvent(),對ViewGroup的onTouchEvent()進行呼叫(ViewGroup沒過載onTouchEvent())。

      move事件時,由於已經不是down事件了,且mFirstTouchTarget依舊為空。則intercepted被設定為true。if(!canceled && !intercepted)條件不成立,無法進入該if語句中呼叫dispatchTransformedTouchEvent(child)對move事件進行分發。子View也就不會再接受到後續的事件。

疑問:
      不分發給View事件,但ViewGroup#dispatchTouchEvent()依據被呼叫,為什麼沒列印。
      因為該ViewGroup不是根佈局,相對於DecorView,自定義的ViewGroup屬於子View。是DecorView不分發事件下來,並且我們也沒有消費事件。

示例2、

      正常的過載ViewGroup和View關於事件分發的方法,在其中一個View#onTouchEvent()的down事件時返回true,並對事件進行列印輸出。

Android事件分發原始碼歸納

現象:
      donw事件中,事件都分發到ViewGroup的全部子View中。當子View2返回true消費事件後,後續事件都只返回給該子View元件。

解析:
      按照正常的事件分發流程,mFirstTouchTarget指向的child是子View2。當move事件下來時,intercepted由onInterceptTouchEvent()賦值,預設為false,條件if(!canceled && !intercepted)成立。但後續判斷語句不成立。actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE(通過debug發現的,但看不懂)。不符合再次將事件全部分發給子View的情況。

      離開if(!canceled && !intercepted)語句。直接遍歷TouchTarget連結串列,將事件分發給連結串列中的每一個元素。子View2在down事件時消費了事件,固存在與連結串列中,所以後續的事件都會分發給子View2。

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (!canceled && !intercepted) {
        //不滿足條件時表示不需要再重新尋找響應事件的View(看不懂)
        if (actionMasked == MotionEvent.ACTION_DOWN
            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        }
    }
}
複製程式碼
示例3、

      正常的過載ViewGroup和View關於事件分發的方法,在其中一個View#onTouchEvent()的down事件時返回true,ViewGroup在第一個move事件時攔截事件,並對事件進行列印輸出。

Android事件分發原始碼歸納

現象:
      子View在onTouchEvent中return true,後續事件都會返回到自定義的ViewGroup中,但在第一個move事件時,ViewGroup#onInterceptTouchEvent()返回true將事件攔截了,交由ViewGroup處理事件,後續的事件都交由ViewGroup處理。同時子View接受到CANCEL事件。

解析:
      ViewGroup#onInterceptTouchEvent()返回true時,intercepted也為true。if(!canceled && !intercepted)條件不成立,直接遍歷TouchTarget連結串列。

      但此時intercepted也為true,cancelChild = resetCancelNextUpFlag(target.child) || intercepted,也就是說cancelChild為true,當cancelChild第二個引數傳入dispatchTransformedTouchEvent()時,會給子View傳送CANCEL事件,並將該子View從連結串列中刪除。直至到TouchTarget清空。

      當TouchTarget被清空後,mFirstTouchTarget為null,後續只呼叫ViewGroup#onTouchEvent()嘗試消費事件。

總結歸納圖:

Android事件分發原始碼歸納

遺留問題:
      為什麼子View消費了事件,mFirstTouchTarget!= null,在move事件時if(actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE)不成立?

      按debug看到的結果,直接跳到遍歷TouchTarget連結串列來將後續的事件分發。不再通過分發事件尋找消費事件的子View,直接根據TouchTarget連結串列將事件分發下去可以理解。但連結串列什麼情況下才會存在更多的元素,並且這段程式碼是在什麼情況下才被使用到?希望大佬們指點下。

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;
}
複製程式碼

demo地址:連結: pan.baidu.com/s/1dRNiQXAt… 提取碼: jepe

相關文章