View的事件分發機制

前言

前幾天寫過一篇文章View的工做原理,講述的View工做的三大流程,其實與View的工做流程一樣重要還有View的事件分發機制,平時咱們常常經過setOnClickListener()方法來設置一個View的點擊監聽,那你有沒有想過這個點擊事件底層是怎麼樣傳遞到這個View的呢?當你自定義控件時,若是要處理滑動事件,那麼到底返回true仍是false?還有當你遇到了滑動嵌套的情景,你要怎麼解決滑動嵌套引發的衝突?因此,本文經過 源碼 + 流程圖 來深刻了解一個事件分發機制,當你掌握了它以後,當你遇到與滑動相關的問題時就更加的遊刃有餘。java

本文源碼基於Android8.0框架

準備知識

一、什麼是觸摸事件

觸摸事件事件就是當你的手觸摸到手機屏幕時所產生的最小單元事件,所謂最小單元,就是不可再拆分的,它通常有4種類型:按下(down)、移動(move)、擡起(up)、取消(cancel)。而後由若干個不可再拆分的最小單元事件就組成了點擊事件、長按事件、滑動事件等。ide

二、什麼是MotionEvent

MotionEvent就是Android對上面觸摸事件相關信息的封裝,View的事件分發中的事件就是這個MotionEvent,當這個MotionEvent產生後,那麼系統就會將這個MotionEvent傳遞給View的層級,MotionEvent在View的層級傳遞的過程就是事件分發。MotionEvent封裝了事件類型和座標兩類信息。post

事件類型能夠經過 motionEvent.getAction() 方法得到,它返回一個常量,對應着一個事件類型,事件類型主要有如下4種:動畫

//MotionEvent.java
public final class MotionEvent extends InputEvent implements Parcelable {
    //按下(down)
    public static final int ACTION_DOWN             = 0;
	//擡起(up)
    public static final int ACTION_UP               = 1;
    //移動(move)
    public static final int ACTION_MOVE             = 2;
    //取消(cancel)
    public static final int ACTION_CANCEL           = 3;
    //還有不少就不一 一列舉
    //...
}
複製代碼

座標信息也是經過MotionEvent獲取,motionEvent.getRawX()、motionEvent.getRawY() 能夠得到以屏幕做爲參考系的座標值,motionEvent.getX()、motionEvent.getY() 能夠得到以被觸摸的 View 做爲參考系的座標值。參考下面的視圖座標:this

藍色點想象成手指觸摸屏幕的位置。spa

三、一個事件序列

從手指按下屏幕到擡起,在這個過程當中所產生的一系列事件,就是一個事件序列,這個事件序列以down事件開始,中間含有數量不定的move事件,最終以up事件結束。因此可能會有下面兩種事件序列:.net

  • ACTION_DOWN -> ACTION_UP:手指按下屏幕後又擡起
  • ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_MOVE -> ACTION_UP:手指按下屏幕,滑動一會,而後擡起

在分析事件分發的過程時,會有事件序列這個概念。代理

四、事件分發的起點,事件從何而來

我想你們都知道View的事件分發機制的起點是View的dispatchTouchEvent()方法,可是若是從View的dispatchTouchEvent()繼續追溯上去,事件是從哪裏來的呢?code

Android的輸入設備有不少種,如屏幕、鍵盤、鼠標、軌跡球等,而屏幕是咱們接觸最多的設備,當用戶手指觸摸屏幕時就會產生觸摸事件,這時Android的輸入系統就會爲這個觸摸事件在/dev/input/路徑下寫入以event[NUMBER]爲名的輸入設備節點,這時輸入系統中的EventHub就會監聽到這個輸入事件,而後InputReader就會把這個原始輸入事件讀取並通過加工後交給輸入系統中的InputDispatcher,InputDispatcher會在Window列表中會找到合適的Window,而後把這個輸入事件分發給合適的Window,而後Window就會把這個事件分發給頂級View,而後頂級View就把這個輸入事件在View樹中層層分發下去,直到找到合適的View來處理這個事件,這來到了咱們熟悉的View的事件分發機制

上面的一些名詞如EventHub、InputReader、InputReader都是屬於Android的輸入系統,這部分是一個很複雜的知識,我只是歸納了一下。因此咱們只要知道,輸入系統監聽到輸入事件後,就會先交給Window,而後Window再交給頂級View,而後頂級View在把它分發下去。(關於Window和View的關係能夠看這篇文章Window, WindowManager和WindowManagerService之間的關係)

這個頂級View多是View,也有多是ViewGroup,具體狀況看你添加Window到WMS時你的addView(View view, ViewGroup.LayoutParams params)方法中的View是View實例仍是ViewGroup實例,因此本文接下來就分別分析View的事件分發和ViewGroup的事件分發。

View的事件分發

一、View::dispatchTouchEvent()

View的事件分發比ViewGroup的簡單,由於它只是一個單獨的元素,因此它只須要處理本身的事件,View的事件分發從View的dispatchTouchEvent()方法開始,因此咱們看它的dispatchTouchEvent方法,以下:

//View.java
public boolean dispatchTouchEvent(MotionEvent event) {
    //...
    //result默認爲false
    boolean result = false;
    //...
     ListenerInfo li = mListenerInfo;
    if (
        li != null//若是ListenerInfo不爲空
        && li.mOnTouchListener != null//若是觸摸事件的監聽不爲空
        && (mViewFlags & ENABLED_MASK) == ENABLED//若是該控件是ENABLED狀態
        && li.mOnTouchListener.onTouch(this, event)//若是onTouch方法返回了true
    ){
        result = true;
    }
    if (
        !result//若是上面四個條件都不知足,result默認爲false
        && onTouchEvent(event)//若是onTouchEvent()方法返回了true
    ) {
        result = true;
    }
    //...
    return result;
}

//View.java
static class ListenerInfo {
     public OnClickListener mOnClickListener;//點擊事件的監聽
     protected OnLongClickListener mOnLongClickListener;//長按事件的監聽
     private OnTouchListener mOnTouchListener;//觸摸事件的監聽
    //...
}
複製代碼

從View的dispatchTouchEvent()方法的僞代碼能夠看出,dispatchTouchEvent()方法首先會根據4個條件來決定是否調用View的onTouchEvent方法,以下:

  • 一、若是ListenerInfo不爲空:ListenerInfo裏面有View的各類監聽,那麼mListenerInfo是何時被賦值的呢?答案是給View設置監聽的時候,在咱們給View設置任何監聽的時候,若是這個mListenerInfo還沒初始化就會先初始化,好比設置觸摸事件的監聽,咱們看setOnTouchListener()方法,以下:

    //View.java
    public void setOnTouchListener(OnTouchListener l) {
        //先調用getListenerInfo方法初始化mListenerInfo,而後把觸摸事件的監聽賦值給mOnTouchListener
        getListenerInfo().mOnTouchListener = l;
    }
    
    //View.java
    ListenerInfo getListenerInfo() {
          if (mListenerInfo != null) {
              return mListenerInfo;
          }
          mListenerInfo = new ListenerInfo();
          return mListenerInfo;
      }
    複製代碼
  • 二、若是觸摸事件的監聽不爲空:即ListenerInfo的mOnTouchListener不爲空,從1能夠看出,當你給View設置OnTouchListener時,就已經知足了一、2條件了。

  • 三、若是該控件是ENABLED狀態:即該Vew處於enable狀態,若是你沒有手動調用過View的setEnable(false)設置控件爲不可用的話,這個條件就爲true,控件默認爲enable狀態。

  • 四、若是onTouch方法返回了true:當你給View設置OnTouchListener,而且在onTouch方法中返回了true,表示消費了此次事件,那麼這個條件就爲true。因此到這裏,若是4個條件都知足的話,result就會等於true,就會致使下面沒法調用View的onTouchEvent()方法。

