第六章 Android 開發(fā)中的View和事件分發(fā)機制

1. 概述

??作為Android開發(fā)中最常見的一個控件,個人覺得有必要談談了柳弄。我們剛開始接觸Android的時候最常見的一些基本控件就有TextView,EditText,Button......,但是細心的你會發(fā)現(xiàn)它們的父類都是View。但是View也不能單純是一個控件谬泌,它應該算是一個體系亲桦。我們在進行Android開發(fā)的時候忆嗜,有時候一些系統(tǒng)提供的控件不能滿足我們的需求凄贩,這時候我們需要對進行自定義控件的編寫,定制符合我們要求的控件苟鸯,這樣才能滿足我們的功能需求同蜻。
??在介紹View的時候,首先想提供這樣一個知識路線圖早处,View的基礎概念湾蔓,讓我們知道什么是View;View的位置參數(shù)砌梆,了解View的移動默责;View的觸碰事件,了解事件分發(fā)機制么库;View的滑動沖突處理 以及View的一些用法傻丝。

2. 認識View

??View是Android中所有控件的基類。同時诉儒,它也是界面層所有控件的一種抽象,它代表了一個控件亏掀〕婪矗或者這樣說泛释,View既可以作為一個控件來使用,也可以是一個基類温算,許多基礎控件都是繼承View的怜校。
??ViewGroup和View的關系。Android中的ViewGroup也是繼承View的注竿。ViewGroup茄茁,翻譯成控件組,意思是很多View控件的集合巩割。ViewGroup的內部包含了很多控件裙顽,也可以說ViewGroup包含了許多的View。這意味著View本身可以是單個的控件宣谈,也可以是很多個控件組成的一組控件愈犹。通過這種關系形成了一個View樹結構。
??舉個栗子闻丑,我們知道TextView是一個View, 而RelativeLayout不但是一個View,還是一個ViewGroup漩怎;再舉個栗子,我們知道數(shù)學中的集合嗦嗡,一個集合中有許多子集合勋锤,但同時,這些子集合也包含一些更小集合侥祭。子集合就相當于自己是一個View怪得,并且還是一個ViewGroup。所以說ViewGroup內部是可以有子View的卑硫,子View同樣還可以是ViewGroup徒恋,以此類推。這種層級關系有助于我們了解View的結構機制欢伏。

3. View的位置參數(shù)

??我們知道數(shù)學系中的坐標軸入挣,一個原點,兩條帶方向箭頭的坐標線硝拧;Android中的位置參數(shù)也是如此,但是頂點的坐標不一樣滋恬。方向也不一樣恢氯,Android中的View坐標以左上角為頂點,向右和向下為遞增方向。

坐標方向

??View的位置主要是由它的四個頂點的來決定的勋拟,分別對應了View的四個屬性: top 勋磕, left , right 敢靡, bottom 挂滓。它們都是以頂點為參照點,top是左上角縱坐標啸胧,left是左上角橫坐標赶站,right是右下角的橫坐標,bottom是右下角的縱坐標纺念。它們的坐標位置都是相對于父容器來說的贝椿。這是一種相對坐標。它們的關系如下圖所示:

View的位置坐標和父容器的關系

??在Android中柠辞,X軸和Y軸的正方向是右和下团秽,不僅如此,大部分顯示系統(tǒng)都是按照這個標準來定義坐標系的叭首。所以在計算View的寬高的時候习勤,我們這樣計算:

width = right - left
height = bottom - top

??Android 還為我們提供了獲取View的left,top,right,bottom四個參數(shù)的方法。

  • Left = getLeft();
  • Right = getRight();
  • Top = getTop();
  • Bottom = getBottom();

??從Android 3.0開始對View增加了額外的參數(shù): x, y,translationX,translationY焙格。其中x,y是View的左上角的坐標图毕,而translationX和translationY是View左上角相對于父容器的偏移量。這幾個參數(shù)也是相對于父容器的坐標眷唉,其中translationX和translationY的默認值是0予颤,和View的四個基本的位置參數(shù)一樣,它們的換算關系:

x= left + translationX
y = top + translationY

注意:當View發(fā)生平移的時候冬阳,top和left表示的是原始左上角的位置信息蛤虐,它的值不會發(fā)生改變,此時發(fā)生改變的是x肝陪,y驳庭,translationX,translationY這四個參數(shù)氯窍。這樣我們就可以通過了解View的位置以及偏移量來了解它的運動軌跡饲常。

4. View的事件分發(fā)

4.1 典型的分發(fā)事件

