開發(fā)筆記-自定義View(十)-View的事件分發(fā)機制

前言

關于自定義View系列的文章阁将,好久沒有寫了潦刃。今天抽空看了下Android開發(fā)藝術探索侮措。正好看到了View的事件分發(fā)機制,所以將它寫成筆記記錄下來乖杠。
關于View的事件分發(fā)分扎,我起初是學習郭神的2篇文章。感覺其實也沒有什么胧洒。大致也就了解下畏吓。不過看完其他很多優(yōu)秀的文章和書籍后,才知道自己too young too simple卫漫。下面我們就一起來分析下Android的時間分發(fā)機制菲饼。

關于事件分發(fā)機制,其實網(wǎng)上的文章已經(jīng)有很多了列赎。我簡單的看了幾篇宏悦,發(fā)現(xiàn)寫的都很好。之所以寫這篇文章包吝,主要是記錄自己的學習過程饼煞,其次也想幫助和我一樣的的初學者更加理解與掌握,才是本篇的目的诗越。
注:本文源碼 API=25 同時本文較長砖瞧,可以先收藏再好好閱讀

概念

在學習事件分發(fā)之前我們先來了解下,什么是事件分發(fā)嚷狞。所謂點擊事件(Touch)的事件分發(fā)块促,其實就是對MotionEvent(Touch的封裝)事件的分發(fā)過程,即當一個MotionEvent產(chǎn)生以后感耙,系統(tǒng)需要把這這個事件傳遞給那個具體的View褂乍。這個傳遞的過程就是事件分發(fā)過程

1.MotionEvent

那么MotionEvent又是什么呢即硼?
這個類就是記錄手指接觸屏幕后所產(chǎn)生的一系列的事件(也就是說我們事件分發(fā)其實就是分發(fā)MotionEvent這個對象)逃片。這個類里包含了一系列的事件。事件的類型與含義如下:

事件類型 具體動作
MotionEvent.ACTION_DOWN 按下View(所有事件的開始)
MotionEvent.ACTION_UP 抬起View(與DOWN對應)
MotionEvent.ACTION_MOVE 滑動View
MotionEvent.ACTION_CANCEL 結束事件(非人為原因)

下面列舉2個我們常見的點擊事件序列:

  1. 事件序列 : DOWN->UP 點擊屏幕后松開 (常見的點擊事件)
  2. 事件序列 : DOWN->MOVE->MOVE->...->MOVE->UP 點擊屏幕滑動一會 (常見的滑動屏幕)

用圖來概括如下:

事件系列

                圖片來源

2. 事件分發(fā)的順序

事件分發(fā)的順序是Activity->ViewGroup->View只酥。也就是說在默認情況下褥实。最后消費事件的都是View。雖然我們現(xiàn)在還沒有開始深入講解裂允。但是結合我們日常開發(fā)的情況我們可以想到下面這張流程圖:

事件分發(fā)概括.png

這張流程圖就算我們沒有了解事件分發(fā)损离,通過我們一直的使用規(guī)則來看,也是非常容易理解的绝编。細心的小伙伴會發(fā)現(xiàn)僻澎。為什么Activity向下分發(fā)第一個就是ViewGroup貌踏,如果我們布局中只有一個簡單View控件(如TextView)呢?還記得我們在講View的繪制流程中介紹的嗎窟勃?我們布局加載中的頂級View是DecorView(繼承FrameLayout)祖乳,他本是就是一個ViewGroup。不了解的可以回頭看下這篇文章秉氧。

2. 事件分發(fā)的核心方法

在對事件分發(fā)機制概念眷昆,以及結合平時我們經(jīng)驗總結出來的原理后。下面我們就來通過源碼來去將我們的想法串聯(lián)起來汁咏。不過在看源碼之前亚斋,我們要先講下在事件分發(fā)機制中三個至關重要的方法。如下:

方法 作用 調用時刻 返回結果
dispatchTouchEvent(MotionEvent event) 用來進行事件分發(fā) 在三個方法中第一個被調用攘滩。 如果事件能夠傳遞給當前View/ViewGroup,那么此方法一定會調用 表示是否消耗當前事件
onInterceptTouchEvent(MotionEvent ev) 用來判斷是否攔截某個事件(ViewGroup有此方法帅刊,View沒有) 在dispatchTouchEvent()方法中調用 如果當前View攔截了某個事件,那么同一事件序列將不再會調用此方法轰驳。 表示是否攔截當前事件
onTouchEvent(MotionEvent event) 用來處理點擊事件 在dispatchTouchEvent()方法中調用厚掷,不消耗當前事件,那么當前View在同一事件序列中無法再接受到事件 表示是否消耗當前事件