可是若是你沒有給你給View設置OnTouchListener或者你給View設置了OnTouchListener,可是onTouch方法返回了false,只要知足這兩個條件之一,就會讓result保持默認值false,從而知足下面的條件調用View的onTouchEvent()方法。這裏得出一個結論:OnTouchListener的onTouch方法的優先級高於onTouchEvent()方法

假設如今不知足上面4個條件,從而調用View的onTouchEvent()方法,咱們來看View的onTouchEvent()方法。

二、View::onTouchEvent()

onTouchEvent()方法裏面會處理View點擊事件、長按事件,即回調你設置的OnClickListener的onClick()方法和OnLongClickListener的OnLongClick()方法,在你設置OnClickListener或OnLongClickListener回調時會同時把你的View設置爲可點擊狀態即clickable狀態,有些控件默承認點擊如Button,而有些控件須要設置點擊回調或setClickable(true)才能夠點擊如TextView。

接下來咱們看View的onTouchEvent()方法的主要源碼,以下:

//View.java
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是否可點擊,能夠看到這裏點擊包含3種點擊:CLICKABLE、LONG_CLICKABLE和CONTEXT_CLICKABLE(回調OnContextClickListener)
    //這裏咱們關注CLICKABLE和LONG_CLICKABLE就行,即點擊和長按
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    //一、若是View處於disabled狀態,即不可用狀態
    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
        //這裏說明了即便View處於不可用狀態,可是若是它能夠點擊,它仍是會消費點擊事件
        return clickable;
    }
    //二、若是View設置有代理機制,那麼就會執行TouchDelegate的onTouchEvent()方法
    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }
    //三、下面是onTouchEvent()對點擊事件和長按事件的處理
    //若是控件能夠點擊
    if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                //...
                break;
            case MotionEvent.ACTION_DOWN:
                //...
                break;

            case MotionEvent.ACTION_CANCEL:
               //...
                break;
            case MotionEvent.ACTION_MOVE:
               //...
                break;
        }
        //從這裏看出,若是咱們的View是能夠點擊的,最終必定返回true,表示消費了此事件
        return true;
    }
    //四、最終雖然控件可用,可是不可點擊,返回false,不消費此事件
    return false;
}
複製代碼

這個onTouchEvent()方法有點長,這裏我截取了總體框架,這裏咱們先明確一點的是onTochEvent()中返回true就表示這個事件由這個View消費,返回false就表示這個View不消費這個事件而後它的父容器會繼續找合適的View消費。首先咱們看註釋1,它說明了即便View處於不可用狀態,可是若是它能夠點擊即clickable = true,它會返回true,代表不可用狀態下的View它仍是會消費事件,即便這個View會沒有響應,反之返回false;接着註釋2,若是設置了mTouchDelegate,則會將事件交給代理者處理,直接return true,若是你們但願本身的View增長它的touch範圍,能夠嘗試使用TouchDelegate;接着註釋3,若是控件能夠點擊,就判斷事件類型:ACTION_UP、ACTION_DOWN、ACTION_CANCEL、ACTION_MOVE,而後根據不一樣的事件類型作出不一樣的行爲,而後都返回了true,表示消費了此事件;最後註釋4若是控件不可點擊,就返回false,不消費此事件。

接下來咱們重點看註釋3,看onTouchEvent()是如何在ACTION_UP、ACTION_DOWN、ACTION_CANCEL、ACTION_MOVE中觸發onClick()和onLingClick()回調的。

2.一、case ACTION_DOWN:

switch (action) {
      case MotionEvent.ACTION_DOWN:
          //...
          //一、設置mHasPerformedLongPress爲false
          mHasPerformedLongPress = false;
          //...
          //二、給mPrivateFlags設置一個PREPRESSED標識
           mPrivateFlags |= PFLAG_PREPRESSED;
          //三、經過postDelayed發送一個延時100毫秒後執行的任務mPendingCheckForTap
          postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
          //...
          break;
  }
return true;
複製代碼

咱們想象一下,咱們的手指按下這個View,這時進入ACTION_DOWN分支,在這個分支裏,在註釋1中它首先設置mHasPerformedLongPress爲false,表示長按事件尚未觸發,而後在註釋2給mPrivateFlags設置一個PREPRESSED的標識,表示開始檢查長按事件,而後在註釋3經過postDelayed發送了一個延時消息,ViewConfiguration.getTapTimeout()返回100毫秒,即100毫秒後會執行任務mPendingCheckForTap,它一個CheckForTap類型任務,它是用來檢測長按事件的。咱們看這個任務是什麼,以下:

//View.java 
//用來檢測長按事件
private final class CheckForTap implements Runnable {
        public float x;
        public float y;

        @Override
        public void run() {
            //一、mPrivateFlags清除PFLAG_PREPRESSED標識
            mPrivateFlags &= ~PFLAG_PREPRESSED;
            //二、見下面調用鏈,這裏傳入true,即給mPrivateFlags設置一個PFLAG_PRESSED標識
            setPressed(true, x, y);
            //三、調用checkForLongClick()方法,傳入100毫秒
            checkForLongClick(ViewConfiguration.getTapTimeout(), x, y);
        }
    }

private void setPressed(boolean pressed, float x, float y) {
        //...
        //主要調用了帶一個參數的setPressed(pressed)方法
        setPressed(pressed);
    }

//設置控件是否處於按下狀態
public void setPressed(boolean pressed) {
    //...
    if (pressed) {//若是pressed爲true
        //給mPrivateFlags設置一個PFLAG_PRESSED標識
        mPrivateFlags |= PFLAG_PRESSED;
    } else {//若是pressed爲false
        //清除mPrivateFlags以前設置的PFLAG_PRESSED標識
        mPrivateFlags &= ~PFLAG_PRESSED;
    }
   //...
}

複製代碼

mPendingCheckForTap的run方法裏面在註釋1首先會先清除mPrivateFlags中PFLAG_PREPRESSED標識,而後在註釋2設置PFLAG_PRESSED標識,表示準備執行長按事件,最主要的是註釋3,咱們看checkForLongClick方法裏面幹了什麼,以下:

//View.java
private void checkForLongClick(int delayOffset, float x, float y) {
    //一、檢查mViewFlags,若是能夠進行長按事件LONG_CLICKABLE
    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
        //此時mHasPerformedLongPress標誌位仍是false
        mHasPerformedLongPress = false;
        if (mPendingCheckForLongPress == null) {
            //二、建立了一個CheckForLongPress類型的任務
            mPendingCheckForLongPress = new CheckForLongPress();
        }
        //...
        //三、ViewConfiguration.getLongPressTimeout()返回500毫秒,再減100毫秒等於400毫秒
        //經過postDelayed()發送延時400毫秒後執行的任務mPendingCheckForLongPress
        postDelayed(mPendingCheckForLongPress,
                    ViewConfiguration.getLongPressTimeout() - delayOffset);
    }
}
複製代碼

這個方法在註釋1首先檢測該View是否能夠進行長按事件,View的LONG_CLICKABLE屬性默認爲false,可是在setOnLongClickListener()時就會把它設置爲true,而後在註釋2建立了一個CheckForLongPress類型的任務,而後在註釋3經過postDelayed()發送了一個延時消息,即400毫秒後執行mPendingCheckForLongPress任務,它是用來執行長按事件的,咱們看這個任務的具體實現,以下:

//View.java
//用來執行長按事件
private final class CheckForLongPress implements Runnable {
        private float mX;
        private float mY;

        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed())//一、首先檢查mPrivateFlags中是否清除了PFLAG_PRESSED標識,若是清除了表示長按事件取消
                && (mParent != null)
                && mOriginalWindowAttachCount == mWindowAttachCount) {
                //二、調用performLongClick()方法
                if (performLongClick(mX, mY)) {
                    //三、設置mHasPerformedLongPress爲true
                    mHasPerformedLongPress = true;
                }
            }
           //...
            if (performLongClick(mX, mY)) {
                mHasPerformedLongPress = true;
            }
        }
	//...
}

public boolean isPressed() {
    //返回true表示檢測mPrivateFlags中清除了PFLAG_PRESSED標識,false反之
    return (mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED;
}
複製代碼

CheckForLongPress就是用於執行長按事件的,它的run方法裏面會先檢查mPrivateFlags中是否清除了PFLAG_PRESSED標識,若是清除了就表示長按事件取消,不然就調用performLongClick()方法,裏面會最終回調onLongClick()方法回調,若是performLongClick()返回true,就會設置mHasPerformedLongPress爲true,不然mHasPerformedLongPress仍是爲false,即mHasPerformedLongPress是否爲true取決performLongClick(float x, float y)是否返回true,接下來咱們看performLongClick(float x, float y)方法,以下:

//View.java
public boolean performLongClick(float x, float y) {
       //...
        final boolean handled = performLongClick();
        //...
        return handled;
    }

public boolean performLongClick() {
    return performLongClickInternal(mLongClickX, mLongClickY);
}

private boolean performLongClickInternal(float x, float y) {
    boolean handled = false;
    final ListenerInfo li = mListenerInfo;
    //若是設置了OnLongClickListener回調
    if (li != null && li.mOnLongClickListener != null) {       
        //回調OnLongClickListener的onLongClick方法
        handled = li.mOnLongClickListener.onLongClick(View.this);
    }
    //...
    return handled;
}

複製代碼

在這個方法中一路跟進,最終來到了performLongClickInternal(float x, float y)方法中,在performLongClickInternal()方法中,若是咱們經過setOnLongClickListener()設置了OnLongClickListener回調,這裏就會回調咱們熟悉的onLongClick()方法,而performLongClickInternal()是否返回true取決於咱們在onLongClick()方法中是否返回true,performLongClick()是否返回true取決於performLongClickInternal()是否返回true,而後這裏結合上面的黑體字得出一個結論:若是你設置了onLongClickListener,mHasPerformedLongPress是否爲true取決咱們在onLongClick()方法中是否返回true,若是沒有設置,mHasPerformedLongPress就一直爲false,這個mHasPerformedLongPress是否爲true會影響咱們在ACTION_UP是否可以回調onClick()方法的關鍵。

如今咱們經過case ACTION_DOWN知道:若是咱們按下手指在500毫秒內沒有擡起,就會觸發長按事件。下面分析ACTION_UP。

2.二、case ACTION_UP:

switch (action) {
    case MotionEvent.ACTION_UP:
        //...
        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
        //一、若是mPrivateFlags中包含PFLAG_PRESSED或PFLAG_PREPRESSED標識,都會進入if分支
        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
           //...
            if (prepressed) {//若是mPrivateFlags中只包含PFLAG_PREPRESSED標識,表示用戶在100毫秒內擡起了手指,還沒執行CheckForTap任務
                //二、這裏傳入爲true,即給mPrivateFlags設置一個PFLAG_PRESSED標識
                //這裏主要讓用戶看到控件仍是按下狀態
                setPressed(true, x, y);
            }
		   //三、這個mHasPerformedLongPress爲false就進入if分支,mIgnoreNextUpEvent默認爲false
            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                //四、移除長按事件CheckForLongPress任務消息,即取消長按事件
                removeLongPressCallback();
                //...
                if (mPerformClick == null) {
                    //五、若是mPerformClick爲null,初始化一個實例
                    mPerformClick = new PerformClick();
                }
                //六、經過Handler把mPerformClick添加到消息隊列,但其實PerformClick中的run方法仍是執行performClick()方法,因此咱們只要看performClick()方法就行
                if (!post(mPerformClick)) {
                    //若是post一個PerformClick失敗就執行performClick()方法
                    performClick();
                }
            }
            //...
            //七、移除檢測長按事件CheckForTap任務消息,即取消檢測長按事件
            removeTapCallback();
        }
        break;
}
return true;

//View.java
//PerformClick中的run方法仍是執行performClick()方法
 private final class PerformClick implements Runnable {
     @Override
     public void run() {
         performClick();
     }
 }
複製代碼

咱們想象一下,如今咱們擡起了手指,分三個時間段擡起:

  • 一、若是你在100毫秒內擡起手指,那麼mPrivateFlags確定只有PFLAG_PREPRESSED標識,且mHasPerformedLongPress爲false,根據註釋1和3,這樣就會執行PerformClick()方法,在執行PerformClick()方法前,在註釋4調用removeLongPressCallback()移除長按事件CheckForLongPress任務,即不會觸發onLongClick()回調。

  • 二、若是你在100毫秒後到500毫秒才擡起,那麼mPrivateFlags確定只有PFLAG_PRESSED標識,且mHasPerformedLongPress爲false,接下來的邏輯和1同樣。

  • 三、若是你在500毫秒後才擡起,那麼mPrivateFlags確定只有PFLAG_PRESSED標識,而mHasPerformedLongPress是否爲true取決咱們是否設置onLongClickListener並在onLongClick()方法中是否返回true。若是你設置了onLongClickListener回調並在onLongClick()方法中返回了false或者你沒有設置onLongClickListener回調,那麼你仍是能夠走到註釋6執行performClick()方法;可是若是你設置了onLongClickListener回調並在onLongClick()方法中返回了true,那麼你就不能執行performClick()方法了。

