第六章 Android 開發中的View和事件分發機制

1. 概述

??作為Android開發中最常見的一個控件,個人覺得有必要談談了。我們剛開始接觸Android的時候最常見的一些基本控件就有TextView,EditText,Button......,但是細心的你會發現它們的父類都是View。但是View也不能單純是一個控件,它應該算是一個體系。我們在進行Android開發的時候,有時候一些系統提供的控件不能滿足我們的需求,這時候我們需要對進行自定義控件的編寫,定制符合我們要求的控件,這樣才能滿足我們的功能需求。
??在介紹View的時候,首先想提供這樣一個知識路線圖,View的基礎概念,讓我們知道什么是View;View的位置參數,了解View的移動;View的觸碰事件,了解事件分發機制;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;再舉個栗子,我們知道數學中的集合,一個集合中有許多子集合,但同時,這些子集合也包含一些更小集合。子集合就相當于自己是一個View,并且還是一個ViewGroup。所以說ViewGroup內部是可以有子View的,子View同樣還可以是ViewGroup,以此類推。這種層級關系有助于我們了解View的結構機制。

3. View的位置參數

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

坐標方向

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

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

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

width = right - left
height = bottom - top

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

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

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

x= left + translationX
y = top + translationY

注意:當View發生平移的時候,top和left表示的是原始左上角的位置信息,它的值不會發生改變,此時發生改變的是x,y,translationX,translationY這四個參數。這樣我們就可以通過了解View的位置以及偏移量來了解它的運動軌跡。

4. View的事件分發

4.1 典型的分發事件

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

  • ACTION_DOWN ——手指剛接觸屏幕

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

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

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

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

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

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

4.2 View的事件分發機制

??點擊事件的事件分發,其實是對MotionEvent事件的分發的過程。即當一個MotionEvent產生以后,系統需要把這個事件傳遞給一個具體的View,這個傳遞的過程其實就是分發過程。點擊事件的分發過程由三個很重要的方法來共同完成。

public boolean dispatchTouchEvent(MotionEvent ev)

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

public boolean onInterceptTouchEvent(MotionEvent ev)

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

  1. 如果 onInterceptTouchEvent 返回 true,則表示將事件進行攔截,并將攔截到的事件交由當前 View 的 onTouchEvent 進行處理;
  2. 如果 onInterceptTouchEvent 返回 false,則表示將事件放行,當前 View 上的事件會被傳遞到子 View 上,再由子 View 的 dispatchTouchEvent 來開始這個事件的分發;
  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。

即隧道式向下分發,然后冒泡式處理

??onInterceptTouchEvent用于改變事件的傳遞方向。決定傳遞方向的是返回值,返回為false時事件會傳遞給子控件,返回值為true時事件會傳遞給當前控件的onTouchEvent(),這就是所謂的Intercept(攔截)。
??正確的使用方法是,在此方法內僅判斷事件是否需要攔截,然后返回。即便需要攔截也應該直接返回true,然后由onTouchEvent方法進行處理。
??onTouchEvent用于處理事件,返回值決定當前控件是否消費(consume)了這個事件。尤其對于ACTION_DOWN事件,返回true,表示我想要處理后續事件;返回false,表示不關心此事件,并返回由父類進行處理。

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

Touch事件相關方法 方法功能 ViewGroup View Activity
dispatchTouchEvent(MotionEvent ev) 事件分發 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 已經是一個最小的單元 View(比如 TextView),那么就無法向這個最小 View 中添加子 View,也就無法向子 View 進行事件的攔截,所以它沒有 onInterceptTouchEvent(MotionEvent ev)。

事件分發機制圖解

  • 從上圖所示中,事件的分發機制分為3層,分別是Activity,ViewGroup,View。
  • 事件的返回值分別為 return false ,true,super.xxxx。super是調用父類實現的意思。
  • 事件的分發機制是從左上角的ACTION_DOWN開始的,由Activity的dispatchTouchEvent()開始分發
  • 在dispatchTouchEvent() 和onTouchEvent()中,return true ,代表事件傳遞到這里就消費掉了,事件不是再進行傳遞了
  • 在Activity中的dispatchTouchEvent()中,只有傳遞過來super ,才能繼續向下分發事件。除此外return true/false都表示事件被消費掉了。

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

左邊是向下事件分發的理解:

第一層是Activity層,Activity層 return true/ false消費事件,return super 將事件分發到了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形圖事件分發

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

  1. 同一個事件序列是指從手指接觸屏幕開始,到手指離開屏幕結束,在這個過程中產生的一系列事件,就是以down事件開始,中間是許多個move事件,最終以up事件結束。
  2. 正常情況下,一個事件序列只能被一個View攔截消耗,一旦一個元素攔截了某個事件,那么這個事件序列中的所有事件都會直接交給它處理。
  3. 事件一旦交給一個View處理,那么它就必須消耗掉。在它還沒有消耗掉該事件之前,那么同一事件中的剩余事件就不再交給它處理。
  4. 如果View不消耗除ACTION_DOWN以外的其他事件,那么這個點擊事件會消失,此時父元素的onTouchEvent 并不會被調用,并且當前Vie可以持續受到后續的事件,最終這些消失的點擊事件會傳遞給Activity處理。
  5. ViewGroup 默認不攔截事件,Android 源碼中的ViewGroup 的onInterceptTouchEvent()方法默認返回false。
  6. View 沒有onInterceptTouchEvent 方法,一旦事件傳遞給他了,那么它的onTouchEvent方法就會被調用。
  7. View 的onTouchEvent 默認會消費事件,默認返回true,除非是不可點擊(clickable和longClickable同時為false)。
  8. View的enable屬性不影響onTouchEvent的默認返回值。
  9. 事件傳遞過程是由外向內的,事件總是先傳遞給父元素,然后再由父元素分發給子View。

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

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

5.1 Activity 對點擊事件的分發過程

??點擊事件用MotionEvent來表示,當一個點擊事件發生的時候,最先傳遞給了Activity,由Activity的dispatchTouchEvent來進行事件的分發。具體的工作是有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鍵等都會觸發此方法。所以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()

}

所以執行了getWindow().superDispatchTouchEvent(ev) ,就是執行了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)的判斷中。所以執行了Activity的dispatchTouchEvent()實際上就是執行了ViewGroup的dispatchTouchEvent()方法。

再來捋捋順序:

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

5.2 ViewGroup 的事件分發機制

??上面我們分析了Activity將事件分到到ViewGroup了,接下來是對ViewGroup的分析。在Android 5.0 以后的源碼發生了改動,但是原理是相同的,這里用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進行分發的時候,需要調用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循環能否繼續,當 值為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,如果是,再進入條件內部,這時候我們已經進入子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 的時候,默認返回不攔截,繼續分發事件到子View中。
——————————源碼 ViewGroup# dispatchTouchEvent# if# for#if#if——————————

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

??到了這一步了,條件判斷子View的dispatchTouchEvent,實現了點擊事件從ViewGroup到View的分發傳遞,調用子View的dispatchTouchEvent是有返回值的,如果子View控件是可點擊的,子View可以消費事件,那么點擊該子View的控件是,事件分發到子View的dispatchTouchEvent的值必定為true,所以該if條件判斷成立,所以進入條件內部 mMotionTarget = child。ViewGroup的dispatchTouchEvent,方法直接返回true,后面的代碼無法執行,直接跳出去了,即把ViewGroup的touch事件攔截掉了。

最后我們捋捋順序:

  1. 首先Activity將事件分發到ViewGroup的dispatchTouchEvent進行事件分發。
  2. 在ViewGroup的dispatchTouchEvent中,我們通過獲取onInterceptTouchEvent()的值來判斷if循環是否繼續,當if值為true的時候,我們對ViewGroup中的子View進行遍歷。
  3. 當遍歷中的子View是我們點擊的View的時候,這時候ViewGroup就將事件分發到子View中。
5.3 View的事件分發

??當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,就會去執行onTouchEvent(event)方法。

最后在捋捋順序:

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

5.4 事件響應

同樣的,對onTouchEvent進行源碼分析,onTouchEvent是事件響應,源碼主要是對一個switch進行判斷,也就是對我們的MotionEvent 分發事件的幾個基本動作進行處理。 源碼有點長,但是我們只要把關注點分別放在不同的動作要求上的時候,就比較好理解了。

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;  
               // 在經過種種判斷之后,會執行到關注點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. 事件分發機制總結

事件分發圖

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

下面是一個總體概括:

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

  2. 事件分發到ViewGroup時,調用dispatchTouchEvent()進行分發處理。首先會被ViewGroup的onInterceptTouchEvent()攔截。如果onInterceptTouchEvent 返回false,則開始遍歷ViewGroup中的子View,將事件依次發給子View,若事件被某個子View消費了,將不再繼續分發;如果onInterceptTouchEvent返回true,事件由ViewGroup自己處理。ViewGroup通過調用子View中的mOnTouchLisenter事件得到onTouchEvent的返回值。當這個返回值為true時,自己消費;否則將事件回傳到Activity中,最后事件結束。

  3. 當事件分發到View層的時候,事件傳遞到View的dispatchTouchEvent() ,首先會判斷OnTouchListener是否存在,倘若存在,則執行onTouch(),若onTouch()未對事件進行消費,事件將繼續交由onTouchEvent處理,根據上面分析可知,View的onClick事件是在onTouchEvent的ACTION_UP中觸發的,因此,onTouch事件優先于onClick事件。

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,283評論 6 530
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,947評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,094評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,485評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,268評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,817評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,906評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,039評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,551評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,502評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,662評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,188評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,907評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,304評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,563評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,255評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,637評論 2 370

推薦閱讀更多精彩內容