??因為我們的是移動設備,屏幕觸碰是基本的要求狼讨。了解觸碰事件(MotionEvent)贝淤,認識一下事件的分發(fā)機制是很有必要的。手指觸碰屏幕以后會產生一系列的事件政供,典型的事件有下面幾個:

  • ACTION_DOWN ——手指剛接觸屏幕

  • ACTION_MOVE ——手指在屏幕上移動

  • ACTION_UP ——手指從屏幕上松開

所以當我們將手指觸摸屏幕的話播聪,考慮如下幾種情況:

  • 屏幕點擊一次就離開: DOWN--------->UP

  • 屏幕點擊且按住滑動再離開:DOWN---->MOVE...----->MOVE------->UP

??上述三種情況是典型的事件序列朽基,同時通過MotionEvent對象,我們可以得到點擊事件發(fā)生的x和y坐標犬耻。系統(tǒng)提供了兩組方法:getX/getY 和getRawX/getRawY踩晶。這兩組方法的不同之處在于參照對象的不同执泰,getX/getY返回的是相對于當前View的左上角的x和y坐標枕磁,而getRawX/getRawY是相對于手機屏幕左上角的x和y坐標。

4.2 View的事件分發(fā)機制

??點擊事件的事件分發(fā)术吝,其實是對MotionEvent事件的分發(fā)的過程计济。即當一個MotionEvent產生以后,系統(tǒng)需要把這個事件傳遞給一個具體的View排苍,這個傳遞的過程其實就是分發(fā)過程沦寂。點擊事件的分發(fā)過程由三個很重要的方法來共同完成。

public boolean dispatchTouchEvent(MotionEvent ev)

?? 用來進行事件的分發(fā)淘衙,如果事件能夠傳遞給當前的View传藏。那么這個方法一定會被調用,返回結果受當前View的onTouchEvent和下級View的dispatchTouchEvent方法影響彤守,表示是否消耗當前事件毯侦。
??Touch 事件發(fā)生時 Activity 的 dispatchTouchEvent(MotionEvent ev) 方法會以隧道方式(從根元素依次往下傳遞直到最內層子元素或在中間某一元素中由于某一條件停止傳遞)將事件傳遞給最外層 View 的 dispatchTouchEvent(MotionEvent ev) 方法,并由該 View 的 dispatchTouchEvent(MotionEvent ev) 方法對事件進行分發(fā)具垫。

public boolean onInterceptTouchEvent(MotionEvent ev)

??在上述的方法內部調用侈离,用來判斷是否攔截當前事件,如果當前View攔截了某個事件筝蚕,那么在同一個事件序列當中卦碾,這個方法不會被調用。返回結果表示是否攔截當前事件起宽。
??在外層 View 的 dispatchTouchEvent(MotionEvent ev) 方法返回系統(tǒng)默認的 super.dispatchTouchEvent(ev) 情況下洲胖,事件會自動的分發(fā)給當前 View 的 onInterceptTouchEvent 方法。onInterceptTouchEvent 的事件攔截邏輯如下:

  1. 如果 onInterceptTouchEvent 返回 true坯沪,則表示將事件進行攔截绿映,并將攔截到的事件交由當前 View 的 onTouchEvent 進行處理;
  2. 如果 onInterceptTouchEvent 返回 false屏箍,則表示將事件放行绘梦,當前 View 上的事件會被傳遞到子 View 上,再由子 View 的 dispatchTouchEvent 來開始這個事件的分發(fā)赴魁;
  3. 如果 onInterceptTouchEvent 返回 super.onInterceptTouchEvent(ev)卸奉,事件默認不會被攔截,并將攔截到的事件交由當前 View 的 onTouchEvent 進行處理颖御。

public boolean onTouchEvent(MotionEvent event)

??在dispatchTouchEvent 方法中調用榄棵,用來處理點擊事件凝颇,返回結果表示是否消費當前事件,如果不消費,則在同一事件序列中逢防,當前View無法再次接受事件表蝙。
??在 dispatchTouchEvent 返回 super.dispatchTouchEvent(ev) 并且 onInterceptTouchEvent 返回 true 或返回 super.onInterceptTouchEvent(ev) 的情況下 onTouchEvent 會被調用。onTouchEvent 的事件響應邏輯如下:

  1. 如果事件傳遞到當前 View 的 onTouchEvent 方法垫蛆,而該方法返回了 false,那么這個事件會從當前 View 向上傳遞腺怯,并且都是由上層 View 的 onTouchEvent 來接收袱饭,如果傳遞到上面的 onTouchEvent 也返回 false,這個事件就會“消失”呛占,而且接收不到下一次事件虑乖。
  2. 如果返回了 true 則會接收并消費該事件。
  3. 如果返回 super.onTouchEvent(ev) 默認處理事件的邏輯和返回 false 時相同晾虑。

