Android 事件分發(fā)機(jī)制詳解

前言

Android事件分發(fā)機(jī)制是Android開發(fā)者必須了解的基礎(chǔ)缕碎。

目錄

image

一. 基礎(chǔ)認(rèn)知

1.1 事件分發(fā)的由來

安卓的View是樹形結(jié)構(gòu)的,View可能會(huì)重疊在一起池户,當(dāng)我們點(diǎn)擊的地方有多個(gè)View都可以響應(yīng)的時(shí)候咏雌,這個(gè)點(diǎn)擊事件應(yīng)該給誰呢?為了解決這一個(gè)問題校焦,就有了事件分發(fā)機(jī)制赊抖。

1.2 事件分發(fā)的 "事件" 是指什么?

點(diǎn)擊事件 (Touch事件) 寨典。具體介紹如下:

image

特別說明:事件列氛雪,即指從手指接觸屏幕至手指離開屏幕這個(gè)過程產(chǎn)生的一系列事件。一般情況下耸成,事件列都是以DOWN事件開始报亩、UP事件結(jié)束,中間有無數(shù)的MOVE事件井氢。

image

1.3 事件分發(fā)的本質(zhì)

將點(diǎn)擊事件(MotionEvent)傳遞到某個(gè)具體的View & 處理的整個(gè)過程弦追。

即 事件傳遞的過程 = 分發(fā)過程。

1.4 事件在哪些對(duì)象之間進(jìn)行傳遞花竞?

答:Activity劲件、ViewGroup、View左胞。

image
image

1.5 事件分發(fā)的順序

即 事件傳遞的順序:Activity -> ViewGroup -> View

即:1個(gè)點(diǎn)擊事件發(fā)生后定罢,事件先傳到Activity蟀淮、再傳到ViewGroup、最終再傳到View

1.6 事件分發(fā)過程由哪些方法協(xié)作完成婿崭?

答:dispatchTouchEvent()俭嘁、onInterceptTouchEvent()onTouchEvent()

image

二. 事件分發(fā)機(jī)制流程詳細(xì)分析

主要包括:Activity事件分發(fā)機(jī)制躺枕、ViewGroup事件分發(fā)機(jī)制、View事件分發(fā)機(jī)制

2.1 Activity事件分發(fā)機(jī)制

事件分發(fā)機(jī)制供填,首先會(huì)將點(diǎn)擊事件傳遞到Activity中拐云,具體是執(zhí)行dispatchTouchEvent()進(jìn)行事件分發(fā)。

源碼分析

/**
  * 源碼分析:Activity.dispatchTouchEvent()
  */ 
  public boolean dispatchTouchEvent(MotionEvent ev) {

    // 僅貼出核心代碼

    // ->>分析1
    if (getWindow().superDispatchTouchEvent(ev)) {

        return true;
        // 若getWindow().superDispatchTouchEvent(ev)的返回true
        // 則Activity.dispatchTouchEvent()就返回true近她,則方法結(jié)束叉瘩。即 :該點(diǎn)擊事件停止往下傳遞 & 事件傳遞過程結(jié)束
        // 否則:繼續(xù)往下調(diào)用Activity.onTouchEvent

    }
    // ->>分析3
    return onTouchEvent(ev);
  }

/**
  * 分析1:getWindow().superDispatchTouchEvent(ev)
  * 說明:
  *     a. getWindow() = 獲取Window類的對(duì)象
  *     b. Window類是抽象類,其唯一實(shí)現(xiàn)類 = PhoneWindow類
  *     c. Window類的superDispatchTouchEvent() = 1個(gè)抽象方法粘捎,由子類PhoneWindow類實(shí)現(xiàn)
  */
  @Override
  public boolean superDispatchTouchEvent(MotionEvent event) {

      return mDecor.superDispatchTouchEvent(event);
      // mDecor = 頂層View(DecorView)的實(shí)例對(duì)象
      // ->> 分析2
  }