對照ACTION_DOWN的流程和ACTION_UP的流程就能更好的理解上面3個時間段,因此從這裏咱們知道:若是你在500毫秒內擡起手指,那麼你就只能執行點擊事件,不能執行長按事件;若是你在500毫秒後擡起,而且你設置了onLongClickListener並在onLongClick()方法中返回了false 或者 你沒有設置onLongClickListener回調,那麼你執行完長按事件後還能夠執行點擊事件,可是若是你設置了onLongClickListener回調並在onLongClick()方法中返回了true,那麼你就不能執行點擊事件。performClick()和 performLongClick()方法相似,它裏面最終回調onClick()方法,以下:

//View.java
public boolean performClick() {
     final boolean result;
     final ListenerInfo li = mListenerInfo;
     if (li != null && li.mOnClickListener != null) {
         //一、執行了OnClickListener的onClick()方法
         li.mOnClickListener.onClick(this);
         result = true;
     } else {
         result = false;
     }
     //...
     return result;
 }
複製代碼

performClick()方法中的邏輯是,若是你設置了OnClickListener回調,那麼就會執行onClick()方法,你們也注意到performClick()會返回一個true或者false,可是這個返回值對於onTouchEvent()方法沒有任何意義,由於上面提到switch語句塊的後面必定返回true。這裏咱們再得出一個結論:OnLongClickListener的onLongClick()方法的優先級高於onClickListener的onClick()方法

好了如今咱們的手指從按下到擡起,就已經分析完onTouchEvent()中的ACTION_DOWN和ACTION_UP分支,若是你的手指在擡起前,不當心移動了一下,就會觸發ACTION_CANCEL或ACTION_MOVE,這個時候它就會根據條件(手指是否移出View的範圍)經過調用 removeLongPressCallback()或 removeTapCallback()方法移除CheckForLongPress或CheckForTap任務,即取消長按或點擊,這裏限於篇幅就再也不展開分析,你們可自行分析。

三、小結

一、View沒有子View,因此它的的分發比較簡單,從View的dispatchTouchEvent()方法開始進入View的事件分發流程,該方法只負責事件的分發,沒有進行實際事件的處理,進行實際事件的處理有兩處地方:一、經過外部設置的onTouchListener的onTouch()方法,二、View的onTouchEvent()方法。

二、當一個View要處理點擊事件時,若是它設置了onTouchListener,那麼onTouch方法就會回調,這時事件如何處理還要看onTouch()方法的返回值,若是返回true,那麼onTouchEvent()方法將不會被調用,dispatchTouchEvent()方法直接返回true;若是返回false,onTouchEvent()方法會被調用,這時事件如何處理就要看onTouchEvent()的返回值,在onTouchEvent()中,無論控件可用仍是不可用,返回值取決於控件是否可點擊,若是控件可點擊(clickabale或longClickabale,只要有一個爲true),onTouchEvent()返回true,若是控件不可點擊(clickabale和longClickabale都爲false),onTouchEvent()返回false。

三、若是咱們同時設置了OnTouchListener、OnLongClickListener和OnClickListener回調,根據優先級,事件的傳遞順序是:onTouch() -> onLongClick() -> onClick(),其中除了onClick()都有boolean返回值,返回值能決定下一個方法是否被調用,onClick()優先級最低,連返回值都沒有。

四、 對於ViewGroup(也就是當前 View 的父容器)而言,它只認識子 View的dispatchTouchEvent()方法,不認識另外兩個處理事件的方法。子View的 onTouch() 和 onTouchEvent() 都是在本身的 dispatchTouchEvent() 裏面調用的,他們兩個會影響 dispatchTouchEvent() 的返回值,可是對於上級 ViewGroup 而言,它只認識 dispatchTouchEvent() 的返回值。

流程圖:

ViewGroup的事件分發

一、ViewGroup::dispatchTouchEvent()

ViewGroup是View的子類,它是一組View的集合,它包含不少子View和子ViewGroup,因此ViewGroup的事件分發比View的複雜,可是ViewGroup的事件分發纔是整個事件分發機制的精髓,和View同樣ViewGroup的事件分發的起點也是dispatchTouchEvent(),雖然這個方法在View中,可是ViewGroup重寫了它,由於它們的分發邏輯不同。因此咱們看ViewGroup的dispatchTouchEvent()方法,以下:

//ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    //...
    //本次事件處理結果
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
         final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
        //一、若是本次事件是ACTION_DOWN
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            //置空mFirstTouchTarget
            cancelAndClearTouchTargets(ev);
            //清除mGroupFlags中的FLAG_DISALLOW_INTERCEPT標誌位,這個標誌等同於下面的disallowIntercept
            resetTouchState();
        }
        //ViewGroup是否攔截本次事件標誌
        final boolean intercepted;
        //二、若是本次事件是ACTION_DOWN 或者 mFirstTouchTarget爲空
        if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
            //子View是否禁止ViewGroup攔截事件標誌
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {//若是子View容許ViewGroup攔截事件
                //調用onInterceptTouchEvent()方法詢問ViewGroup是否攔截事件,intercepted的值由onInterceptTouchEvent(ev)決定
                intercepted = onInterceptTouchEvent(ev);
                //...
            } else {//若是子View禁止ViewGroup攔截事件
                intercepted = false;//intercepted值爲false
            }
        } else {//若是本次事件不是ACTION_DOWN又沒有target
            //intercepted值爲true,在此以後,當前事件序列中的全部事件序列都由ViewGroup處理,不會再傳遞給子View
            intercepted = true;
        }
        //...
        //檢查本次事件是不是ACTION_CANCEL
        final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        //三、若是本次事件不取消而且不攔截,就尋找合適的子View處理
        if (!canceled && !intercepted) {
            //...
            //3.一、若是本次事件是ACTION_DOWN
            if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
               
			  final int childrenCount = mChildrenCount;
                //若是target是null而且ViewGroup有子View,就尋找某個子View當mFirstTouchTarget
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    final View[] children = mChildren;
                    //從後往前逐個取出子View
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
                        //判斷子View可否接受點擊事件:子View可見或在播放動畫,而且觸摸點在子View範圍內
                        if (!canViewReceivePointerEvents(child)
                            || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
					  //走到這裏表示子View知足處理事件的條件
                        //...
                        //dispatchTransformedTouchEvent()裏面會調用子View的dispatchTouchEvent()方法,在這個方法裏把事件分發給子View
                         if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                              //...
                               //若是dispatchTransformedTouchEvent()返回true,表示找到子View消費本次事件了,就會走到這裏, 因此這個子View就被看成mFirstTouchTarget,這裏會調用addTouchTarget()方法爲mFirstTouchTarget賦值
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
						//...
                    }//end...for()
                    
                    //...
                    
                }//end...if(newTouchTarget == null && childrenCount != 0)
                
            }//end...if(actionMasked == MotionEvent.ACTION_DOWN...)
            
        }///end...if(!canceled && !intercepted)
        
        //四、根據mFirstTouchTarget是否爲null作出不一樣行爲
        if (mFirstTouchTarget == null) {//這通常有三種狀況致使mFirstTouchTarget爲空:
            //一、ViewGroup沒有子View;
            //二、子View處理了ACTION_DOWN事件,可是在dispatchTouchEvent()返回了false;
            //三、ViewGroup在DOWN事件中的onInterceptTouchEvent(ev)返回了true
       		//在這三種狀況下ViewGroup就會本身處理事件
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                                                    TouchTarget.ALL_POINTER_IDS);
        } else { //有兩種狀況mFirstTouchTarget不爲空,表示找到合適的子View爲target:
            //一、本次事件是ACTION_DOWN,遍歷完ViewGroup全部的子View後找到了合適的子View爲target;
            //二、本次事件是除了ACTION_DOWN之外的其餘事件,可是在ACTION_DOWN時已經找到了合適的子View爲target
           //因此接下來就直接把事件分發給mFirstTouchTarget的child處理處理就行
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            //mFirstTouchTarget是一個單鏈表結構
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {//狀況1的處理
                    //由於在找到target時已經調用過dispatchTransformedTouchEvent()了,表示該target的View已經消費了該事件,handle直接等於true
                    handled = true;
                } else {//狀況2的處理
                     final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;//注意這個intercepted,若是爲true,cancelChild爲true,會致使子View收到一個ACTION_CANCEL, 表示子View的本次事件取消
                    //調用dispatchTransformedTouchEvent()方法把事件分發給target
                    if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                        //handle的是否爲true取決於子View的dispatchTouchEvent()返回值
                        handled = true;
                    }
                    //清空這個子View對應的target,致使該事件序列的後序事件該子View都沒法再收到
                     if (cancelChild) {
                         //...
                         target.recycle();
                         target = next;
                         continue;
                     }
                }
                predecessor = target;
                target = next;
            }//end...while (target != null) 
            
         }//end...if (mFirstTouchTarget == null)
        
        //...
        
    }//end...if (onFilterTouchEventForSecurity(ev))
    
}
複製代碼

