初探Android事件分發(fā)機(jī)制源碼下之ViewGroup,View

在上一篇中我們一起分析了事件從手機(jī)硬件傳遞到DecroView的過程,接著本文我們一起來(lái)分析一下ViewGroup和View是怎么傳遞,處理觸摸事件的。
View的事件分發(fā)機(jī)制重要性不言而喻,面試,平時(shí)做都是經(jīng)常接觸。平時(shí)都是照著代碼寫,但是其實(shí)并不知道很多原理。比如為什么onTouch比OnClick先執(zhí)行?為什么onTouch返回true后OnClick就不再執(zhí)行?onTouch和onTouchEvent有什么區(qū)別?這些問題可以說是一直困擾著很多人。今天,我們就通過寫demo,邊看效果邊跟源碼來(lái)一個(gè)一個(gè)問題的分析。本文源碼均來(lái)自API24。文末會(huì)附加分析觸摸事件從硬件開始到最后View處理的整個(gè)流程。

秉承著前人栽樹后人乘涼的想法,首先給出自己所讀總結(jié)的精華和大神的總結(jié)圖:
在Android中,View的事件分發(fā)主要有3個(gè)方法:dispatchTouchEvent()、 onInterceptTouchEvent() 以及 onTouchEvent()。當(dāng)我們點(diǎn)擊某個(gè)子View控件時(shí),首先調(diào)用的是這個(gè)view的父ViewGroup的dispatchTouchEvent(),dispatchTouchEvent()內(nèi)部調(diào)用onInterceptTouchEvent()來(lái)決定是否攔截該事件,如果攔截,則調(diào)用ViewGroup的onTouchEvent()進(jìn)行處理,并且屏蔽掉該事件,不再往下傳遞,所以有可能會(huì)產(chǎn)生你點(diǎn)擊某個(gè)按鈕,按鈕的處理動(dòng)作沒有發(fā)生,父控件的處理事件觸發(fā)了的情況。如果父ViewGroup不攔截事件,否則調(diào)用子View的dispatchTouchEvent(),可以參考如下圖:


出自http://blog.csdn.net/huachao1001
-------------------------------一條酷炫的分割線-----------------------------------------
總結(jié)說完了,說句實(shí)話也就只有那些以前研究過回頭來(lái)看的大神們想起來(lái)了是個(gè)什么回事,沒了解過的萌新們看了半天還是一句哈玩意兒?那么接下來(lái)我們就寫一個(gè)demo,邊看效果邊跟源代碼來(lái)分析一下。
------------------------------又是一條酷炫的分割線------------------------------------

ViewGroup

首先我們來(lái)定義一個(gè)布局,只有一個(gè)Button按鈕,外圍是一個(gè)我們自定義的Layout,我們?nèi)∶麨門estLayout。暫時(shí)只繼承于LineLayout,不加任何代碼。

xml布局

TestLayout代碼:

public class TestLayout extends LinearLayout {

    public TestLayout(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public TestLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
}

Activity代碼:

public class Main6Activity extends AppCompatActivity {

    Button mButton;
    TestLayout mTestLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main6);
        mButton = (Button) findViewById(R.id.test_bt);
        mTestLayout = (TestLayout) findViewById(R.id.test_layout);

        //TestLayout注冊(cè)onTouchListener
        mTestLayout.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d("touchEvent", "mTestLayout的觸摸事件執(zhí)行了!執(zhí)行Action為:" + event.getAction());
                return false;
            }
        });

        //mButton注冊(cè)O(shè)nClickListener        
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("touchEvent", "button的點(diǎn)擊事件執(zhí)行了!");
            }
        });
        //mButton注冊(cè)onTouchListener        
        mButton.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d("touchEvent", "button的觸摸事件執(zhí)行了!執(zhí)行Action為:" + event.getAction());
                return false;
            }
        });
    }
}

點(diǎn)擊按鈕運(yùn)行一下看一下日志:


點(diǎn)擊按鈕日志

首先我們看到是button的onTouch方法首先執(zhí)行(有手指按下和抬起兩個(gè)事件),隨后執(zhí)行button的onClick方法。而父布局testLayout的touch事件沒有被觸發(fā)。
emmm.......
哈玩意兒?不是說好的事件先給viewGroup么?咋這都沒反應(yīng)呢??那給父布局注冊(cè)touch事件干嘛啊。別忙,既然這樣,我們就去看看ViewGroup的dispatchTouchEvent()方法的源代碼(讀者如果親自跟進(jìn)去就會(huì)發(fā)現(xiàn)這個(gè)方法的源碼很長(zhǎng)很復(fù)雜,這里為了分析,分解為各個(gè)部分講解):

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
                ...//其他代碼

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
代碼1處---------------------------
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