/**
  * 分析2:mDecor.superDispatchTouchEvent(event)
  * 定義:屬于頂層View(DecorView)
  * 說明:
  *     a. DecorView類是PhoneWindow類的一個(gè)內(nèi)部類
  *     b. DecorView繼承自FrameLayout薇缅,是所有界面的父類
  *     c. FrameLayout是ViewGroup的子類危彩,故DecorView的間接父類 = ViewGroup
  */
  public boolean superDispatchTouchEvent(MotionEvent event) {

      return super.dispatchTouchEvent(event);
      // 調(diào)用父類的方法 = ViewGroup的dispatchTouchEvent()
      // 即將事件傳遞到ViewGroup去處理,詳細(xì)請(qǐng)看后續(xù)章節(jié)分析的ViewGroup的事件分發(fā)機(jī)制

  }
  // 回到最初的分析2入口處

/**
  * 分析3:Activity.onTouchEvent()
  * 調(diào)用場(chǎng)景:當(dāng)一個(gè)點(diǎn)擊事件未被Activity下任何一個(gè)View接收/處理時(shí)泳桦,就會(huì)調(diào)用該方法
  */
  public boolean onTouchEvent(MotionEvent event) {

        // ->> 分析5
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
        // 即 只有在點(diǎn)擊事件在Window邊界外才會(huì)返回true汤徽,一般情況都返回false,分析完畢
    }

/**
  * 分析4:mWindow.shouldCloseOnTouch(this, event)
  * 作用:主要是對(duì)于處理邊界外點(diǎn)擊事件的判斷:是否是DOWN事件灸撰,event的坐標(biāo)是否在邊界內(nèi)等
  */
  public boolean shouldCloseOnTouch(Context context, MotionEvent event) {

  if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
          && isOutOfBounds(context, event) && peekDecorView() != null) {

        // 返回true:說明事件在邊界外谒府,即 消費(fèi)事件
        return true;
    }

    // 返回false:在邊界內(nèi),即未消費(fèi)(默認(rèn))
    return false;
  } 

源碼總結(jié)

當(dāng)一個(gè)點(diǎn)擊事件發(fā)生時(shí)浮毯,從Activity的事件分發(fā)開始(Activity.dispatchTouchEvent())完疫,流程總結(jié)如下:

image

核心方法總結(jié)

主要包括:dispatchTouchEvent()、onTouchEvent() 總結(jié)如下:

image

那么ViewGroupdispatchTouchEvent() 什么時(shí)候返回true / false? 請(qǐng)繼續(xù)往下看ViewGroup事件的分發(fā)機(jī)制债蓝。

2.2 ViewGroup事件分發(fā)機(jī)制

從上面Activity的事件分發(fā)機(jī)制可知趋惨,在Activity.dispatchTouchEvent()實(shí)現(xiàn)了將事件從Activity->ViewGroup的傳遞,ViewGroup的事件分發(fā)機(jī)制從dispatchTouchEvent()開始惦蚊。

源碼分析