??Android 中提供了View 疹味,ViewGroup,Activity三個層次的Touch事件處理帜篇。處理過程是按照Touch事件從上到下傳遞糙捺,再按照是否消費的返回值從下往上傳遞。如果View的onTouchEvent返回false,將會向上傳給它的parent的ViewGroup坠狡,如果ViewGroup不消費继找,會往上傳給Activity。

即隧道式向下分發(fā)逃沿,然后冒泡式處理

??onInterceptTouchEvent用于改變事件的傳遞方向婴渡。決定傳遞方向的是返回值,返回為false時事件會傳遞給子控件凯亮,返回值為true時事件會傳遞給當前控件的onTouchEvent()边臼,這就是所謂的Intercept(攔截)。
??正確的使用方法是假消,在此方法內僅判斷事件是否需要攔截柠并,然后返回。即便需要攔截也應該直接返回true富拗,然后由onTouchEvent方法進行處理臼予。
??onTouchEvent用于處理事件,返回值決定當前控件是否消費(consume)了這個事件啃沪。尤其對于ACTION_DOWN事件粘拾,返回true,表示我想要處理后續(xù)事件创千;返回false缰雇,表示不關心此事件入偷,并返回由父類進行處理。

??Android 中與 Touch 事件相關的方法包括:dispatchTouchEvent(MotionEvent ev)械哟、onInterceptTouchEvent(MotionEvent ev)疏之、onTouchEvent(MotionEvent ev);能夠響應這些方法的控件包括:ViewGroup暇咆、View锋爪、Activity。方法與控件的對應關系如下表所示:

Touch事件相關方法 方法功能 ViewGroup View Activity
dispatchTouchEvent(MotionEvent ev) 事件分發(fā) Yes Yes Yes
onInterceptTouchEvent(MotionEvent ev) 事件攔截 Yes No No
onTouchEvent(MotionEvent event) 事件響應 Yes Yes Yes

??從這張表中我們可以看到 ViewGroup 和 View 對與 Touch 事件相關的三個方法均能響應糯崎,而 Activity 對 onInterceptTouchEvent(MotionEvent ev) 也就是事件攔截不進行響應几缭。另外需要注意的是 View 對 onInterceptTouchEvent(MotionEvent ev) 的響應的前提是可以向該 View 中添加子 View河泳,如果當前的 View 已經(jīng)是一個最小的單元 View(比如 TextView)沃呢,那么就無法向這個最小 View 中添加子 View,也就無法向子 View 進行事件的攔截拆挥,所以它沒有 onInterceptTouchEvent(MotionEvent ev)薄霜。

事件分發(fā)機制圖解

  • 從上圖所示中,事件的分發(fā)機制分為3層纸兔,分別是Activity惰瓜,ViewGroup,View汉矿。
  • 事件的返回值分別為 return false 崎坊,true,super.xxxx洲拇。super是調用父類實現(xiàn)的意思奈揍。
  • 事件的分發(fā)機制是從左上角的ACTION_DOWN開始的,由Activity的dispatchTouchEvent()開始分發(fā)
  • 在dispatchTouchEvent() 和onTouchEvent()中赋续,return true 男翰,代表事件傳遞到這里就消費掉了,事件不是再進行傳遞了纽乱。
  • 在Activity中的dispatchTouchEvent()中蛾绎,只有傳遞過來super ,才能繼續(xù)向下分發(fā)事件。除此外return true/false都表示事件被消費掉了鸦列。

再來一張U形圖方便記憶租冠,從Action_Down開始,每個事件分別返回true,false,super薯嗤。

左邊是向下事件分發(fā)的理解:

第一層是Activity層顽爹,Activity層 return true/ false消費事件,return super 將事件分發(fā)到了ViewGroup層应民;

第二層是ViewGroup層话原,return true消費事件,return false 將事件回傳到父類Activity夕吻,進行事件的響應。return super 進行事件攔截繁仁。事件攔截以后涉馅,返回false/或者super才能將事件傳遞到下一層View.

第三層是View層,return true 消費事件黄虱,return false將事件會傳導父類ViewGroup,return true 進行事件的響應稚矿。

右邊是冒泡向上消費事件的理解:

第三層是View層,在這一層捻浦,進行事件響應的時候晤揣,如果return true ,則直接消費事件朱灿,return super /false 的時候不消費事件昧识,需要將事件響應回傳到父類ViewGroup