這三個方法就是事件分發(fā)機制中的核心三個方法级解,也是我們下面在源碼中重要去分析的三個方法冒黑。他們三者之間的關系可以概述如下(注意這是一段偽代碼。在任何類中并沒有此方法勤哗。只是為了對解釋三個方法關系):


/**
  * 點擊事件產(chǎn)生后 
  */ 
  // 步驟1:調用dispatchTouchEvent()
  public boolean dispatchTouchEvent(MotionEvent ev) {

    boolean consume = false; //代表 是否會消費事件

    // 步驟2:判斷是否攔截事件
    if (onInterceptTouchEvent(ev)) {
      // a. 若攔截抡爹,則將該事件交給當前View進行處理
      // 即調用onTouchEvent ()方法去處理點擊事件
        consume = onTouchEvent (ev) ;

    } else {

      // b. 若不攔截,則將該事件傳遞到下層
      // 即 下層元素的dispatchTouchEvent()就會被調用芒划,重復上述過程
      // 直到點擊事件被最終處理為止
      consume = child.dispatchTouchEvent (ev) ;
    }

    // 步驟3:最終返回通知 該事件是否被消費(接收 & 處理)
    return consume;


三個方法的解釋在加上這段偽代碼冬竟,就很好理解三者的關系了:對于一個跟ViewGroup,點擊事件產(chǎn)生后民逼,首先會傳給它泵殴,這時它的dispatchTouchEvent就會被調用,開始進行事件的分發(fā)拼苍,首先會進行判斷笑诅,判斷當前ViewGroup是否進行了攔截。如果進行攔截疮鲫,那么ev(點擊事件)就會交給ViewGroup去處理吆你。不再向下傳遞。分發(fā)結束俊犯。如果沒設置攔截妇多。那么就會調用ViewGroup中所包含的子控件的dispatchTouchEvent (ev)方法,并將事件ev向下傳遞燕侠。如果子控件還是ViewGroup繼續(xù)上面的循環(huán)者祖。知道將事件最終被處理消費掉立莉。這么一看,這不正好對應了我們前面總結的流程圖嘛七问√倚颍看來我們將事件分發(fā)的大致流程已經(jīng)都搞清楚了。

源碼分析

從上面來看烂瘫,好像事件分發(fā)機制也就這些東西了。好像我們都掌握了奇适。其實不然坟比,不過如果你上面的都理解了,說你對Android事件分發(fā)機制了個整體認識嚷往,那就一點都不為過了葛账。不過事件分發(fā)還遠不止這么簡單。里面還是有很多需要注意的點和事件在分發(fā)過程中的一些規(guī)則皮仁。下面我們就從源碼的角度來一一探索籍琳。

1. Activity事件分發(fā)

上面我們說了當一個事件的產(chǎn)生首先是傳遞個Activity。由Activity來進行事件的分發(fā)贷祈。那么我就看下Activity#dispatchTouchEvent():


    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 一般事件列開始都是DOWN事件 = 按下事件趋急,故此處基本是true
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            //實現(xiàn)屏保功能
            //是一個空方法 
            //當此activity在棧頂時,觸屏點擊按home势誊,back呜达,menu鍵等都會觸發(fā)此方法
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        //當一個點擊事件未被Activity下任何一個View接收 / 處理
        //或是發(fā)生在Window邊界外的觸摸事件就會調用
        return onTouchEvent(ev);
    }

這個方法非常短。這里我們重點看第二個if語句粟耻。這里的getWindow().superDispatchTouchEvent(ev)點進去是Window#superDispatchTouchEvent()查近。這是一個抽象方法。不過相信看過我前面文章的小伙伴一定知道這個方法的實現(xiàn)實在PhoneWindow中挤忙。那么找到這個方法如下:

    //PhoneWindow#superDispatchTouchEvent()
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

    /**
     * 這里調用了mDecor.superDispatchTouchEvent(event);方法霜威。這個mDecor就* 是DecorView 下面我們繼續(xù)跟蹤
     */
    //DecorView#superDispatchTouchEvent()
    public boolean superDispatchTouchEvent(MotionEvent event) {
        //DecorView繼承FrameLayout 那么他本是就是一個ViewGroup
        //那么這個方法最后就會調用到ViewGroup#dispatchTouchEvent()
        return super.dispatchTouchEvent(event);
    }