/**
  * 源碼分析:ViewGroup.dispatchTouchEvent()
  */ 
  public boolean dispatchTouchEvent(MotionEvent ev) { 

  // 僅貼出關(guān)鍵代碼
  ... 

  if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
  // 分析1:ViewGroup每次事件分發(fā)時(shí)器虾,都需調(diào)用onInterceptTouchEvent()詢問是否攔截事件
    // 判斷值1-disallowIntercept:是否禁用事件攔截的功能(默認(rèn)是false),可通過調(diào)用requestDisallowInterceptTouchEvent()修改
    // 判斷值2-!onInterceptTouchEvent(ev) :對(duì)onInterceptTouchEvent()返回值取反
        // a. 若在onInterceptTouchEvent()中返回false蹦锋,即不攔截事件兆沙,從而進(jìn)入到條件判斷的內(nèi)部
        // b. 若在onInterceptTouchEvent()中返回true,即攔截事件莉掂,從而跳出了該條件判斷
        // c. 關(guān)于onInterceptTouchEvent() ->>分析1

  // 分析2
    // 1. 通過for循環(huán)葛圃,遍歷當(dāng)前ViewGroup下的所有子View
    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);  

            // 2. 判斷當(dāng)前遍歷的View是不是正在點(diǎn)擊的View,從而找到當(dāng)前被點(diǎn)擊的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;  

                // 3. 條件判斷的內(nèi)部調(diào)用了該View的dispatchTouchEvent()
                // 即 實(shí)現(xiàn)了點(diǎn)擊事件從ViewGroup到子View的傳遞(具體請(qǐng)看下面章節(jié)介紹的View事件分發(fā)機(jī)制)
                if (child.dispatchTouchEvent(ev))  { 

                // 調(diào)用子View的dispatchTouchEvent后是有返回值的
                // 若該控件可點(diǎn)擊憎妙,那么點(diǎn)擊時(shí)dispatchTouchEvent的返回值必定是true库正,因此會(huì)導(dǎo)致條件判斷成立
                // 于是給ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
                // 即該子View把ViewGroup的點(diǎn)擊事件消費(fèi)掉了

                mMotionTarget = child;  
                return true; 
                      }  
                  }  
              }  
          }  
      }  
    }  

  ...

  return super.dispatchTouchEvent(ev);
  // 若無任何View接收事件(如點(diǎn)擊空白處)/ViewGroup本身攔截了事件(復(fù)寫了onInterceptTouchEvent()返回true)
  // 會(huì)調(diào)用ViewGroup父類的dispatchTouchEvent()厘唾,即View.dispatchTouchEvent()
  // 因此會(huì)執(zhí)行ViewGroup的onTouch() -> onTouchEvent() -> performClick() -> onClick()褥符,即自己處理該事件,事件不會(huì)往下傳遞
  // 具體請(qǐng)參考View事件分發(fā)機(jī)制中的View.dispatchTouchEvent()

  ... 

}

/**
  * 分析1:ViewGroup.onInterceptTouchEvent()
  * 作用:是否攔截事件
  * 說明:
  *     a. 返回false:不攔截(默認(rèn))
  *     b. 返回true:攔截抚垃,即事件停止往下傳遞(需手動(dòng)復(fù)寫onInterceptTouchEvent()其返回true)
  */
  public boolean onInterceptTouchEvent(MotionEvent ev) {  

    // 默認(rèn)不攔截
    return false;

  } 
  // 回到調(diào)用原處

源碼總結(jié)

Android事件分發(fā)傳遞到Acitivity后喷楣,總是先傳遞到ViewGroup、再傳遞到View鹤树。流程總結(jié)如下:(假設(shè)已經(jīng)經(jīng)過了Acitivity事件分發(fā)傳遞并傳遞到ViewGroup)

image

核心方法總結(jié)

主要包括:dispatchTouchEvent()铣焊、onTouchEvent()onInterceptTouchEvent()總結(jié)如下:

image

實(shí)例分析

1. 布局說明
image
2. 測(cè)試代碼

布局文件:activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/my_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:focusableInTouchMode="true"
    android:orientation="vertical">

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按鈕1" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="按鈕2" />

</LinearLayout>

核心代碼:MainActivity.java

public class MainActivity extends AppCompatActivity {

  Button button1,button2;
  ViewGroup myLayout;

  @Override
  public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);

      button1 = (Button)findViewById(R.id.button1);
      button2 = (Button)findViewById(R.id.button2);
      myLayout = (LinearLayout)findViewById(R.id.my_layout);

      // 1.為ViewGroup布局設(shè)置監(jiān)聽事件
      myLayout.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View v) {
              Log.d("TAG", "點(diǎn)擊了ViewGroup");
          }
      });

      // 2. 為按鈕1設(shè)置監(jiān)聽事件
      button1.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View v) {
              Log.d("TAG", "點(diǎn)擊了button1");
          }
      });

      // 3. 為按鈕2設(shè)置監(jiān)聽事件
      button2.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View v) {
              Log.d("TAG", "點(diǎn)擊了button2");
          }
      });
   }
}