這個方法特別長,裏面就是整個ViewGroup的事件分發邏輯,我知道你們也沒有想看的慾望了,這個方法對應的流程圖以下:

能夠看到,在同一個事件序列內(從down開始,到up結束),ViewGroup的dispatchTouchEvent()方法能夠分爲兩大過程:一、ACTION_DOWN事件的處理流程;二、除了ACTION_DOWN之外的事件處理流程。下面跟着這兩個流程分別走一遍。

二、ViewGroup處理ACTION_DOWN事件的流程

ACTION_DOWN事件的處理流程又能夠分爲兩個流程即:ViewGroup攔截事件(intercepted = true)與不攔截事件(intercepted = false)

看流程圖,在dispatchTouchEvent()方法註釋2中的if語句會決定 intercepted 的值,以下:

//ViewGroup是否攔截本次事件標誌
final boolean intercepted;
//二、若是本次事件是ACTION_DOWN 或者 mFirstTouchTarget爲空
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    //2.一、子View是否禁止ViewGroup攔截事件標誌
    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
    if (!disallowIntercept) {//若是子View容許ViewGroup攔截事件
        //調用onInterceptTouchEvent()方法詢問ViewGroup是否攔截事件,intercepted的值由onInterceptTouchEvent(ev)決定
        intercepted = onInterceptTouchEvent(ev);
        //...
    } else {//若是子View禁止ViewGroup攔截事件
        intercepted = false;//intercepted值爲false
    }
} else {
    //...
}
複製代碼

若是本次事件是ACTION_DOWN也會進入這個if分支,看註釋2.1檢查 mGroupFlags 中是否包含FLAG_DISALLOW_INTERCEPT標識,默認沒有,即默認disallowIntercept爲false,因此就會調用onInterceptTouchEvent()方法詢問ViewGroup是否攔截事件,intercepted的值由onInterceptTouchEvent()決定,onInterceptTouchEvent()默認返回false,因此intercepted = false

2.一、intercepted = false

當DOWN事件沒有被ViewGroup攔截,intercepted = false,它就會進入dispatchTouchEvent()方法註釋3的if語句,以下:

//檢查本次事件是不是ACTION_CANCEL
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
//...
//三、若是本次事件不取消而且不攔截,就尋找合適的子View處理
if (!canceled && !intercepted) {
    //...
    //若是本次事件是ACTION_DOWN
    if (actionMasked == MotionEvent.ACTION_DOWN
        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

        final int childrenCount = mChildrenCount;
        //若是target是null而且ViewGroup有子View,就尋找某個子View當mFirstTouchTarget
        if (newTouchTarget == null && childrenCount != 0) {
            final float x = ev.getX(actionIndex);
            final float y = ev.getY(actionIndex);
            final View[] children = mChildren;
            //從後往前逐個取出子View
            for (int i = childrenCount - 1; i >= 0; i--) {
                final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
                //3.一、判斷子View可否接受點擊事件:子View可見或在播放動畫,而且觸摸點在子View範圍內
                if (!canViewReceivePointerEvents(child)
                    || !isTransformedTouchPointInView(x, y, child, null)) {
                    ev.setTargetAccessibilityFocus(false);
                    continue;
                }
                //走到這裏表示子View知足處理事件的條件
                //...
                //3.二、dispatchTransformedTouchEvent()裏面會調用子View的dispatchTouchEvent()方法,在這個方法裏把事件分發給子View
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    //...
                    //3.三、若是dispatchTransformedTouchEvent()返回true,表示找到子View消費本次事件了,就會走到這裏, 因此這個子View就被看成mFirstTouchTarget,這裏會調用addTouchTarget()方法爲mFirstTouchTarget賦值
                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                    alreadyDispatchedToNewTouchTarget = true;
                    break;
                }
                //...
            }//end...for()

            //...

        }//end...if(newTouchTarget == null && childrenCount != 0)

    }//end...if(actionMasked == MotionEvent.ACTION_DOWN...)

}///end...if(!canceled && !intercepted)
複製代碼

若是是DOWN事件,假設ViewGroup有子View,就會進入for循環,ViewGroup就會遍歷全部子View,先在註釋3.1中判斷這個子View是否知足接收事件的條件,若是不知足,就再找下一個子View,若是知足,就來到了註釋3.2,而後調用dispatchTransformedTouchEvent()方法看這個子View是否消費DOWN事件。

dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)方法以下:

//ViewGroup.java
//dispatchTransformedTouchEvent()只須要關注兩個參數:
//@params cancel 是否取消本次事件
//@params child 準備接收分發事件的子View
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
     final boolean handled;
    final int oldAction = event.getAction();
    //一、若是cancel爲true,進入這個if分支
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        //設置ACTION_CANCEL事件
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    //...
    //二、若是cancel爲false,進入這個if分支
    if (child == null) {//若是child爲空
        //調用 super.dispatchTouchEvent(event),表示ViewGroup本身決定是否處理本次事件
        handled = super.dispatchTouchEvent(event);
    } else {//若是child不爲空
        //...
        //調用child.dispatchTouchEvent(event),表示讓子View決定是否處理本次事件
        handled = child.dispatchTouchEvent(event);
    }
    return handled;
}
複製代碼