第二層是ViewGroup層,return true 盗扒,則直接消費事件跪楞,return super /false 的時候不消費事件,需要將事件響應回傳到父類Activity侣灶。

第一層 是Activity層甸祭,無論返回什么都結束掉。

U形圖事件分發(fā)

4.3 關于事件傳遞的一些小結論

  1. 同一個事件序列是指從手指接觸屏幕開始褥影,到手指離開屏幕結束池户,在這個過程中產生的一系列事件,就是以down事件開始凡怎,中間是許多個move事件校焦,最終以up事件結束。
  2. 正常情況下栅贴,一個事件序列只能被一個View攔截消耗斟湃,一旦一個元素攔截了某個事件,那么這個事件序列中的所有事件都會直接交給它處理檐薯。
  3. 事件一旦交給一個View處理凝赛,那么它就必須消耗掉。在它還沒有消耗掉該事件之前坛缕,那么同一事件中的剩余事件就不再交給它處理墓猎。
  4. 如果View不消耗除ACTION_DOWN以外的其他事件,那么這個點擊事件會消失赚楚,此時父元素的onTouchEvent 并不會被調用毙沾,并且當前Vie可以持續(xù)受到后續(xù)的事件,最終這些消失的點擊事件會傳遞給Activity處理宠页。
  5. ViewGroup 默認不攔截事件左胞,Android 源碼中的ViewGroup 的onInterceptTouchEvent()方法默認返回false寇仓。
  6. View 沒有onInterceptTouchEvent 方法,一旦事件傳遞給他了烤宙,那么它的onTouchEvent方法就會被調用遍烦。
  7. View 的onTouchEvent 默認會消費事件,默認返回true,除非是不可點擊(clickable和longClickable同時為false)。
  8. View的enable屬性不影響onTouchEvent的默認返回值躺枕。
  9. 事件傳遞過程是由外向內的服猪,事件總是先傳遞給父元素,然后再由父元素分發(fā)給子View拐云。

5. 從源碼的角度來看事件分發(fā)機制

很多我們需要探討的機制都離不開源碼的設計罢猪,從源碼的角度來看待問題,有助于加深理解叉瘩。

5.1 Activity 對點擊事件的分發(fā)過程

??點擊事件用MotionEvent來表示膳帕,當一個點擊事件發(fā)生的時候,最先傳遞給了Activity房揭,由Activity的dispatchTouchEvent來進行事件的分發(fā)备闲。具體的工作是有Activity內部的Window來完成。Window會將事件傳遞給decor view,decor view一般是當前界面的底層容器(即是setContentView 所設置的View的父容器)捅暴,通過Activity.getWindow.getDecorView()可以獲得。
——————————源碼 Activity # dispatchTouchEvent——————————

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

一般事件都是從ACTION_DOWN開始咧纠,所以這個if 返回的結果是true,接下來查看onUserInteraction()的源碼.

——————源碼 Activity # dispatchTouchEvent#onUserInteraction———————

 /**
     * Called whenever a key, touch, or trackball event is dispatched to the
     * activity.  Implement this method if you wish to know that the user has
     * interacted with the device in some way while your activity is running.
     * This callback and {@link #onUserLeaveHint} are intended to help
     * activities manage status bar notifications intelligently; specifically,
     * for helping activities determine the proper time to cancel a notfication.
     *
     * <p>All calls to your activity's {@link #onUserLeaveHint} callback will
     * be accompanied by calls to {@link #onUserInteraction}.  This
     * ensures that your activity will be told of relevant user activity such
     * as pulling down the notification pane and touching an item there.
     *
     * <p>Note that this callback will be invoked for the touch down action
     * that begins a touch gesture, but may not be invoked for the touch-moved
     * and touch-up actions that follow.
     *
     * @see #onUserLeaveHint()
     */
    public void onUserInteraction() {
    }

??呃蓬痒,你沒有看錯,這是一個空方法漆羔。查看一下注釋梧奢,當此activity在棧頂時,觸屏點擊按home演痒,back亲轨,menu鍵等都會觸發(fā)此方法。所以onUserInteraction()主要用于屏保鸟顺。
接下來再看看下一個方法superDispatchTouchEvent
——————源碼 Activity # dispatchTouchEvent#superDispatchTouchEvent——————

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
  return mDecor.superDispatchTouchEvent(event);
//mDecor是DecorView的實例
//DecorView是視圖的頂層view惦蚊,繼承自FrameLayout,是所有界面的父類
}