3. 測(cè)試結(jié)果
// 點(diǎn)擊按鈕1罕伯,輸出如下
點(diǎn)擊了button1

// 點(diǎn)擊按鈕2曲伊,輸出如下
點(diǎn)擊了button2

// 點(diǎn)擊空白處,輸出如下
點(diǎn)擊了ViewGroup

4. 結(jié)果分析
  • 點(diǎn)擊Button時(shí)追他,因?yàn)?code>ViewGroup默認(rèn)不攔截坟募,所以事件會(huì)傳遞到子View Button岛蚤,于是執(zhí)行Button.onClick()
  • 此時(shí)ViewGroup. dispatchTouchEvent()會(huì)直接返回true婿屹,所以ViewGroup自身不會(huì)處理該事件灭美,于是ViewGroupLayoutdispatchTouchEvent()不會(huì)執(zhí)行,所以注冊(cè)的onTouch()不會(huì)執(zhí)行昂利,即onTouchEvent()-> performClick()-> onClick()整個(gè)鏈路都不會(huì)執(zhí)行届腐,所以最后不會(huì)執(zhí)行ViewGroup設(shè)置的onClick()里。
  • 點(diǎn)擊空白區(qū)域時(shí)蜂奸,ViewGroup. dispatchTouchEvent()里遍歷所有子View希望找到被點(diǎn)擊子View時(shí)找不到犁苏,所以ViewGroup自身會(huì)處理該事件,于是執(zhí)行onTouchEvent() -> performClick() -> onClick()扩所,最終執(zhí)行ViewGroupLayout的設(shè)置的onClick()围详。

2.3 View事件分發(fā)機(jī)制

從上面ViewGroup事件分發(fā)機(jī)制知道,View事件分發(fā)機(jī)制從dispatchTouchEvent()開始

源碼分析

/**
  * 源碼分析:View.dispatchTouchEvent()
  */
  public boolean dispatchTouchEvent(MotionEvent event) {  

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

        return onTouchEvent(event);  
  }
  // 說明:只有以下3個(gè)條件都為真祖屏,dispatchTouchEvent()才返回true助赞;否則執(zhí)行onTouchEvent()
  //   1. (mViewFlags & ENABLED_MASK) == ENABLED
  //   2. mOnTouchListener != null
  //   3. mOnTouchListener.onTouch(this, event)
  // 下面對(duì)這3個(gè)條件逐個(gè)分析

/**
  * 條件1:(mViewFlags & ENABLED_MASK) == ENABLED
  * 說明:
  *    1. 該條件是判斷當(dāng)前點(diǎn)擊的控件是否enable
  *    2. 由于很多View默認(rèn)enable,故該條件恒定為true(除非手動(dòng)設(shè)置為false)
  */

/**
  * 條件2:mOnTouchListener != null
  * 說明:
  *   1. mOnTouchListener變量在View.setOnTouchListener()里賦值
  *   2. 即只要給控件注冊(cè)了Touch事件袁勺,mOnTouchListener就一定被賦值(即不為空)
  */
  public void setOnTouchListener(OnTouchListener l) { 

    mOnTouchListener = l;  

} 