由於傳入cancel爲false,因此來帶註釋2的if分支,由於傳入的child不爲空,因此調用child.dispatchTouchEvent(event),表示讓子View決定是否處理本次事件,到這裏DOWN事件就傳遞給子View,若是子View是一個View,那麼它的處理流程就像前面介紹的View的事件分發同樣,若是子View是一個ViewGroup,那麼它的處理流程就又是ViewGroup的事件分發

好了,假設子View消費這個事件,返回true,則dispatchTransformedTouchEvent()返回true,ViewGrou找到了要消費這個DOWN事件的子View,這時進入註釋3.3,調用addTouchTarget(child, idBitsToAssign)方法,以下:

//ViewGroup.java
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    //給mFirstTouchTarget賦值
    mFirstTouchTarget = target;
    return target;
}

//ViewGroup::TouchTarget
public static TouchTarget obtain(@NonNull View child, int pointerIdBits) {
    //...
    target.child = child;
    return target;
}
複製代碼

若是找到了要消費這個DOWN事件的子View,那麼這個子View就會被賦值給mFirstTouchTarget的child字段,這個就至關於作了一個記錄,當下一個事件到來時,若是發現mFirstTouchTarget不爲空,我就能夠直接把事件分發給mFirstTouchTarget中的View,就不用再去遍歷子View了。那麼mFirstTouchTarget是什麼?它是一個TouchTarget類型,以下:

///ViewGroup::TouchTarget
private static final class TouchTarget {
    //當前消費事件的View
    public View child;
    //它的下一個結點
    public TouchTarget next;
    //...
}
複製代碼

它是一個鏈表結構,爲何mFirstTouchTarget是一個鏈表?個人猜想是因爲多點觸控的存在,例如我5個手指能夠同時觸摸到列表的5個子View,若是5個子View都是要消費這個DOWN事件的話,那麼就要用鏈表把它們記錄起來,當下一個事件到來時,5個子View都能分發到事件。

好了,如今找到能夠消費事件的子View了,而且mFirstTouchTarget也被賦值了,就一個break跳出for循環,直接來到dispatchTouchEvent()方法的註釋4,以下:

//四、根據mFirstTouchTarget是否爲null作出不一樣行爲
if (mFirstTouchTarget == null) {
    //...
} else {//有兩種狀況mFirstTouchTarget不爲空,表示找到合適的子View爲target:
    //一、本次事件是ACTION_DOWN,遍歷完ViewGroup全部的子View後找到了合適的子View爲target;
    //二、本次事件是除了ACTION_DOWN之外的其餘事件,可是在ACTION_DOWN時已經找到了合適的子View爲target
    //因此接下來就直接把事件分發給mFirstTouchTarget的child處理就行
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    //mFirstTouchTarget是一個單鏈表結構
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {//狀況1的處理
            //由於在找到target時已經調用過dispatchTransformedTouchEvent()了,表示該target的View已經消費了該事件,handle直接等於true
            handled = true;
        } else {//狀況2的處理
            //...
        }
        predecessor = target;
        target = next;
    }//end...while (target != null) 

}//end...if (mFirstTouchTarget == null)
複製代碼

mFirstTouchTarget不爲空,就來到else分支,而後由於是DOWN事件,在上面的for循環中找到子View消費事件後alreadyDispatchedToNewTouchTarget賦值爲true而且mFirstTouchTarget等於newTouchTarget實例,就來到狀況1的處理的if分支,這裏直接返回了true,由於上面在for循環中target的View已經消費了該事件,handle直接等於true。

到這裏在DOWN事件下ViewGroup不攔截的狀況下分析完畢。上面是假設找到了子View而且子View消費了事件,這樣當下一次事件到來時mFirstTouchTarget不爲空,就直接把這個事件給子View;可是若是上面是找到子View而這個子View不消費這個DOWN事件,即子View的dispatchTouchEvent()方法返回false,那麼dispatchTransformedTouchEvent()返回false,就致使沒法爲mFirstTouchTarget賦值,mFirstTouchTarget爲空,當下一次事件序列到來時,ViewGroup會直接處理,而再也不轉發給子View。這裏得出一個結論:子View若是不消費ACTION_DOWN事件,那麼同一事件序列的其餘事件都不會再交給它來處理,而是交給它的父ViewGroup處理;子View一旦消費ACTION_DOWN事件,那麼同一事件序列的其餘事件都會交給它處理

因此若是此時子View沒有消費ACTION_DOWN事件,或者我重寫了ViewGroup的onInterceptTouchEvent()並返回了true,那麼ViewGroup就會開始攔截事件,接下來看在DOWN事件下ViewGroup攔截的狀況,即intercepted = true

2.二、intercepted = true

若是ViewGroup攔截DOWN事件,那麼intercepted = true,就不會進入dispatchTouchEvent()方法的註釋3的if語句,這樣在DOWN事件下ViewGroup就不會遍歷它的子View,也就沒法調用dispatchTransformedTouchEvent()找到要消費事件的子View,同理沒法調用addTouchTarget()方法爲mFirstTouchTarget賦值,就會致使在DOWN事件下mFirstTouchTarget爲空,這樣就直接來到了dispatchTouchEvent()方法的註釋4的if語句,以下:

//檢查本次事件是不是ACTION_CANCEL
 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
//... 
//四、根據mFirstTouchTarget是否爲null作出不一樣行爲
if (mFirstTouchTarget == null) {//這通常有三種狀況致使mFirstTouchTarget爲空:
    //一、ViewGroup沒有子View;
    //二、子View處理了ACTION_DOWN事件,可是在dispatchTouchEvent()返回了false;
    //三、ViewGroup在DOWN事件中的onInterceptTouchEvent(ev)返回了true
    //在這三種狀況下ViewGroup就會本身處理事件
    //注意第三個參數傳入null,表示ViewGroup本身處理事件
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                                            TouchTarget.ALL_POINTER_IDS);
} else {         
    //... 
}//end...if (mFirstTouchTarget == null)
複製代碼

很明顯這裏是狀況3,因此沒有找到子View,dispatchTransformedTouchEvent()方法的第三個參數爲空,而第二個參數爲false,由於不是ACTION_CANCEL事件,咱們參考上面的dispatchTransformedTouchEvent()方法分析,以下:

//ViewGroup.java
//dispatchTransformedTouchEvent()只須要關注兩個參數:
//@params cancel 是否取消本次事件
//@params child 準備接收分發事件的子View
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
     final boolean handled;
    //...
    //二、若是cancel爲false,進入這個if分支
    if (child == null) {//若是child爲空
        //調用 super.dispatchTouchEvent(event),表示ViewGroup本身決定是否處理本次事件
        handled = super.dispatchTouchEvent(event);
    } else {//若是child不爲空
        //...
        //調用child.dispatchTouchEvent(event),表示讓子View決定是否處理本次事件
        handled = child.dispatchTouchEvent(event);
    }
    return handled;
}
複製代碼