可以看到最后Activity的分發(fā)過程最后就是將事件交給頂級DecorView去進行事件分發(fā)。然后它又會調用ViewGroup#dispatchTouchEvent()册烈。OK戈泼!到這里我們就將我們的事件由Activity->ViewGroup的傳遞。并將返回值設置成true茄厘。表示這個事件已經(jīng)被我們消耗掉了矮冬。

這里還有一點需要注意。從源碼中我們可以看到次哈,假設Activity下的所有View或者我們點擊了Window邊界以外胎署,那么就會調用Activity#onTouchEvent(ev);這個方法:

public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

//Window#shouldCloseOnTouch()
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
        // 主要是對于處理邊界外點擊事件的判斷:是否是DOWN事件,event的坐標是否在邊界內等
        if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
                && isOutOfBounds(context, event) && peekDecorView() != null) {
            // 返回true:說明事件在邊界外窑滞,即 消費事件
            return true;
        }
        // 返回false:未消費(默認)
        return false;
    }

這部分了解即可琼牧,并不是我們的重點恢筝。

2. ViewGroup事件分發(fā)

通過上面的分析,現(xiàn)在事件已經(jīng)從Activity->ViewGroup巨坊。那么我們就分析ViewGroup#dispatchTouchEvent()方法:

    @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.
            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);
                //清空mFirstTouchTarget
                resetTouchState();
            }

            // Check for interception.
            /**
            *講解一
            */
            //檢查是否攔截事件  
            //1.當事件為ACTION_DOWN時 
            //2. 當ViewGroup不攔截事件交給子元素處理 條件成立 即mFirstTouchTarget != null
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //調用事件攔截方法
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

           ......

            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //非MotionEvent.ACTION_CANCEL并且沒有攔截事件
            //進入if語句撬槽,對ViewGroup的子元素進行遍歷
            /**
            *講解三
            */
            if (!canceled && !intercepted) {

                // If the event is targeting accessiiblity focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    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--) {
                            //1.子元素是否在做動畫
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            //2.事件左邊是否落在子元素區(qū)域內
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            //接受點擊事件的View根據(jù)1.2條件判斷  
                            //是否能夠接受點擊事件
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                //不符合要求 執(zhí)行下一個循環(huán)
                                continue;
                            }
                            ......
                            //調用此方法進行事件分發(fā)處理 如果有子元素
                            //那么參數(shù)中的child!=null
                             /**
                              *講解四
                              */
                            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;
                                            //條件滿足跳出循環(huán)
                                            //這里是跳出內部for循環(huán)不是外部的
                                            //其實就是break的用法
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                            /**
                              *講解五
                              */
                                //對mFirstTouchTarget 進行賦值
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        
                        ......
                    }
                }
            }

            /**
            *講解六
            */
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                ......
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                //MotionEvent.ACTION_UP 后 清空mFirstTouchTarget
                /**
                *講解七
                */
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

     ......
    }

關于ViewGroup的事件分發(fā)源碼(即dispatchTouchEvent()方法)趾撵,還是比較長的侄柔,同時也是難點,下面我們將上面的代碼拆分來看,來具體分析占调。

part1
·
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
             
             /**
             * 講解二
             */
            // Handle an initial down.
            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);
                //清空mFirstTouchTarget
                resetTouchState();
            }

            // Check for interception.
            /**
            *講解一
            */
            //檢查是否攔截事件  
            //1.當事件為ACTION_DOWN時 
            //2. 當ViewGroup不攔截事件交給子元素處理 條件成立 即mFirstTouchTarget != null
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //調用事件攔截方法
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

  • 講解一
    這個方法首先會判斷暂题,是否攔截當前事件。這個條件第一個好理解究珊。一個事件的系列一定是由DOWN開始的薪者,那么就會進入條件語句。調用onInterceptTouchEvent(ev)開始進行事件攔截剿涮。關于第二個代碼中有解釋言津。這個方法是在那里還原初始值與賦值請看下面。
  • 講解二
    事件開始時會調用 resetTouchState();清空mFirstTouchTarget
