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

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

秉承著前人栽樹后人乘涼的想法,首先給出自己所讀總結(jié)的精華和大神的總結(jié)圖:
在Android中惋耙,View的事件分發(fā)主要有3個方法:dispatchTouchEvent()捣炬、 onInterceptTouchEvent() 以及 onTouchEvent()黔龟。當(dāng)我們點擊某個子View控件時心包,首先調(diào)用的是這個view的父ViewGroup的dispatchTouchEvent()加矛,dispatchTouchEvent()內(nèi)部調(diào)用onInterceptTouchEvent()來決定是否攔截該事件习劫,如果攔截到踏,則調(diào)用ViewGroup的onTouchEvent()進行處理拦宣,并且屏蔽掉該事件擒权,不再往下傳遞缚陷,所以有可能會產(chǎn)生你點擊某個按鈕届腐,按鈕的處理動作沒有發(fā)生铁坎,父控件的處理事件觸發(fā)了的情況。如果父ViewGroup不攔截事件犁苏,否則調(diào)用子View的dispatchTouchEvent()硬萍,可以參考如下圖:


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

ViewGroup

首先我們來定義一個布局短曾,只有一個Button按鈕寒砖,外圍是一個我們自定義的Layout,我們?nèi)∶麨門estLayout。暫時只繼承于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注冊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注冊O(shè)nClickListener        
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("touchEvent", "button的點擊事件執(zhí)行了!");
            }
        });
        //mButton注冊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;
            }
        });
    }
}

點擊按鈕運行一下看一下日志:


點擊按鈕日志

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

 @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)鍵代碼貼出來圾浅,我們來開始分析一下掠手,在上面代碼1處判斷如果是按下事件,會調(diào)用cancelAndClearTouchTargets(ev)方法狸捕,這個方法里面會把mFirstTouchTarget置空喷鸽。這個變量非常重要,在后面會繼續(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;
            }
          ...//其他代碼

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

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

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

public class TestLayout extends LinearLayout {

   ...//其他代碼

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

我們再點擊按鈕看看效果:

點擊按鈕日志

如上圖,我們可以看到碰煌,當(dāng)TestLayout攔截了觸摸事件時舒岸,Button的觸摸和點擊事件都沒有被觸發(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處,會定義一個 newTouchTarget變量个少。接著會判斷事件是否取消并且是否打斷洪乍,如果都不。那么執(zhí)行if條件里面的代碼夜焦。
在代碼4處壳澳,會定義一個int類型的childrenCount來表示該ViewGroup有多少個子View。接下來再判斷childrenCount個數(shù)是否為0(即ViewGroup是否有子View)茫经。如果有巷波,那么就執(zhí)行代碼去尋找事件應(yīng)該傳遞到哪個集體的子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處會調(diào)用dispatchTransformedTouchEvent()方法,這個是什么方法呢抹镊?我們再跟進去看看:

   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;
        }
....//其他代碼

我們在這里就可以看到锉屈,當(dāng)child為空時,表示沒有找到觸摸事件在某個具體子View的范圍內(nèi)垮耳。此時調(diào)用super.dispatchTouchEvent()方法颈渊,而ViewGroup的父類是View對象。表明此時事件已經(jīng)交給ViewGroup自己處理终佛。而如果child不為空儡炼,則調(diào)用子View的dispatchTouchEvent()方法來處理。當(dāng)觸摸事件處理完畢查蓉,就會返回一個布爾值handled乌询,該值表示子View是否消耗了事件。怎樣判斷一個子View是否消耗了事件呢豌研?如果說子View的onTouchEvent()返回true妹田,那么就是表明消耗了事件。

接下來在代碼6處鹃共,會調(diào)用addTouchTarget()方法來賦值給newTouchTarget變量鬼佣。我們再跟進去看一看:

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

我們可以看到,首先是找到具體的child的TouchTarget對象霜浴,然后mFirstTouchTarget指向這個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處會判斷mFirstTouchTarget是否為空,很明顯阴孟,在剛才的分析里可以看到晌纫,如果我們的觸摸事件沒有被攔截,就會在addTouchTarget()方法中為mFirstTouchTarget賦值永丝,因此這段代碼就不會被執(zhí)行锹漱。只有在ViewGroup類型不攔截或者點擊了ViewGroup的空白處(沒有點擊任何子控件,事件沒有發(fā)生在任何子View的范圍內(nèi))慕嚷。這兩種情況下mFirstTouchTarget不會被改變依然為null才會執(zhí)行if條件里面的代碼哥牍。換句話說反過來也就是如果mFirstTouchTarget == null條件成立執(zhí)行的這段代碼會處理ViewGroup攔截了事件或者所有子View均不消耗事件這兩種情況。那么在這里調(diào)用dispatchTransformedTouchEvent()方法交由ViewGroup處理事件喝检。如果mFirstTouchTarget不為空嗅辣,那么表明有child已經(jīng)處理了ACTION_DOWN觸摸事件,那么執(zhí)行else代碼塊的代碼。

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