裏面就會調用 super.dispatchTouchEvent(event),表示ViewGroup本身決定是否處理本次事件,ViewGroup的父類是View,因此super.dispatchTouchEvent(event)裏面的處理邏輯就是View的事件分發的處理邏輯,見前面分析的View的事件分發。

到這裏在DOWN事件下ViewGroup攔截的狀況分析完畢。這裏得出一個結論:ViewGroup若是在onInterceptTouchEvent()方法的ACTION_DOWN事件中返回true,那麼整個事件序列都會交給ViewGroup處理,再也不交給子View

咱們回到dispatchTouchEvent()方法,還有一點要注意的是在ACTION_DOWN下無論攔截仍是不攔截都會進入dispatchTouchEvent()方法中註釋1的if語句,以下:

//一、若是本次事件是ACTION_DOWN
if (actionMasked == MotionEvent.ACTION_DOWN) {
    //置空mFirstTouchTarget
    cancelAndClearTouchTargets(ev);
    //清除mGroupFlags中的FLAG_DISALLOW_INTERCEPT標誌位,這個標誌等同於下面的disallowIntercept
    resetTouchState();
}
複製代碼

這個if語句的做用就是防止前一次事件序列對本次事件序列形成影響,因此它會向先調用 cancelAndClearTouchTargets(ev)清空mFirstTouchTarget,而後調用resetTouchState()清除FLAG_DISALLOW_INTERCEPT標誌位,由於ACTION_DOWN事件是一個新的事件序列的開始,因此dispatchTouchEvent()方法首先要作的就是判斷是否是迎來了一個新的事件序列,因此要判斷該事件是不是ACTION_DOWN 事件,若是是 ACTION_DOWN 事件,做爲一個事件序列的開頭,應當要消除前面的事件序列可能留下的影響。關於FLAG_DISALLOW_INTERCEPT標誌位後面會講。

到這裏ViewGroup處理ACTION_DOWN事件的流程分析完畢,下面咱們來看除了ACTION_DOWN之外的事件的處理流程。

三、ViewGroup處理除了ACTION_DOWN之外的事件的流程

ACTION_DOWN事件的處理流程又能夠分爲兩個流程即:mFirstTouchTarget != null與mFirstTouchTarget == null。你會發現intercepted這個標記位彷佛已經沒有多大做用, 它若是是true,它根本不會進入dispatchTouchEvent()方法的註釋3,就算是false進入了dispatchTouchEvent()方法的註釋3,它也不會知足註釋3.1的條件。因此咱們就直接來到註釋4。

3.一、mFirstTouchTarget == null

//檢查本次事件是不是ACTION_CANCEL
 final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
//... 
//四、根據mFirstTouchTarget是否爲null作出不一樣行爲
if (mFirstTouchTarget == null) {//這通常有三種狀況致使mFirstTouchTarget爲空:
    //一、ViewGroup沒有子View;
    //二、子View處理了ACTION_DOWN事件,可是在dispatchTouchEvent()返回了false;
    //三、ViewGroup在DOWN事件中的onInterceptTouchEvent(ev)返回了true
    //在這三種狀況下ViewGroup就會本身處理事件
    //注意第三個參數傳入null,表示ViewGroup本身處理事件
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                                            TouchTarget.ALL_POINTER_IDS);
} else {         
    //... 
}//end...if (mFirstTouchTarget == null)
複製代碼

這裏一、二、3狀況都有可能發生,從ACTION_DOWN的處理流程咱們知道爲mFirstTouchTarget賦值的過程只會在處理ACTION_DOWN事件的時候出現,因此若是在處理ACTION_DOWN事件的時候ViewGroup沒有子View,不會進入for循環,致使mFirstTouchTarget爲空;若是ViewGroup有子View,進入了for循環,可是View不消費DOWN事件,即在dispatchTouchEvent()返回了false,致使沒法調用addTouchTarget()方法爲mFirstTouchTarget賦值,致使mFirstTouchTarget爲空;ViewGroup在DOWN事件中的onInterceptTouchEvent(ev)返回了true,不會進入註釋3的if語句,致使mFirstTouchTarget爲空;因此在處理ACTION_DOWN事件的時候沒有找到mFirstTouchTarget,就會致使在除了ACTION_DOWN其餘事件到來時mFirstTouchTarget == null,這裏就直接讓ViewGroup本身處理事件了。

3.二、mFirstTouchTarget != null

//四、根據mFirstTouchTarget是否爲null作出不一樣行爲
if (mFirstTouchTarget == null) {
    //...
} else {//有兩種狀況mFirstTouchTarget不爲空,表示找到合適的子View爲target:
    //一、本次事件是ACTION_DOWN,遍歷完ViewGroup全部的子View後找到了合適的子View爲target;
    //二、本次事件是除了ACTION_DOWN之外的其餘事件,可是在ACTION_DOWN時已經找到了合適的子View爲target
    //因此接下來就直接把事件分發給mFirstTouchTarget的child處理就行
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    //mFirstTouchTarget是一個單鏈表結構
    while (target != null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {//狀況1的處理
           //...
        } else {//狀況2的處理
             //4.1
              final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;//注意這個intercepted,若是爲true,cancelChild爲true,會致使子View收到一個ACTION_CANCEL, 表示子View的本次事件取消
                    //4.二、調用dispatchTransformedTouchEvent()方法把事件分發給target
                    if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
                        //handle的是否爲true取決於子View的dispatchTouchEvent()返回值
                        handled = true;
                    } 
                    //4.三、清空這個子View對應的target,致使該事件序列的後序事件該子View都沒法再收到
                     if (cancelChild) {
                         //...
                         target.recycle();
                         target = next;
                         continue;
                     }
                }
                predecessor = target;
                target = next;
        }
        predecessor = target;
        target = next;
    }//end...while (target != null) 

}//end...if (mFirstTouchTarget == null)
複製代碼

mFirstTouchTarget != null,表示在處理ACTION_DOWN事件的時候已經找到mFirstTouchTarget,就會進入註釋4的else分支,這裏是狀況2,就會進入狀況2的處理的else分支,註釋4.1的cancelChild這個值會決定子View是收到ACTION_CANCEL事件仍是其餘事件,而cancelChild的值取決於intercepted的值,因此若是ViewGroup在除了ACTION_DOWN之外的其餘事件中的onInterceptTouchEvent(ev)方法返回了true,致使intercepted = true,從而cancelChild = true,而若是ViewGroup一直保持默認狀態,intercepted = false,從而cancelChild = false,緊接着在註釋4.2把cancelChild和target.child傳進了dispatchTransformedTouchEvent()方法中。

我再貼一下dispatchTransformedTouchEvent()方法的代碼,以下:

//ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
     final boolean handled;
    final int oldAction = event.getAction();
    //一、若是cancel爲true,進入這個if分支
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        //設置ACTION_CANCEL事件
        event.setAction(MotionEvent.ACTION_CANCEL);
        //分發ACTION_CANCEL事件
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }
    //...
    //二、若是cancel爲false,進入這個if分支
    if (child == null) {
        //調用 super.dispatchTouchEvent(event),表示ViewGroup本身決定是否處理本次事件
        handled = super.dispatchTouchEvent(event);
    } else {
        //...
        //調用child.dispatchTouchEvent(event),表示讓子View決定是否處理本次事件
        handled = child.dispatchTouchEvent(event);
    }
    return handled;
}
複製代碼