/**
  * 條件3:mOnTouchListener.onTouch(this, event)
  * 說明:
  *   1. 即回調(diào)控件注冊(cè)Touch事件時(shí)的onTouch()雹食;
  *   2. 需手動(dòng)復(fù)寫設(shè)置,具體如下(以按鈕Button為例)
  */
  button.setOnTouchListener(new OnTouchListener() {  
      @Override  
      public boolean onTouch(View v, MotionEvent event) {  

        return false;  
        // 若在onTouch()返回true期丰,就會(huì)讓上述三個(gè)條件全部成立群叶,從而使得View.dispatchTouchEvent()直接返回true,事件分發(fā)結(jié)束
        // 若在onTouch()返回false钝荡,就會(huì)使得上述三個(gè)條件不全部成立街立,從而使得View.dispatchTouchEvent()中跳出If,執(zhí)行onTouchEvent(event)
        // onTouchEvent()源碼分析 -> 分析1
      }  
  });

/**
  * 分析1:onTouchEvent()
  */
  public boolean onTouchEvent(MotionEvent event) {  

    ... // 僅展示關(guān)鍵代碼

    // 若該控件可點(diǎn)擊埠通,則進(jìn)入switch判斷中
    if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  

        // 根據(jù)當(dāng)前事件類型進(jìn)行判斷處理
        switch (event.getAction()) { 

            // a. 事件類型=抬起View(主要分析)
            case MotionEvent.ACTION_UP:  
                    performClick(); 
                    // ->>分析2
                    break;  

            // b. 事件類型=按下View
            case MotionEvent.ACTION_DOWN:  
                postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());  
                break;  

            // c. 事件類型=結(jié)束事件
            case MotionEvent.ACTION_CANCEL:  
                refreshDrawableState();  
                removeTapCallback();  
                break;

            // d. 事件類型=滑動(dòng)View
            case MotionEvent.ACTION_MOVE:  
                final int x = (int) event.getX();  
                final int y = (int) event.getY();  

                int slop = mTouchSlop;  
                if ((x < 0 - slop) || (x >= getWidth() + slop) ||  
                        (y < 0 - slop) || (y >= getHeight() + slop)) {  
                    removeTapCallback();  
                    if ((mPrivateFlags & PRESSED) != 0) {  
                        removeLongPressCallback();  
                        mPrivateFlags &= ~PRESSED;  
                        refreshDrawableState();  
                    }  
                }  
                break;  
        }  

        // 若該控件可點(diǎn)擊赎离,就一定返回true
        return true;  
    }  
  // 若該控件不可點(diǎn)擊,就一定返回false
  return false;  
}

/**
  * 分析2:performClick()
  */  
  public boolean performClick() {  

      if (mOnClickListener != null) {
          // 只要通過setOnClickListener()為控件View注冊(cè)1個(gè)點(diǎn)擊事件
          // 那么就會(huì)給mOnClickListener變量賦值(即不為空)
          // 則會(huì)往下回調(diào)onClick() & performClick()返回true
          playSoundEffect(SoundEffectConstants.CLICK);  
          mOnClickListener.onClick(this);  
          return true;  
      }  
      return false;  
  }  

源碼總結(jié)

image

這里需要特別注意的是植阴,onTouch()的執(zhí)行 先于onClick()

核心方法總結(jié)

主要包括: dispatchTouchEvent()蟹瘾、onTouchEvent()

image

實(shí)例分析

在本示例中,將分析2種情況:

  1. 注冊(cè)Touch事件監(jiān)聽 且 在onTouch()返回false
  2. 注冊(cè)Touch事件監(jiān)聽 且 在onTouch()返回true
分析1:注冊(cè)Touch事件監(jiān)聽 且 在onTouch()返回false
代碼示例
// 1. 注冊(cè)Touch事件監(jiān)聽setOnTouchListener 且 在onTouch()返回false
button.setOnTouchListener(new View.OnTouchListener() {

      @Override
      public boolean onTouch(View v, MotionEvent event) {
          System.out.println("執(zhí)行了onTouch(), 動(dòng)作是:" + event.getAction());
          return false;
      }
});

// 2. 注冊(cè)點(diǎn)擊事件OnClickListener()
button.setOnClickListener(new View.OnClickListener() {

      @Override
      public void onClick(View v) {
          System.out.println("執(zhí)行了onClick()");
      }
});