接下來看 mDecor.superDispatchTouchEvent(event)

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
//DecorView繼承自FrameLayout
//那么它的父類就是ViewGroup
而super.dispatchTouchEvent(event)方法讯嫂,其實就應該是ViewGroup的dispatchTouchEvent()

}

所以執(zhí)行了getWindow().superDispatchTouchEvent(ev) 蹦锋,就是執(zhí)行了ViewGroup的dispatchTouchEvent(event)。然后再回頭看源碼欧芽,
——————————源碼 Activity # dispatchTouchEvent——————————

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

??從事件MotionEvent.ACTION_DOWN開始莉掂,返回true,所以注定會返回到下一個if判斷中千扔,也就是getWindow().superDispatchTouchEvent(ev)的判斷中憎妙。所以執(zhí)行了Activity的dispatchTouchEvent()實際上就是執(zhí)行了ViewGroup的dispatchTouchEvent()方法库正。

再來捋捋順序:

  1. 首先我們在手指按下屏幕,事件最先傳遞到Activity的dispatchTouchEvent() 進行事件分發(fā)厘唾。
  2. 具體的工作由Window類的實現(xiàn)類PhoneView的superDispatchTouchEvent來完成诀诊。
  3. 調用DecorView的superDispatchTouchEvent。
  4. 最終調用DecorView的父類ViewGroup的dispatchTouchEvent()阅嘶,將事件分發(fā)到了ViewGroup属瓣。

5.2 ViewGroup 的事件分發(fā)機制

??上面我們分析了Activity將事件分到到ViewGroup了,接下來是對ViewGroup的分析讯柔。在Android 5.0 以后的源碼發(fā)生了改動,但是原理是相同的抡蛙,這里用5.0之前的源碼來分析。源碼太長了魂迄,我們分開了討論粗截。
——————————源碼 ViewGroup# dispatchTouchEvent——————————

public boolean dispatchTouchEvent(MotionEvent ev) {
    final int action = ev.getAction();
    final float xf = ev.getX();
    final float yf = ev.getY();
    final float scrolledXFloat = xf + mScrollX;
    final float scrolledYFloat = yf + mScrollY;
    final Rect frame = mTempRect;
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (action == MotionEvent.ACTION_DOWN) {
        if (mMotionTarget != null) {
            mMotionTarget = null;
        }

        if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            ev.setAction(MotionEvent.ACTION_DOWN);
            final int scrolledXInt = (int) scrolledXFloat;
            final int scrolledYInt = (int) scrolledYFloat;
            final View[] children = mChildren;
            final int count = mChildrenCount;

            for (int i = count - 1; i >= 0; i--) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);

                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                        if (child.dispatchTouchEvent(ev))  {
                            mMotionTarget = child;
                            return true;
                        }
                    }
                }
            }
        }
    }
    boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
            (action == MotionEvent.ACTION_CANCEL);
    if (isUpOrCancel) {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    final View target = mMotionTarget;



    if (target == null) {
        ev.setLocation(xf, yf);
        if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        }

        return super.dispatchTouchEvent(ev);
    }
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        ev.setAction(MotionEvent.ACTION_CANCEL);
        ev.setLocation(xc, yc);
        if (!target.dispatchTouchEvent(ev)) {
        }
        mMotionTarget = null;
        return true;
    }
    if (isUpOrCancel) {
        mMotionTarget = null;
    }
    final float xc = scrolledXFloat - (float) target.mLeft;
    final float yc = scrolledYFloat - (float) target.mTop;
    ev.setLocation(xc, yc);
    if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
        ev.setAction(MotionEvent.ACTION_CANCEL);
        target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
        mMotionTarget = null;
    }
    return target.dispatchTouchEvent(ev);
} 
5.2.1 關于onInterceptTouchEvent的分析

??ViewGroup在dispatchTouchEvent進行分發(fā)的時候,需要調用onInterceptTouchEvent()來判斷是否攔截捣炬。
——————————源碼 ViewGroup# onInterceptTouchEvent——————————

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

截取其中關于onInterceptTouchEvent的的判斷分析
——————————源碼 ViewGroup#dispatchTouchEvent#if ——————————

if (disallowIntercept || !onInterceptTouchEvent(ev)) {
            ev.setAction(MotionEvent.ACTION_DOWN);
            final int scrolledXInt = (int) scrolledXFloat;
            final int scrolledYInt = (int) scrolledYFloat;
            final View[] children = mChildren;
            final int count = mChildrenCount;
            for (int i = count - 1; i >= 0; i--) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                        if (child.dispatchTouchEvent(ev))  {
                            mMotionTarget = child;
                            return true;
                        }
                    }
                }
            }
        }
    }