我們將關(guān)鍵代碼貼出來(lái),我們來(lái)開始分析一下,在上面代碼1處判斷如果是按下事件,會(huì)調(diào)用cancelAndClearTouchTargets(ev)方法,這個(gè)方法里面會(huì)把mFirstTouchTarget置空。這個(gè)變量非常重要,在后面會(huì)繼續(xù)講解。接著往下看:

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
代碼2處---------------------------
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                intercepted = true;
            }
          ...//其他代碼

接下來(lái)繼續(xù)分析,當(dāng)如果是按下事件或者 mFirstTouchTarget 不為空的時(shí)候會(huì)調(diào)用代碼2處的onInterceptTouchEvent()方法來(lái)判斷是否攔截觸摸事件。我們跟進(jìn)去看看:

  public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

在這里我們可以看做這個(gè)方法正常情況下是默認(rèn)返回false。因此這里也說明了ViewGroup是默認(rèn)不會(huì)攔截觸摸事件的。所以在之前的demo效果中自然會(huì)出現(xiàn)父布局沒有做任何處理直接將觸摸事件往下傳遞給Button的情況。如果我們重寫父布局的onInterceptTouchEvent()方法返回true。那么表明ViewGroup父布局?jǐn)r截觸摸事件,事件就不會(huì)再繼續(xù)往下傳遞給子View。我們來(lái)繼續(xù)改demo看看效果:
在TestLayout中重寫onInterceptTouchEvent()方法返回false:

public class TestLayout extends LinearLayout {

   ...//其他代碼

  //重寫該方法,返回true
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return true;
    }
}

我們?cè)冱c(diǎn)擊按鈕看看效果:

點(diǎn)擊按鈕日志

如上圖,我們可以看到,當(dāng)TestLayout攔截了觸摸事件時(shí),Button的觸摸和點(diǎn)擊事件都沒有被觸發(fā)。因此我們可以得出結(jié)論,事件分發(fā)是從父布局向子控件傳遞的。
回到ViewGroup的代碼我們接著往下看:

代碼3處---------------------------
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {
                        ...//其他代碼

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                      ...//其他代碼
代碼4處---------------------------
                    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 = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                           ...//其他代碼
                        }

在代碼3處,會(huì)定義一個(gè) newTouchTarget變量。接著會(huì)判斷事件是否取消并且是否打斷,如果都不。那么執(zhí)行if條件里面的代碼。
在代碼4處,會(huì)定義一個(gè)int類型的childrenCount來(lái)表示該ViewGroup有多少個(gè)子View。接下來(lái)再判斷childrenCount個(gè)數(shù)是否為0(即ViewGroup是否有子View)。如果有,那么就執(zhí)行代碼去尋找事件應(yīng)該傳遞到哪個(gè)集體的子View。

繼續(xù)往下看源碼:

代碼5處---------------------------
                            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();
代碼6處---------------------------
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

我們可以看到在代碼5處會(huì)調(diào)用dispatchTransformedTouchEvent()方法,這個(gè)是什么方法呢?我們?cè)俑M(jìn)去看看:

   private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
...//其他代碼
        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;
        }
....//其他代碼

我們?cè)谶@里就可以看到,當(dāng)child為空時(shí),表示沒有找到觸摸事件在某個(gè)具體子View的范圍內(nèi)。此時(shí)調(diào)用super.dispatchTouchEvent()方法,而ViewGroup的父類是View對(duì)象。表明此時(shí)事件已經(jīng)交給ViewGroup自己處理。而如果child不為空,則調(diào)用子View的dispatchTouchEvent()方法來(lái)處理。當(dāng)觸摸事件處理完畢,就會(huì)返回一個(gè)布爾值handled,該值表示子View是否消耗了事件。怎樣判斷一個(gè)子View是否消耗了事件呢?如果說子View的onTouchEvent()返回true,那么就是表明消耗了事件。

接下來(lái)在代碼6處,會(huì)調(diào)用addTouchTarget()方法來(lái)賦值給newTouchTarget變量。我們?cè)俑M(jìn)去看一看:

  private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }

我們可以看到,首先是找到具體的child的TouchTarget對(duì)象,然后mFirstTouchTarget指向這個(gè)child.然后返回target。

繼續(xù)回到ViewGroup的dispatchTouchEvent()方法往下看:

代碼7處---------------------------
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
代碼8處---------------------------
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                    }
                             ...//其他代碼
                }
            }

          ...//其他代碼
        return handled;
    }

在代碼7處會(huì)判斷mFirstTouchTarget是否為空,很明顯,在剛才的分析里可以看到,如果我們的觸摸事件沒有被攔截,就會(huì)在addTouchTarget()方法中為mFirstTouchTarget賦值,因此這段代碼就不會(huì)被執(zhí)行。只有在ViewGroup類型不攔截或者點(diǎn)擊了ViewGroup的空白處(沒有點(diǎn)擊任何子控件,事件沒有發(fā)生在任何子View的范圍內(nèi))。這兩種情況下mFirstTouchTarget不會(huì)被改變依然為null才會(huì)執(zhí)行if條件里面的代碼。換句話說反過來(lái)也就是如果mFirstTouchTarget == null條件成立執(zhí)行的這段代碼會(huì)處理ViewGroup攔截了事件或者所有子View均不消耗事件這兩種情況。那么在這里調(diào)用dispatchTransformedTouchEvent()方法交由ViewGroup處理事件。如果mFirstTouchTarget不為空,那么表明有child已經(jīng)處理了ACTION_DOWN觸摸事件,那么執(zhí)行else代碼塊的代碼。

到代碼8處,在上面的分析中可以看到,如果子View消耗了ACTION_DOWN觸摸事件,那么alreadyDispatchedToNewTouchTarget會(huì)修改為true并且target == newTouchTarget也是成立的。因此表示這是個(gè)ACTION_DOWN事件,如果有一個(gè)不成立,表明是ACTION_DOWN之外的其他事件,那么在這里繼續(xù)把其他事件分發(fā)給子View處理。
至此我們就已經(jīng)把ViewGroup的dispatchTouchEvent()方法分析完畢了??偨Y(jié)一下:

  • 當(dāng)事件傳遞到我們的ViewGroup時(shí),會(huì)調(diào)用dispatchTouchEvent()方法,在里面首先會(huì)調(diào)用onInterceptTouchEvent()方法(默認(rèn)為false不攔截)來(lái)決定ViewGroup是否攔截該事件。結(jié)果為兩種:1. 如果方法返回true,即要攔截,那么事件傳遞到此為止,不再傳遞給子View。2.如果放回false,不攔截事件,那么將事件傳遞給子View,由子View去進(jìn)行處理。

View

上面分析完了ViewGroup的dispatchTouchEvent()源碼,接下來(lái)我們來(lái)分析分析View的dispatchTouchEvent()方法。

我們先來(lái)把demo改一下代碼,首先把之前的Button去掉,重寫一個(gè)自定義View繼承自Button重寫onTouchEvent()方法(記得把我們之前的TestLayout的攔截事件方法改為返回false哦,否則事件都被攔截了):

public class TestButton extends Button {

    public TestButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public void init() {
        setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("touchEvent", "button的點(diǎn)擊事件執(zhí)行了!");
            }
        });

        setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d("touchEvent", "button的onTouch方法執(zhí)行了!執(zhí)行Action為:" + event.getAction());
                return false;
            }
        });
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("touchEvent", "button的onTouchEvent方法執(zhí)行了!執(zhí)行Action為:" + event.getAction());
        return super.onTouchEvent(event);
    }
}

布局如下很簡(jiǎn)單:

<com.demo.dltlayoutlayoutparams.TestLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/test_layout"
    tools:context="com.demo.dltlayoutlayoutparams.Main6Activity">

    <com.demo.dltlayoutlayoutparams.TestButton
        android:id="@+id/test_bt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</com.demo.dltlayoutlayoutparams.TestLayout>

Activity的代碼如下:

public class Main6Activity extends AppCompatActivity {

    TestLayout mTestLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main6);
        mTestLayout = (TestLayout) findViewById(R.id.test_layout);

        mTestLayout.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d("touchEvent", "mTestLayout的觸摸事件執(zhí)行了!執(zhí)行Action為:" + event.getAction());
                return false;
            }
        });
    }
}

我們運(yùn)行demo點(diǎn)擊一下按鈕,看一下日志結(jié)果:


點(diǎn)擊按鈕日志

如上圖結(jié)果,我們可以看到顯然是onTouch方法先執(zhí)行,然后OntouchEvent()再執(zhí)行,然后又執(zhí)行了click()方法,因?yàn)槲覀兪种赴聪潞笥痔?。所以有兩個(gè)事件先后發(fā)生處理。

那么為什么onTouch()方法會(huì)比onTouchEvent()方法先執(zhí)行呢?onTouch()方法返回類型為布爾值,有什么用呢?(不可能因?yàn)閛nTouch字母數(shù)比onTouchEvent()字母少所以先執(zhí)行把?)帶著這些疑惑,我們來(lái)一起分析分析View的dispatchTouchEvent()方法。

在這里先貼出源碼:

   public boolean dispatchTouchEvent(MotionEvent event) {
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }

        boolean result = false;

        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //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;
    }

emmm...首先可以看到源碼其實(shí)包含了很多注釋了很多其他代碼來(lái)做一些判斷和處理工作,在這里我抽出核心過程代碼來(lái)看看:

  //ListenerInfo是一個(gè)包裝類,里面封裝了View的各種監(jiān)聽器Listener。
  //我們?cè)O(shè)置onTouchListener也是設(shè)置給ListenerInfo。
   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;
            }

可以看出,首先如果view的OnTouchListener不為空并且我們的View是可以編輯的(處于Enable狀態(tài))并且重寫的onTouch()方法返回true,那么result設(shè)置為true。那么接下來(lái)就不會(huì)再執(zhí)行onTouchEvent()方法。因此onTouch()方法比onTouchEvent()方法優(yōu)先執(zhí)行的原因在這里。我們來(lái)設(shè)置onTouch()方法返回true來(lái)看看效果:

setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d("touchEvent", "button的onTouch方法執(zhí)行了!執(zhí)行Action為:" + event.getAction());
                //修改前
                return false;
                //修改后
                return true;
            }
        });
    }

運(yùn)行點(diǎn)擊一下按鈕:

按鈕點(diǎn)擊日志

可以看到我們onTouch方法返回true,那么就不會(huì)再執(zhí)行onTouchEvent()方法。

誒?突然發(fā)現(xiàn),怎么onClick()方法也沒了呢??我沒有改點(diǎn)擊事件???那么為什么我們的onTouch()方法返回true后,onClick()方法沒有執(zhí)行呢?我們跟進(jìn)去onTouchEvent()方法一探究竟。

先放上onTouchEvent()方法的源代碼:

 public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                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)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        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 ((mPrivateFlags & PFLAG_PRESSED) != 0 || 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 && !mIgnoreNextUpEvent) {
                            // 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();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    // Be lenient about moving outside of buttons
                    if (!pointInView(x, y, mTouchSlop)) {
                        // Outside button
                        removeTapCallback();
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            setPressed(false);
                        }
                    }
                    break;
            }

            return true;
        }

        return false;

    }

emmm.....好像有點(diǎn)長(zhǎng)??看了真是讓人頭大。那么長(zhǎng)的代碼。大概的我們也不可能一一去分析,那么我們就過濾掉一些無(wú)關(guān)代碼,來(lái)提取一下精華:

 public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
     
        .......//不重要代碼

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                                .......//不重要代碼

                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }
        .......//不重要代碼
        return false;

    }

經(jīng)過這么一提取,關(guān)鍵代碼就出現(xiàn)了。我們可以看到在上面的case MotionEvent.ACTION_UP情況下,我們獲取執(zhí)行performClick()方法。我們繼續(xù)跟進(jìn)去查看代碼:

 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;
    }

至此就很清晰了,就是在這里執(zhí)行了我們?cè)诖a中設(shè)置的onClickListener的onClick()方法。因此,如果我們的onTouch()方法返回true,那么onTouchEvent()方法也不會(huì)執(zhí)行,那么onTouchEvent()方法里的onClick()方法更不能得到執(zhí)行!

那么問題又又又來(lái)了....onTouch()返回值影響了onTouchEvent()是否執(zhí)行,那么onTouchEvent()的返回值又是拿來(lái)干嘛的?好像這個(gè)返回值又不影響點(diǎn)擊事件的執(zhí)行?我們來(lái)改一改試成false和true試試:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d("touchEvent", "button的onTouchEvent方法執(zhí)行了!執(zhí)行Action為:" + event.getAction());
        //修改前
        return super.onTouchEvent(event);
        //第一次修改后
        return false;
        //第二次修改后
        return true;
    }