  • 當(dāng)事件傳遞到我們的ViewGroup時伯顶,會調(diào)用dispatchTouchEvent()方法囚灼,在里面首先會調(diào)用onInterceptTouchEvent()方法(默認為false不攔截)來決定ViewGroup是否攔截該事件。結(jié)果為兩種:1. 如果方法返回true,即要攔截祭衩,那么事件傳遞到此為止灶体,不再傳遞給子View。2.如果放回false,不攔截事件掐暮,那么將事件傳遞給子View,由子View去進行處理蝎抽。

View

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

我們先來把demo改一下代碼樟结,首先把之前的Button去掉,重寫一個自定義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的點擊事件執(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);
    }
}

布局如下很簡單:

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

我們運行demo點擊一下按鈕瓢宦,看一下日志結(jié)果:


點擊按鈕日志

如上圖結(jié)果,我們可以看到顯然是onTouch方法先執(zhí)行灰羽,然后OntouchEvent()再執(zhí)行驮履,然后又執(zhí)行了click()方法,因為我們手指按下后又抬起廉嚼。所以有兩個事件先后發(fā)生處理玫镐。

那么為什么onTouch()方法會比onTouchEvent()方法先執(zhí)行呢?onTouch()方法返回類型為布爾值前鹅,有什么用呢摘悴?(不可能因為onTouch字母數(shù)比onTouchEvent()字母少所以先執(zhí)行把?)帶著這些疑惑舰绘,我們來一起分析分析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...首先可以看到源碼其實包含了很多注釋了很多其他代碼來做一些判斷和處理工作葱椭,在這里我抽出核心過程代碼來看看:

  //ListenerInfo是一個包裝類捂寿,里面封裝了View的各種監(jiān)聽器Listener。
  //我們設(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驳概。那么接下來就不會再執(zhí)行onTouchEvent()方法赤嚼。因此onTouch()方法比onTouchEvent()方法優(yōu)先執(zhí)行的原因在這里顺又。我們來設(shè)置onTouch()方法返回true來看看效果:

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

運行點擊一下按鈕:

按鈕點擊日志

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

誒上枕?突然發(fā)現(xiàn),怎么onClick()方法也沒了呢弱恒?辨萍?我沒有改點擊事件啊返弹?那么為什么我們的onTouch()方法返回true后分瘦,onClick()方法沒有執(zhí)行呢?我們跟進去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.....好像有點長嘲玫??看了真是讓人頭大并扇。那么長的代碼去团。大概的我們也不可能一一去分析,那么我們就過濾掉一些無關(guān)代碼穷蛹,來提取一下精華:

 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ù)跟進去查看代碼:

 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í)行了我們在代碼中設(shè)置的onClickListener的onClick()方法蛙吏。因此源哩,如果我們的onTouch()方法返回true,那么onTouchEvent()方法也不會執(zhí)行,那么onTouchEvent()方法里的onClick()方法更不能得到執(zhí)行鸦做!