??這個if判斷語句,第一個判斷值disallowIntercept:是否禁用事件攔截的功能(默認是false)熊昌,可以通過調用requestDisallowInterceptTouchEvent方法對這個值進行修改;所以onInterceptTouchEvent()的值決定了這個if循環(huán)能否繼續(xù)湿酸,當 值為flase時婿屹,!onInterceptTouchEvent(ev) 為true,從而進入條件的內部了。當值為true的時候推溃,!onInterceptTouchEvent(ev) 為false昂利,跳出了這個條件判斷。

再截取這個關于onInterceptTouchEvent的源碼判斷铁坎,當條件符合進入if內部的時候,遍歷ViewGroup中的子View
——————————源碼 ViewGroup# dispatchTouchEvent# if# for——————————

 for (int i = count - 1; i >= 0; i--) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                        if (child.dispatchTouchEvent(ev))  {
                            mMotionTarget = child;
                            return true;
                        }
                    }

??判斷當前遍歷的View是不是正在點擊的View蜂奸,如果是,再進入條件內部硬萍,這時候我們已經(jīng)進入子View的 if (child.dispatchTouchEvent(ev))中
——————————源碼 ViewGroup# dispatchTouchEvent# if# for#if——————————

        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                        || child.getAnimation() != null) {
                    child.getHitRect(frame);
                     //判斷當前遍歷的View是不是正在點擊的View
                    //如果是扩所,則進入條件判斷內部
                    if (frame.contains(scrolledXInt, scrolledYInt)) {
                        final float xc = scrolledXFloat - child.mLeft;
                        final float yc = scrolledYFloat - child.mTop;
                        ev.setLocation(xc, yc);
                        child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                         //進入到了子View層中了
                        if (child.dispatchTouchEvent(ev))  {
                            mMotionTarget = child;
                           return true;
                        }
                    }

??所以onInterceptTouchEvent()當值為flase 的時候,默認返回不攔截朴乖,繼續(xù)分發(fā)事件到子View中祖屏。
——————————源碼 ViewGroup# dispatchTouchEvent# if# for#if#if——————————

                  if (child.dispatchTouchEvent(ev))  {
                            mMotionTarget = child;
                            return true;
                        }

??到了這一步了,條件判斷子View的dispatchTouchEvent,實現(xiàn)了點擊事件從ViewGroup到View的分發(fā)傳遞寒砖,調用子View的dispatchTouchEvent是有返回值的赐劣,如果子View控件是可點擊的,子View可以消費事件哩都,那么點擊該子View的控件是魁兼,事件分發(fā)到子View的dispatchTouchEvent的值必定為true,所以該if條件判斷成立,所以進入條件內部 mMotionTarget = child。ViewGroup的dispatchTouchEvent,方法直接返回true,后面的代碼無法執(zhí)行咐汞,直接跳出去了盖呼,即把ViewGroup的touch事件攔截掉了。

最后我們捋捋順序:

  1. 首先Activity將事件分發(fā)到ViewGroup的dispatchTouchEvent進行事件分發(fā)化撕。
  2. 在ViewGroup的dispatchTouchEvent中几晤,我們通過獲取onInterceptTouchEvent()的值來判斷if循環(huán)是否繼續(xù),當if值為true的時候植阴,我們對ViewGroup中的子View進行遍歷蟹瘾。
  3. 當遍歷中的子View是我們點擊的View的時候,這時候ViewGroup就將事件分發(fā)到子View中掠手。
5.3 View的事件分發(fā)

??當ViewGroup將事件傳給了View之后憾朴,我們接下來對View的dispatchTouchEvent()事件進行處理。

——————————源碼 View# dispatchTouchEvent——————————

public boolean dispatchTouchEvent(MotionEvent event) {  
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
            mOnTouchListener.onTouch(this, event)) {  
        return true;  
    }  
    return onTouchEvent(event);  
}

??子View的條件判斷有三個:

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

只有三個條件都為真的時候喷鸽,dispatchTouchEvent()才返回true,接下來是對這個條件的判斷众雷。

  1. 條件一 : mOnTouchListener != null
public void setOnTouchListener(OnTouchListener l) { 
    mOnTouchListener = l;  
}

??mOnTouchListener是View類下的setOnTouchListener()方法賦值,只要給控件注冊了Touch事件做祝,mOnTouchListener 的值就一定不為空砾省。

  1. (mViewFlags & ENABLED_MASK) == ENABLED
    這個條件是判斷當前點擊的事件是否可點擊,很多View的默認條件是enable混槐,所以這個條件默認為true

  2. mOnTouchListener.onTouch(this, event)
    回調控件注冊Touch事件時的onTouch方法