測(cè)試結(jié)果
執(zhí)行了onTouch(), 動(dòng)作是:0
執(zhí)行了onTouch(), 動(dòng)作是:1
執(zhí)行了onClick()

測(cè)試結(jié)果說明
  • 點(diǎn)擊按鈕會(huì)產(chǎn)生兩個(gè)類型的事件-按下View與抬起View掠手,所以會(huì)回調(diào)兩次onTouch()
  • 因?yàn)?code>onTouch()返回了false狸捕,所以事件無被消費(fèi)喷鸽,會(huì)繼續(xù)往下傳遞,即調(diào)用View.onTouchEvent()灸拍;
  • 調(diào)用View.onTouchEvent()時(shí)做祝,對(duì)于抬起View事件砾省,在調(diào)用performClick()時(shí),因?yàn)樵O(shè)置了點(diǎn)擊事件混槐,所以會(huì)回調(diào)onClick()编兄。
分析2:注冊(cè)Touch事件監(jiān)聽 且 在onTouch()返回true
代碼示例
// 1. 注冊(cè)Touch事件監(jiān)聽setOnTouchListener 且 在onTouch()返回false
button.setOnTouchListener(new View.OnTouchListener() {

        @Override
        public boolean onTouch(View v, MotionEvent event) {
            System.out.println("執(zhí)行了onTouch(), 動(dòng)作是:" + event.getAction());
            return true;
        }
    });

// 2. 注冊(cè)點(diǎn)擊事件OnClickListener()
button.setOnClickListener(new View.OnClickListener() {

        @Override
        public void onClick(View v) {
            System.out.println("執(zhí)行了onClick()");
        }

  });

測(cè)試結(jié)果
執(zhí)行了onTouch(), 動(dòng)作是:0
執(zhí)行了onTouch(), 動(dòng)作是:1

測(cè)試結(jié)果說明
  • 點(diǎn)擊按鈕會(huì)產(chǎn)生兩個(gè)類型的事件-按下View與抬起View,所以會(huì)回調(diào)兩次onTouch()声登;
  • 因?yàn)?code>onTouch()返回true狠鸳,所以事件被消費(fèi),不會(huì)繼續(xù)往下傳遞悯嗓,View.dispatchTouchEvent()直接返回true件舵;
  • 所以最終不會(huì)調(diào)用View.onTouchEvent(),也不會(huì)調(diào)用onClick()脯厨。

三. 事件分發(fā)機(jī)制流程總結(jié)

image

這個(gè)三個(gè)方法均有一個(gè) boolean(布爾) 類型的返回值铅祸,通過返回 true 和 false 來控制事件傳遞的流程。
PS: 從上表可以看到 ActivityView都是沒有事件攔截的合武,這是因?yàn)椋?/p>

  • Activity 作為原始的事件分發(fā)者临梗,如果 Activity 攔截了事件會(huì)導(dǎo)致整個(gè)屏幕都無法響應(yīng)事件,這肯定不是我們想要的效果稼跳。
  • View最為事件傳遞的最末端盟庞,要么消費(fèi)掉事件,要么不處理進(jìn)行回傳岂贩,根本沒必要進(jìn)行事件攔截茫经。

View相關(guān)

Question: 為什么 View 會(huì)有 dispatchTouchEvent ?

A:View 可以注冊(cè)很多事件監(jiān)聽器,例如:?jiǎn)螕羰录?onClick)萎津、長(zhǎng)按事件(onLongClick)卸伞、觸摸事件(onTouch),并且View自身也有onTouchEvent方法锉屈,那么問題來了荤傲,這么多與事件相關(guān)的方法應(yīng)該由誰管理?毋庸置疑就是 dispatchTouchEvent颈渊,所以 View 也會(huì)有事件分發(fā)遂黍。