能夠看到若是cancel爲true,進入註釋1這個if分支,裏面會set一個ACTION_CANCEL事件,而後傳遞給target記錄的子View;若是cancel爲false,進入註釋2這個else分支,調用child.dispatchTouchEvent(event),表示讓target記錄的子View決定是否處理本次事件,前面已經講過了。

好,如今咱們走出dispatchTransformedTouchEvent()方法,來到註釋4,若是cancelChild爲true,就會調用TouchTarget的recycler()方法回收這個target,這樣作的後果是什麼呢?這樣至關於清空了mFirstTouchTarget,當下一次事件到來時mFirstTouchTarget == null,ViewGroup直接處理事件,不會再分發給子View。

到這裏ViewGroup處理除了ACTION_DOWN之外事件的流程分析完畢。

四、子View如何禁止ViewGroup攔截事件

前面的分析都是默認子View不由止ViewGroup攔截事件,因此ViewGroup能夠經過onInterceptTouchEvent()返回true從而攔截下子View的事件,但此時子View但願依然可以響應這些事件該怎麼辦呢?Android給咱們提供了一個方法:requestDisallowInterceptTouchEvent(boolean) 用於設置是否容許攔截,以下:

//ViewGroup.java
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    //...
    if (disallowIntercept) {
        mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
    } else {
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
    }
    // Pass it up to our parent
    if (mParent != null) {
        mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
    }
}
複製代碼

當子View調用getParent.requestDisallowInterceptTouchEvent(true),mGroupFlags就會有FLAG_DISALLOW_INTERCEPT標識,當子View調用getParent.requestDisallowInterceptTouchEvent(false),mGroupFlags就會清除FLAG_DISALLOW_INTERCEPT標識,那麼FLAG_DISALLOW_INTERCEPT標識又是怎麼控制ViewGroup的攔截的呢?以下:

final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {//若是子View容許ViewGroup攔截事件
    //調用onInterceptTouchEvent()方法詢問ViewGroup是否攔截事件,intercepted的值由onInterceptTouchEvent(ev)決定
    intercepted = onInterceptTouchEvent(ev);
    //...
} else {//若是子View禁止ViewGroup攔截事件
    intercepted = false;//intercepted值爲false
}
複製代碼

子View經過調用getParent.requestDisallowInterceptTouchEvent(true),來禁止ViewGroup攔截除了ACTION_DOWN之外的其餘事件,這樣當下一個事件到來時就會交給這個子View,

爲何是除了ACTION_DOWN之外的其餘事件?由於ACTION_DOWN事件是事件序列的開始,ACTION_DOWN事件會先通過ViewGroup的onInterceptTouchEvent()方法,從ACTION_DOWN事件的處理流程 - intercepted = true咱們知道,若是ViewGroup一開始在onInterceptTouchEvent()的ACTION_DOWN返回true,它就不會進入dispatchTouchEvent()方法的註釋3的if語句,這樣在DOWN事件下就沒法找到mFirstTouchTarget,這樣當同一個事件序列的其餘事件到來時,mFirstTouchTarget == null,這樣ViewGroup只能把事件交給本身處理,沒法傳遞給子View,也就沒法調用子View的dispatchTouchEvent()方法,這樣子View在dispatchTouchEvent()方法中調用getParent.requestDisallowInterceptTouchEvent(true)就沒有意義了。

五、小結

從ViewGroup的事件分發中得出幾個結論:

一、ViewGroup若是在onInterceptTouchEvent()方法的ACTION_DOWN事件中返回true,那麼整個事件序列都會交給ViewGroup處理,再也不交給子View,從而致使沒法調用子View的dispatchTouchEvent()方法,致使子View調用getParent.requestDisallowInterceptTouchEvent(true)失效。

二、ViewGroup若是在onInterceptTouchEvent()方法中一旦攔截除了ACTION_DOWN的事件,那麼子View將會收到一個ACTION_CANCEL事件,而且接下來的事件都是交給ViewGroup處理。

三、一、2點的含義都是ViewGroup決定攔截事件,那麼一旦ViewGroup決定攔截事件,那麼接下來的事件都是交給ViewGroup處理,而且ViewGroup的onInterceptTouchEvent()方法在這個事件序列內不會再調用,這說明ViewGroup的onInterceptTouchEvent()方法不是每次都調用,只有ViewGroup的dispatchTouchEvent()才能保證每次調用。

三、在ViewGroup中ACTION_DOWN 事件負責尋找 target,即尋找可以消費ACTION_DOWN事件的子View,若是找到,那麼接下來同一事件序列內的全部事件都會交給這個子View處理,再也不交給ViewGroup;若是沒有找到,有兩種狀況:一、ViewGroup沒有子View,二、子View處理了ACTION_DOWN事件,可是在dispatchTouchEvent()返回了false,那麼接下來同一事件序列下的全部事件都是ViewGroup本身處理。

四、子View若是不消費ACTION_DOWN事件,那麼同一事件序列的其餘事件都不會再交給它來處理,而是交給它的父ViewGroup處理;子View一旦消費ACTION_DOWN事件,若是ViewGroup不攔截,那麼同一事件序列的其餘事件都會交給子View處理。

五、當調用super.dispatchTouchEvent(event)就表明ViewGroup開始本身處理事件,裏面會執行ViewGroup的onTouchEvent(), 邏輯和View的事件分發同樣。

結語

當點擊事件到達ViewGroup時,它的dispatchTouchEvent()方法就會被調用,若是這個ViewGroup的onInterceptTouchEvent()方法返回true,就表示它要攔截當前事件,接下來這個事件序列內的事件都會交給它處理,即super.dispatchTouchEvent()方法獲得調用;若是這個ViewGroup的onInterceptTouchEvent()方法返回false,就表示它不攔截當前事件,這時當前事件就會傳遞給它的子View,接着子View的dispatchTouchEvent()方法就會被調用,若是子View是一個View,那麼它的處理流程就像前面介紹的View的事件分發同樣,若是子View是一個ViewGroup,那麼它的處理流程就又是ViewGroup的事件分發,如此遞歸,從上到下,直到整顆View樹都收到事件,接下來遞歸返回,從下到上,每一層的返回值都決定是否消費本次事件,若是消費,返回true,它的上一層就沒法處理這個事件,若是不消費,返回false,它的上一層又繼續傳給上一層,直到根視圖。

View的事件分發小結和ViewGroup的事件分發小結均可以在源碼中找到證實,能夠自行驗證一下,本文經過源碼 + 流程圖 說明了整個View的事件分發體制,在看的過程最好要結合上下文來看,始終記住這是在同一個事件序列內,跟着流程圖的每個分支在源碼中走一遍,那樣你就會有更深入的理解。

參考資料:

Android事件分發徹底解析之事件從何而來

經過流程圖來分析Android事件分發

相關文章
相關標籤/搜索