button.setOnTouchListener(new OnTouchListener() {  

  @Override  
  public boolean onTouch(View v, MotionEvent event) {  
      return false;  
  }  
});

如果在onTouch方法返回true编兄,就會讓上述三個條件全部成立,從而整個方法直接返回true纵隔。
如果返回false,就會去執(zhí)行onTouchEvent(event)方法翻诉。

最后在捋捋順序:

  1. 在ViewGroup將事件分發(fā)到了View層之后,View層的dispatchTouchEvent對事件判斷是否需要消費掉捌刮,當三個條件都滿足的時候,事件直接消費掉了舒岸,不需要進行分發(fā)了绅作。不完全滿足時就會將事件傳遞到onTouchEvent中。

5.4 事件響應

同樣的蛾派,對onTouchEvent進行源碼分析俄认,onTouchEvent是事件響應,源碼主要是對一個switch進行判斷洪乍,也就是對我們的MotionEvent 分發(fā)事件的幾個基本動作進行處理眯杏。 源碼有點長,但是我們只要把關注點分別放在不同的動作要求上的時候壳澳,就比較好理解了岂贩。

public boolean onTouchEvent(MotionEvent event) {  
    final int viewFlags = mViewFlags;  
    if ((viewFlags & ENABLED_MASK) == DISABLED) {  
        // 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));  
    }  
    if (mTouchDelegate != null) {  
        if (mTouchDelegate.onTouchEvent(event)) {  
            return true;  
        }  
    }  
     //如果該控件是可以點擊的就會進入到下兩行的switch判斷中去;

    if (((viewFlags & CLICKABLE) == CLICKABLE ||  
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  
    //如果當前的事件是抬起手指巷波,則會進入到MotionEvent.ACTION_UP這個case當中萎津。

        switch (event.getAction()) {  
            case MotionEvent.ACTION_UP:  
                boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;  
               // 在經(jīng)過種種判斷之后卸伞,會執(zhí)行到關注點1的performClick()方法。
                if ((mPrivateFlags & PRESSED) != 0 || prepressed) {  
                    boolean focusTaken = false;  
                    if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {  
                        focusTaken = requestFocus();  
                    }  
                    if (!mHasPerformedLongPress) {  
                        removeLongPressCallback();  
                        if (!focusTaken) {  
                            if (mPerformClick == null) {  
                                mPerformClick = new PerformClick();  
                            }  
                            if (!post(mPerformClick)) {  
                          //          請往下看performClick()的源碼分析
                                performClick();  
                            }  
                        }  
                    }  
                    if (mUnsetPressedState == null) {  
                        mUnsetPressedState = new UnsetPressedState();  
                    }  
                    if (prepressed) {  
                        mPrivateFlags |= PRESSED;  
                        refreshDrawableState();  
                        postDelayed(mUnsetPressedState,  
                                ViewConfiguration.getPressedStateDuration());  
                    } else if (!post(mUnsetPressedState)) {  
                        // If the post failed, unpress right now  
                        mUnsetPressedState.run();  
                    }  
                    removeTapCallback();  
                }  
                break;  
            case MotionEvent.ACTION_DOWN:  
                if (mPendingCheckForTap == null) {  
                    mPendingCheckForTap = new CheckForTap();  
                }  
                mPrivateFlags |= PREPRESSED;  
                mHasPerformedLongPress = false;  
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                break;  
            case MotionEvent.ACTION_CANCEL:  
                mPrivateFlags &= ~PRESSED;  
                refreshDrawableState();  
                removeTapCallback();  
                break;  
            case MotionEvent.ACTION_MOVE:  
                final int x = (int) event.getX();  
                final int y = (int) event.getY();  
                // Be lenient about moving outside of buttons  
                int slop = mTouchSlop;  
                if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                        (y < 0 - slop) || (y >= getHeight() + slop)) {  
                    // Outside button  
                    removeTapCallback();  
                    if ((mPrivateFlags & PRESSED) != 0) {  
                        // Remove any future long press/tap checks  
                        removeLongPressCallback();  
                        // Need to switch from pressed to not pressed  
                        mPrivateFlags &= ~PRESSED;  
                        refreshDrawableState();  
                    }  
                }  
                break;  
        }  
//如果該控件是可以點擊的锉屈,就一定會返回true
        return true;  
    }  
//如果該控件是可以點擊的荤傲,就一定會返回false
    return false;  
}

注意看一下當MotionEvent_ACTION_UP ,手指抬起時,里面有很多的判斷颈渊,最后有一個performClick()方法遂黍,這個方法的源碼再看看。