那么問題又又又來了....onTouch()返回值影響了onTouchEvent()是否執(zhí)行励烦,那么onTouchEvent()的返回值又是拿來干嘛的?好像這個返回值又不影響點擊事件的執(zhí)行泼诱?我們來改一改試成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;
    }

我們來看一下效果:


返回false效果

返回true效果

我們看到坛掠,首先修改為true或者false后,點擊事件都消失了,這個其實很好理解屉栓。因為onClick()方法之前分析過是在View的onTouchEvent()方法調(diào)用的舷蒲。而我們現(xiàn)在重寫onTouchEvent()方法卻沒有調(diào)用super.onTouchEvent()方法,因此點擊事件肯定不會被調(diào)用友多。

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

其實這是因為onTouchEvent()當(dāng)我們返回false,表明我們子View不處理事件藐窄,于是就會將事件全部回傳給父布局。當(dāng)down事件不消耗后酬土,后續(xù)的move,up等等事件也不再執(zhí)行onTouch()方法荆忍。因此在這里直接將事件交給父布局,而在這里我們的父布局TestLayout在Activity中設(shè)置的onTouch()方法也返回false撤缴。因此也會將事件繼續(xù)上傳刹枉,這里有興趣的同志可以把TestLayout的onTouch()方法返回true來看一下效果(文章篇幅實在太長了。屈呕。微宝。)。
而當(dāng)我們返回true就表示我們要處理事件虎眨,因此事件就不會回傳到父布局了蟋软。

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


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

  • View的執(zhí)行順序為onTouch()->onTouchEvent()->onClick()恰矩。如果onTouch()返回true表示消耗了事件记盒,就不再傳給onTouchEvent()執(zhí)行。(好比小明和小剛一起去買冰淇淋外傅,小明先吃一口纪吮,他可以選擇把冰淇淋給小剛吃還是把冰淇淋留下來)

  • 如果View只消耗down事件,而不消耗其他事件萎胰,那么其他事件不會回傳給ViewGroup碾盟,而是默默的消逝掉。我們知道技竟,一旦消耗down事件冰肴,接下來的該系列所有的事件都會交給這個View,因此榔组,如果不處理down以外的事件熙尉,這些事件就會被“遺棄”。

  • 某個View搓扯,在onTouchEvent中检痰,如果針對最開始的down事件都返回false,那么接下來的事件系列都不會交給這個View锨推。

  • View沒有onInterceptTouchEvent方法铅歼。因為它不是一個父控件,不需要決定是否攔截爱态。


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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末锦担,一起剝皮案震驚了整個濱河市俭识,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌洞渔,老刑警劉巖套媚,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異磁椒,居然都是意外死亡堤瘤,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門浆熔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來本辐,“玉大人,你說我怎么就攤上這事∩髦澹” “怎么了老虫?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長茫多。 經(jīng)常有香客問我祈匙,道長,這世上最難降的妖魔是什么天揖? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任夺欲,我火速辦了婚禮,結(jié)果婚禮上今膊,老公的妹妹穿的比我還像新娘些阅。我一直安慰自己,他們只是感情好万细,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布扑眉。 她就那樣靜靜地躺著,像睡著了一般赖钞。 火紅的嫁衣襯著肌膚如雪腰素。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天雪营,我揣著相機與錄音弓千,去河邊找鬼。 笑死献起,一個胖子當(dāng)著我的面吹牛洋访,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播谴餐,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼姻政,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了岂嗓?” 一聲冷哼從身側(cè)響起汁展,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎厌殉,沒想到半個月后食绿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡公罕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年器紧,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片楼眷。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡铲汪,死狀恐怖熊尉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情桥状,我是刑警寧澤帽揪,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布硝清,位于F島的核電站辅斟,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏芦拿。R本人自食惡果不足惜士飒,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蔗崎。 院中可真熱鬧酵幕,春花似錦、人聲如沸缓苛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽未桥。三九已至笔刹,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間冬耿,已是汗流浹背舌菜。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留亦镶,地道東北人日月。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像缤骨,于是被迫代替她去往敵國和親爱咬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容