part2
//非MotionEvent.ACTION_CANCEL并且沒有攔截事件
            //進入if語句取试,對ViewGroup的子元素進行遍歷
            /**
            *講解三
            */
            if (!canceled && !intercepted) {

                        ......省略
                        
                        //對子元素進行遍歷
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            //1.子元素是否在做動畫
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            //2.事件左邊是否落在子元素區(qū)域內
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            //接受點擊事件的View根據(jù)1.2條件判斷  
                            //是否能夠接受點擊事件
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                //不符合要求 執(zhí)行下一個循環(huán)
                                continue;
                            }
                            ......
                            //調用此方法進行事件分發(fā)處理 如果有子元素
                            //那么參數(shù)中的child悬槽!=null
                             /**
                              *講解四
                              */
                            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;
                                            //條件滿足跳出循環(huán)
                                            //這里是跳出內部for循環(huán)不是外部的
                                            //其實就是break的用法
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                            /**
                              *講解五
                              */
                                //對mFirstTouchTarget 進行賦值
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        
                        ......
                    }
                }
            }
  • 講解三
    進入if語句,判斷條件為沒有對事件進行攔截想括,同時事件沒有結束陷谱。對ViewGroup的子元素進行遍歷
  • 講解?四
    通過判斷,將ViewGroup的子元素進行遍歷瑟蜈,找到能夠處理點擊事件的子元素并調用dispatchTransformedTouchEvent()方法烟逊,進行事件的分發(fā)。
  • 講解五
    當子元素能夠處理點擊事件铺根,就調用addTouchTarget()方法宪躯,對mFirstTouchTarget()方法進行賦值。那么下次再進入講解一方法位迂。
part3

            /**
            *講解六
            */
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                ......
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                //MotionEvent.ACTION_UP 后 清空mFirstTouchTarget
                /**
                *講解七
                */
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

  • 講解六
    講解三中的if語句不成立表示對事件進行攔截访雪。那么直接走到了講解六,并且mFirstTouchTarget == null沒有在子元素的遍歷中賦值掂林,即條件成立臣缀。執(zhí)行dispatchTransformedTouchEvent()方法。
  • 講解七
    在同一事件系列結束后調用resetTouchState();對mFirstTouchTarget清空還原泻帮。

這樣我們就將ViewGroup#dispatchTouchEvent()方法分析完成了精置。

在上面的講解中我們多此提到mFirstTouchTarget與dispatchTransformedTouchEvent()方法。前者已經(jīng)說明了他的作用與賦值及清空還原的位置锣杂。對于后者脂倦,這個方法其實就是ViewGroup對事件分發(fā)番宁。看下他的源碼:

   private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
        ......
    }

是ViewGroup#dispatchTransformedTouchEvent()方法赖阻,其他方法省略蝶押。可以可看到火欧,如果ViewGroup有子元素同時子元素可以處理點擊事件棋电。那么就會調用子元素的child.dispatchTouchEvent(event);方法。如果是child是ViewGroup繼續(xù)上面的循環(huán)苇侵,如果子元素是View离陶,那么就或調用View.dispatchTouchEvent(event)方法。關于這個方法我們后面分析衅檀。如果child為空(即講解六),那么就會調用super.dispatchTouchEvent(event)方法霎俩,那么就會調用ViewGroup父類的哀军,即View.dispatchTouchEvent(event)方法,ViewGroup自己處理點擊事件打却。最后都會默認(是默認不是一定) 調用onTounchEvent()方法杉适。