public boolean performClick() {  
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);  
    if (mOnClickListener != null) {  
        playSoundEffect(SoundEffectConstants.CLICK);  
        mOnClickListener.onClick(this);  
        return true;  
    }  
    return false;  
}

只要mOnClickListener不為null俊嗽,就會去調用onClick方法雾家。mOnClickListener的源碼如下:

public void setOnClickListener(OnClickListener l) {  
    if (!isClickable()) {  
        setClickable(true);  
    }  
    mOnClickListener = l;  
}

當我們通過調用setOnClickListener方法來給控件注冊一個點擊事件時,就會給mOnClickListener賦值(不為空)乌询,即會回調onClick()榜贴,最終消費事件。

6. 事件分發(fā)機制總結

事件分發(fā)圖

因為縮小了圖妹田,怕看不清唬党,所以用大圖來看了。

下面是一個總體概括:

  1. 事件由Activity的dispatchTouchEvent()開始鬼佣,將事件傳遞給當前的Activity的根ViewGroup:mDecorView驶拱,事件自上而下傳遞,直到被消費晶衷。

  2. 事件分發(fā)到ViewGroup時蓝纲,調用dispatchTouchEvent()進行分發(fā)處理。首先會被ViewGroup的onInterceptTouchEvent()攔截晌纫。如果onInterceptTouchEvent 返回false税迷,則開始遍歷ViewGroup中的子View,將事件依次發(fā)給子View,若事件被某個子View消費了锹漱,將不再繼續(xù)分發(fā)箭养;如果onInterceptTouchEvent返回true,事件由ViewGroup自己處理哥牍。ViewGroup通過調用子View中的mOnTouchLisenter事件得到onTouchEvent的返回值毕泌。當這個返回值為true時,自己消費嗅辣;否則將事件回傳到Activity中撼泛,最后事件結束。

  3. 當事件分發(fā)到View層的時候澡谭,事件傳遞到View的dispatchTouchEvent() 愿题,首先會判斷OnTouchListener是否存在,倘若存在,則執(zhí)行onTouch()抠忘,若onTouch()未對事件進行消費撩炊,事件將繼續(xù)交由onTouchEvent處理,根據(jù)上面分析可知崎脉,View的onClick事件是在onTouchEvent的ACTION_UP中觸發(fā)的拧咳,因此,onTouch事件優(yōu)先于onClick事件囚灼。

  4. 事件在自上而下的傳遞過程中一直沒有被消費骆膝,而且最底層的子View也沒有對其進行消費,事件會反向向上傳遞灶体,此時阅签,父ViewGroup可以對事件進行消費,若仍然沒有被消費的話蝎抽,最后會回到Activity的onTouchEvent政钟。

參考文章:
http://allenfeng.com/2017/02/22/android-touch-event-transfer-mechanism/
http://www.reibang.com/p/38015afcdb58

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市樟结,隨后出現(xiàn)的幾起案子养交,更是在濱河造成了極大的恐慌,老刑警劉巖瓢宦,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件碎连,死亡現(xiàn)場離奇詭異,居然都是意外死亡驮履,警方通過查閱死者的電腦和手機鱼辙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來玫镐,“玉大人倒戏,你說我怎么就攤上這事】炙疲” “怎么了峭梳?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蹂喻。 經(jīng)常有香客問我,道長捂寿,這世上最難降的妖魔是什么口四? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮秦陋,結果婚禮上蔓彩,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好赤嚼,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布旷赖。 她就那樣靜靜地躺著,像睡著了一般更卒。 火紅的嫁衣襯著肌膚如雪等孵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天蹂空,我揣著相機與錄音俯萌,去河邊找鬼。 笑死上枕,一個胖子當著我的面吹牛咐熙,可吹牛的內容都是我干的。 我是一名探鬼主播辨萍,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼棋恼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了锈玉?” 一聲冷哼從身側響起爪飘,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎嘲玫,沒想到半個月后悦施,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡去团,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年抡诞,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片土陪。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡昼汗,死狀恐怖,靈堂內的尸體忽然破棺而出鬼雀,到底是詐尸還是另有隱情顷窒,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布源哩,位于F島的核電站鞋吉,受9級特大地震影響,放射性物質發(fā)生泄漏励烦。R本人自食惡果不足惜谓着,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望坛掠。 院中可真熱鬧赊锚,春花似錦治筒、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至牲平,卻和暖如春堤框,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背欠拾。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工胰锌, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人藐窄。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓资昧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親荆忍。 傳聞我的和親對象是個殘疾皇子格带,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內容