我們來(lái)看一下效果:


返回false效果

返回true效果

我們看到,首先修改為true或者false后,點(diǎn)擊事件都消失了,這個(gè)其實(shí)很好理解。因?yàn)閛nClick()方法之前分析過是在View的onTouchEvent()方法調(diào)用的。而我們現(xiàn)在重寫onTouchEvent()方法卻沒有調(diào)用super.onTouchEvent()方法,因此點(diǎn)擊事件肯定不會(huì)被調(diào)用。

其次,我們發(fā)現(xiàn),在返回false的時(shí)候發(fā)現(xiàn)onTouch和onTouchEvent()都只是處理了按下的動(dòng)作,并沒有繼續(xù)處理后面的抬起動(dòng)作。而且TestLayout的onTouch()抬起動(dòng)作執(zhí)行了,但是也沒處理抬起動(dòng)作。

其實(shí)這是因?yàn)閛nTouchEvent()當(dāng)我們返回false,表明我們子View不處理事件,于是就會(huì)將事件全部回傳給父布局。當(dāng)down事件不消耗后,后續(xù)的move,up等等事件也不再執(zhí)行onTouch()方法。因此在這里直接將事件交給父布局,而在這里我們的父布局TestLayout在Activity中設(shè)置的onTouch()方法也返回false。因此也會(huì)將事件繼續(xù)上傳,這里有興趣的同志可以把TestLayout的onTouch()方法返回true來(lái)看一下效果(文章篇幅實(shí)在太長(zhǎng)了。。。)。
而當(dāng)我們返回true就表示我們要處理事件,因此事件就不會(huì)回傳到父布局了。

------------------------------一條假裝很華麗的分割線---------------------------------
額外分析: 具體原因是在文初的ViewGroup源碼的代碼5處調(diào)用onTouch()方法,而這個(gè)代碼5處是包含在代碼4處上面的if條件里面的,條件是事件為手指按下事件或者其他事件才會(huì)執(zhí)行。然后onTouch方法中調(diào)用的onTouchEvent()方法,如果onTouchEvent()返回false,那么onTouch()方法也返回false(可看上面View的dispatchTouchEvent()源碼),那么代碼5處的條件判斷不成立,那么mFirstTouchTarget變量不會(huì)被修改仍然為null,因此在走到ViewGroup源代碼7處交給父布局處理了。而其他比如手指移動(dòng),手指抬起事件不會(huì)再走代碼4,5處。因此直接跳到代碼7處,mFirstTouchTarget仍然為null。所以說當(dāng)子View不處理down事件,所有事件就不會(huì)再交給子View來(lái)處理了。換言之所以只有onTouchEvent()中第一個(gè)down事件處理返回true,才會(huì)使得mFirstTouchTarget賦值不為null。那么在代碼7處才會(huì)把后續(xù)事件交給子View處理。
------------------------------一條假裝很華麗的結(jié)束線---------------------------------


呼...至此總算就分析完了ViewGroup和View的事件分發(fā)的傳遞和回傳過程,也算是完結(jié)撒花啦!
最后總結(jié)一下View:

  • View的執(zhí)行順序?yàn)閛nTouch()->onTouchEvent()->onClick()。如果onTouch()返回true表示消耗了事件,就不再傳給onTouchEvent()執(zhí)行。(好比小明和小剛一起去買冰淇淋,小明先吃一口,他可以選擇把冰淇淋給小剛吃還是把冰淇淋留下來(lái))

  • 如果View只消耗down事件,而不消耗其他事件,那么其他事件不會(huì)回傳給ViewGroup,而是默默的消逝掉。我們知道,一旦消耗down事件,接下來(lái)的該系列所有的事件都會(huì)交給這個(gè)View,因此,如果不處理down以外的事件,這些事件就會(huì)被“遺棄”。

  • 某個(gè)View,在onTouchEvent中,如果針對(duì)最開始的down事件都返回false,那么接下來(lái)的事件系列都不會(huì)交給這個(gè)View。

  • View沒有onInterceptTouchEvent方法。因?yàn)樗皇且粋€(gè)父控件,不需要決定是否攔截。


再ps:本文也參考借鑒了郭霖大神和各位大神的文章:
Android事件分發(fā)機(jī)制完全解析,帶你從源碼的角度徹底理解
徹底理解View事件體系!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容