通關對源碼與這七個重要部分的講解。我們可以總結如下幾點:

  1. 一個事件序列只能一個View進行攔截且消耗柳击。由講解三我們知道猿推。如果攔截事件,就不會進入if語句對子元素進行遍歷與事件分發(fā)捌肴。同時又講解六我們知道蹬叭,如果攔截了某一事件(如MOVE)那么統(tǒng)一事件序列內的所有事件都交給它處理。
  2. 某個View一旦攔截事件状知,那么這一事件序列只能有它來處理(由1可知)弥雹。同時我們知道既然攔截就無法進入講解三中转捕,那么mFirstTouchTarget就無法被賦值,那么講解一中的條件就不成立。所以調用onInterceptTouchEvent()不會再被調用运悲。其實通過1.我們也都理解,如果攔截那么同一事件序列的所有事件都間給當前View處理察绷。你攔截就說明你必須全都處理炮叶。那我還問你干啥。
  3. 贷揽。dispatchTransformedTouchEvent()方法中棠笑,當dispatchTouchEvent()的返回值與dispatchTransformedTouchEvent()返回值相同,由講解六得知擒滑。這樣會直接影響ViewGroup#dispatchTouchEvent()返回值(兩者相同)腐晾。也就是說:View一旦開始處理事件叉弦,如果它不消耗ACTION_DOWN事件(onTounchEvent()返回false)。那么統(tǒng)一事件序列的其他事件都不會交給他處理藻糖。并會重新交給父元素(注意是父元素淹冰,不是父類)去處理,即父元素onTounchEvent()會被調用巨柒。
  4. onInterceptTouchEvent()默認返回false樱拴,即默認不攔截任何事件。
  5. View沒有onInterceptTouchEvent()洋满。一旦事件傳遞給他晶乔,那么他的onTouchEvent就會調用。

關于ViewGroup的源碼分析我們也就到這里了牺勾。有的啰嗦正罢。不過詳細才能跟好的理解與全面

3. View事件分發(fā)

由上面dispatchTransformedTouchEvent()方法可知,最后方法無論是ViewGroup消耗還是View消耗都會調用View#dispatchTouchEvent()方法驻民。那么我們就來看這個方法:

 public boolean dispatchTouchEvent(MotionEvent event) {
       
        ......
        
        boolean result = false;

        ......

        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 //是否是enable可點擊的 按鈕默認都是可點擊的 ImageView 不可點擊
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //調用onTouchEvent(event)方法 返回值直接影響此方法的返回值
            //返回值與次方法相同
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        ......

        return result;
    }

View的dispatchTouchEvent比較簡單翻具。首先判斷mOnTouchListener!=null同時li.mOnTouchListener.onTouch(this, event)返回為true那么result = true;回还。那么下面的 if (!result && onTouchEvent(event)) 中的第一個條件就不會成立所以onTouchEvent(event)永遠不會得到執(zhí)行裆泳。有此可見onTouch()優(yōu)先級要高于onTouchEvent(event)。
下面我們看下默認情況進入onTouchEvent(event)方法中:

    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        
        /**
        *講解一
        */
        //首先判斷當前View是不是DISABLED不可用狀態(tài)
        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.
            //如果不可用 同時當前控件的clickable與long_clickable
            //與CONTEXT_CLICKABLE全是false
            //那么才返回false
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        //如果View有代理會執(zhí)行這個方法
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        /**
        *講解二
        */
        //只要控件的clickable與long_clickable
        //與CONTEXT_CLICKABLE 有一個為true 就進入次循環(huán)
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                        ......
                  
                        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)) {
                                 /**
                                  *講解三
                                  */
                                //onClickListener監(jiān)聽在此方法中
                                    performClick();
                                }
                            }
                        }

                       ......
                    }
                    break;

                ......
            }
            //默認返回 true
            /**
            *講解四
            */
            return true;
        }

        return false;
    }

這部分代碼比較簡單柠硕。主要的都有注釋工禾。如果控件!=DISABLED蝗柔,那么就會進入同時講解二判斷有一個成立闻葵。就會進入switch語句。當接收到MotionEvent.ACTION_UP是癣丧。最后執(zhí)行performClick()方法.這個方法代碼如下:

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

可以看到我們的設置setOnclickListener就是在這個賦值笙隙,li.mOnClickListener.onClick(this);就會調用我們的onClik方法。
那么有的同學會問View的longClickable默認是false坎缭,同時TextView的clickable也為false竟痰,那么為何我們給TextView設置setOnclickListener也能生效。我們下來看下TextView源碼其他默認clickable=false的控件是一樣的掏呼。:

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
        //設置clickable
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
    
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
        //設置longclickable
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

可以看到在設置監(jiān)聽時坏快,方法內部已經(jīng)幫我們設置了。

下面我們在針對onTouchEvent(MotionEvent event)方法來拆分分析下:

part1

        /**
        *講解一
        */
        //首先判斷當前View是不是DISABLED不可用狀態(tài)
        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.
            //如果不可用 同時當前控件的clickable與long_clickable
            //與CONTEXT_CLICKABLE全是false
            //那么才返回false
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        //如果View有代理會執(zhí)行這個方法
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
  • 講解一
    由代碼可知即使控件是DISABLED狀態(tài)憎夷,只要clickable與longclickable有一個返回true那么此方法就返回true莽鸿,即事件被消費。但是不會執(zhí)行onClick()方法。這點通過代碼很容易理解祥得。