Question: 與 View 事件相關(guān)的各個(gè)方法調(diào)用順序是怎樣的?

A:如果不去看源碼俊嗽,想一下讓自己設(shè)計(jì)會(huì)怎樣雾家?

  • 單擊事件(onClickListener) 需要兩個(gè)事件(ACTION_DOWNACTION_UP)才能觸發(fā),如果先分配給onClick判斷绍豁,等它判斷完芯咧,用戶手指已經(jīng)離開屏幕,黃花菜都涼了,定然造成 View 無法響應(yīng)其他事件敬飒,應(yīng)該最后調(diào)用邪铲。(最后)
  • 長(zhǎng)按事件(onLongClickListener) 同理,也是需要長(zhǎng)時(shí)間等待才能出結(jié)果无拗,肯定不能排到前面带到,但因?yàn)椴恍枰?code>ACTION_UP,應(yīng)該排在onClick前面英染。(onLongClickListener> onClickListener)
  • 觸摸事件(onTouchListener) 揽惹, 如果用戶注冊(cè)了觸摸事件,說明用戶要自己處理觸摸事件税迷,這個(gè)應(yīng)該排在最前面永丝。(最前)、
  • View自身處理(onTouchEvent) 提供了一種默認(rèn)的處理方式箭养,如果用戶已經(jīng)處理好了慕嚷,也就不需要了,所以應(yīng)該排在onTouchListener后面毕泌。(onTouchListener > onTouchEvent)

所以事件的調(diào)度順序應(yīng)該是 onTouchListener> onTouchEvent > onLongClickListener> onClickListener喝检。

ViewGroup相關(guān)

ViewGroup(通常是各種Layout) 的事件分發(fā)相對(duì)來說就要麻煩一些,因?yàn)?ViewGroup 不僅要考慮自身撼泛,還要考慮各種ChildView挠说,一旦處理不好就容易引起各種事件沖突,正所謂養(yǎng)兒方知父母難啊愿题。
VIewGroup的事件分發(fā)流程又是如何的呢损俭?

我們了解到事件是通過ViewGroup一層一層傳遞的,最終傳遞給View潘酗,ViewGroup要比它的 ChildView 先拿到事件杆兵,并且有權(quán)決定是否告訴要告訴ChildView。在默認(rèn)的情況下 ViewGroup事件分發(fā)流程是這樣的仔夺。

  1. 判斷自身是否需要(詢問 onInterceptTouchEvent 是否攔截)琐脏,如果需要,調(diào)用自己的 onTouchEvent缸兔。
  2. 自身不需要或者不確定日裙,則詢問ChildView,一般來說是調(diào)用手指觸摸位置的 ChildView惰蜜。
  3. 如果子 ChildView不需要?jiǎng)t調(diào)用自身的onTouchEvent昂拂。

用偽代碼應(yīng)該是這樣子的:

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

    boolean consume = false; //代表 是否會(huì)消費(fèi)事件

    // 步驟2:判斷是否攔截事件
    if (onInterceptTouchEvent(ev)) {
      // a. 若攔截,則將該事件交給當(dāng)前View進(jìn)行處理
      // 即調(diào)用onTouchEvent()去處理點(diǎn)擊事件
      consume = onTouchEvent (ev) ;

    } else {

      // b. 若不攔截抛猖,則將該事件傳遞到下層
      // 即 下層元素的dispatchTouchEvent()就會(huì)被調(diào)用政钟,重復(fù)上述過程
      // 直到點(diǎn)擊事件被最終處理為止
      consume = child.dispatchTouchEvent (ev) ;
    }

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