part2
/**
        *講解二
        */
        //只要控件的clickable與long_clickable
        //與CONTEXT_CLICKABLE 有一個為true 就進入次循環(huán)
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                        ......
                  
                        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)) {
                                 /**
                                  *講解三
                                  */
                                //onClickListener監(jiān)聽在此方法中
                                    performClick();
                                }
                            }
                        }

                       ......
                    }
                    break;

                ......
            }
            //默認返回 true
            /**
            *講解四
            */
            return true;
        }


  • 講解二
    講解二中只要有一個條件滿足兔沃。就會進入switch語句。當接收到MotionEvent.ACTION_UP時(前提MotionEvent.ACTION_DOWN也接收到了)會經(jīng)過判斷最后執(zhí)行 performClick();方法级及。
  • 講解三
    performClick()方法內部會執(zhí)行我們設置的監(jiān)聽乒疏,即onClick()方法。
  • 講解四
    由代碼可知只要講解二中的if語句成立饮焦,不管進入switch中的任何ACTION或是都不進入怕吴,返回值都是true,即事件消費了县踢。同時講解四也證明默認情況下是返回true

總結

下面我們用流程圖在來總結下:

總體流程總結

                 圖片來源 侵權即刪

流程圖真的懶得畫了转绷。一篇文章學習得3.4天。寫出來又得很長時間硼啤,所以大家勿怪议经。同時這張圖結合文章理解起來簡直是so easy。
其實關于Android事件分發(fā)機制優(yōu)秀的文章由很多谴返。如果觀看一篇文章無法完全掌握爸业,就多看幾篇文章。然后自己總結亏镰,結合。反正最后能理解成自己的就算成功了拯爽。

結語

本人是個菜鳥索抓,如果文章哪里有錯誤,歡迎指出毯炮。有問題也可以留言逼肯。最后如果文章對您有幫助。感謝支持桃煎。

優(yōu)秀干貨

Android事件分發(fā)機制
Android事件分發(fā)機制詳解:史上最全面篮幢、最易懂
《Android開發(fā)藝術探索》

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市为迈,隨后出現(xiàn)的幾起案子三椿,更是在濱河造成了極大的恐慌,老刑警劉巖葫辐,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搜锰,死亡現(xiàn)場離奇詭異,居然都是意外死亡耿战,警方通過查閱死者的電腦和手機蛋叼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人狈涮,你說我怎么就攤上這事狐胎。” “怎么了歌馍?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵握巢,是天一觀的道長。 經(jīng)常有香客問我骆姐,道長镜粤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任玻褪,我火速辦了婚禮肉渴,結果婚禮上,老公的妹妹穿的比我還像新娘带射。我一直安慰自己同规,他們只是感情好,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布窟社。 她就那樣靜靜地躺著券勺,像睡著了一般。 火紅的嫁衣襯著肌膚如雪灿里。 梳的紋絲不亂的頭發(fā)上关炼,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機與錄音匣吊,去河邊找鬼儒拂。 笑死,一個胖子當著我的面吹牛色鸳,可吹牛的內容都是我干的社痛。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼命雀,長吁一口氣:“原來是場噩夢啊……” “哼蒜哀!你這毒婦竟也來了?” 一聲冷哼從身側響起吏砂,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤撵儿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后狐血,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體统倒,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年氛雪,在試婚紗的時候發(fā)現(xiàn)自己被綠了房匆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖浴鸿,靈堂內的尸體忽然破棺而出井氢,到底是詐尸還是另有隱情,我是刑警寧澤岳链,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布花竞,位于F島的核電站,受9級特大地震影響掸哑,放射性物質發(fā)生泄漏约急。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一苗分、第九天 我趴在偏房一處隱蔽的房頂上張望厌蔽。 院中可真熱鬧,春花似錦摔癣、人聲如沸奴饮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽戴卜。三九已至,卻和暖如春琢岩,著一層夾襖步出監(jiān)牢的瞬間投剥,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工担孔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留江锨,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓攒磨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親汤徽。 傳聞我的和親對象是個殘疾皇子娩缰,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內容