安卓為了保證所有的事件都是被一個(gè) View 消費(fèi)的路克,對(duì)第一次的事件( ACTION_DOWN )進(jìn)行了特殊判斷樟结,View 只有消費(fèi)了 ACTION_DOWN 事件养交,才能接收到后續(xù)的事件(可點(diǎn)擊控件會(huì)默認(rèn)消費(fèi)所有事件),并且會(huì)將后續(xù)所有事件傳遞過來瓢宦,不會(huì)再傳遞給其他 View碎连,除非上層 View 進(jìn)行了攔截。如果上層 View 攔截了當(dāng)前正在處理的事件驮履,會(huì)收到一個(gè) ACTION_CANCEL鱼辙,表示當(dāng)前事件已經(jīng)結(jié)束,后續(xù)事件不會(huì)再傳遞過來玫镐。

核心要點(diǎn)

  1. 事件分發(fā)原理: 責(zé)任鏈模式倒戏,事件層層傳遞,直到被消費(fèi)恐似。
  2. View 的 dispatchTouchEvent主要用于調(diào)度自身的監(jiān)聽器和 onTouchEvent杜跷。
  3. View的事件的調(diào)度順序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener
  4. 不論View自身是否注冊(cè)點(diǎn)擊事件矫夷,只要 View 是可點(diǎn)擊的就會(huì)消費(fèi)事件葛闷。
  5. 事件是否被消費(fèi)由返回值決定,true 表示消費(fèi)双藕,false 表示不消費(fèi)淑趾,與是否使用了事件無關(guān)。
  6. ViewGroup 中可能有多個(gè) ChildView 時(shí)忧陪,將事件分配給包含點(diǎn)擊位置的 ChildView扣泊。
  7. ViewGroup 和 ChildView 同時(shí)注冊(cè)了事件監(jiān)聽器(onClick等),由 ChildView 消費(fèi)嘶摊。
  8. 一次觸摸流程中產(chǎn)生事件應(yīng)被同一 View 消費(fèi)延蟹,全部接收或者全部拒絕。
  9. 只要接受 ACTION_DOWN 就意味著接受所有的事件更卒,拒絕 ACTION_DOWN 則不會(huì)收到后續(xù)內(nèi)容等孵。
  10. 如果當(dāng)前正在處理的事件被上層 View 攔截,會(huì)收到一個(gè) ACTION_CANCEL蹂空,后續(xù)事件不會(huì)再傳遞過來俯萌。

原文鏈接:https://www.haomeiwen.com/subject/atfkartx.htmlGoodSong

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末上枕,一起剝皮案震驚了整個(gè)濱河市咐熙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辨萍,老刑警劉巖棋恼,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件返弹,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡爪飘,警方通過查閱死者的電腦和手機(jī)义起,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來师崎,“玉大人默终,你說我怎么就攤上這事±缯郑” “怎么了齐蔽?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)床估。 經(jīng)常有香客問我含滴,道長(zhǎng),這世上最難降的妖魔是什么丐巫? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任谈况,我火速辦了婚禮,結(jié)果婚禮上鞋吉,老公的妹妹穿的比我還像新娘鸦做。我一直安慰自己,他們只是感情好谓着,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布泼诱。 她就那樣靜靜地躺著,像睡著了一般赊锚。 火紅的嫁衣襯著肌膚如雪治筒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天舷蒲,我揣著相機(jī)與錄音耸袜,去河邊找鬼。 笑死牲平,一個(gè)胖子當(dāng)著我的面吹牛堤框,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播纵柿,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼蜈抓,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了昂儒?” 一聲冷哼從身側(cè)響起沟使,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎渊跋,沒想到半個(gè)月后腊嗡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體着倾,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年燕少,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了卡者。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡棺亭,死狀恐怖虎眨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情镶摘,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布岳守,位于F島的核電站凄敢,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏湿痢。R本人自食惡果不足惜涝缝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望譬重。 院中可真熱鬧拒逮,春花似錦、人聲如沸臀规。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽塔嬉。三九已至玩徊,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谨究,已是汗流浹背恩袱。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留胶哲,地道東北人畔塔。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像鸯屿,于是被迫代替她去往敵國和親澈吨。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

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