Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

南方吳彥祖_藍斯發表於2021-11-05

前言

  • Android 事件分發機制是 Android 開發者必須瞭解的基礎

  • 網上有大量關於 Android 事件分發機制的文章,但存在一些問題: 內容不全、思路不清晰、無原始碼分析、簡單問題複雜化等等

  • 今天,我將全面總結 Android 的事件分發機制,我能保證這是 市面上的最全面、最清晰、最易懂的

  1. 本文秉著“結論先行、詳細分析在後”的原則,即先讓大家感性認識,再透過理性分析從而理解問題;

  2. 所以,請各位讀者先記住結論,再往下繼續看分析;

  • 文章較長,閱讀需要較長時間,建議收藏等充足時間再進行閱讀

目錄

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制


1 . 基礎認知

1.1 事件分發的”事件“是指什麼?

答:點選事件( Touch 事件) 。具體介紹如下:

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

image

此處需要特別說明:事件列,即指從手指接觸螢幕至手指離開螢幕這個過程產生的一系列事件。一般情況下,事件列都是以DOWN事件開始、UP事件結束,中間有無數的MOVE事件。

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

1.2 事件分發的本質

答:將點選事件(MotionEvent)傳遞到某個具體的 View & 處理的整個過程

即 事件傳遞的過程 = 分發過程。

1.3 事件在哪些物件之間進行傳遞?

答:Activity、ViewGroup、View Android UI 介面由 Activity ViewGroup View 及其派生類組成

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

1.4 事件分發的順序

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

即:1個點選事件發生後,事件先傳到 Activity 、再傳到 ViewGroup 、最終再傳到 View

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

1.5 事件分發過程由哪些方法協作完成?

答:dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

下文會對這3個方法進行詳細介紹

1.6 總結

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

  • 至此,相信大家已經對 Android 的事件分發有了感性的認知

  • 下面,我將詳細介紹 Android 事件分發機制

2 . 事件分發機制流程概述

Android 事件分發流程 = Activity -> ViewGroup -> View

即:1個點選事件發生後,事件先傳到 Activity 、再傳到 ViewGroup 、最終再傳到 View

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

即要想充分理解Android分發機制,本質上是要理解:

  1. Activity 對點選事件的分發機制

  2. ViewGroup 對點選事件的分發機制

  3. View 對點選事件的分發機制

下面,我將透過原始碼,全面解析 事件分發機制

即按順序講解: Activity 事件分發機制、 ViewGroup 事件分發機制、 View 事件分發機制


3 . 事件分發機制流程詳細分析

主要包括: Activity 事件分發機制、 ViewGroup 事件分發機制、 View 事件分發機制

流程1:Activity的事件分發機制

Android事件分發機制首先會將點選事件傳遞到Activity中,具體是執行dispatchTouchEvent()進行事件分發。

原始碼分析

/**

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

   // 僅貼出核心程式碼

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

       return true;
       // 若getWindow().superDispatchTouchEvent(ev)的返回true
       // 則Activity.dispatchTouchEvent()就返回true,則方法結束。即 :該點選事件停止往下傳遞 & 事件傳遞過程結束
       // 否則:繼續往下呼叫Activity.onTouchEvent

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

/**
 * 分析1:getWindow().superDispatchTouchEvent(ev)
 * 說明:
 *     a. getWindow() = 獲取Window類的物件
 *     b. Window類是抽象類,其唯一實現類 = PhoneWindow類
 *     c. Window類的superDispatchTouchEvent() = 1個抽象方法,由子類PhoneWindow類實現
 */
 @Override
 public boolean superDispatchTouchEvent(MotionEvent event) {

     return mDecor.superDispatchTouchEvent(event);
     // mDecor = 頂層View(DecorView)的例項物件
     // ->> 分析2
 }

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

     return super.dispatchTouchEvent(event);
     // 呼叫父類的方法 = ViewGroup的dispatchTouchEvent()
     // 即將事件傳遞到ViewGroup去處理,詳細請看後續章節分析的ViewGroup的事件分發機制

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

/**
 * 分析3:Activity.onTouchEvent()
 * 呼叫場景:當一個點選事件未被Activity下任何一個View接收/處理時,就會呼叫該方法
 */
 public boolean onTouchEvent(MotionEvent event) {

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

       return false;
       // 即 只有在點選事件在Window邊界外才會返回true,一般情況都返回false,分析完畢
   }

/**
 * 分析4:mWindow.shouldCloseOnTouch(this, event)
 * 作用:主要是對於處理邊界外點選事件的判斷:是否是DOWN事件,event的座標是否在邊界內等
 */
 public boolean shouldCloseOnTouch(Context context, MotionEvent event) {

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

       // 返回true:說明事件在邊界外,即 消費事件
       return true;
   }

   // 返回false:在邊界內,即未消費(預設)
   return false;
 }

原始碼總結

當一個點選事件發生時,從 Activity 的事件分發開始( Activity.dispatchTouchEvent() ),流程總結如下:

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

核心方法總結

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

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

那麼, ViewGroup dispatchTouchEvent() 什麼時候返回 true / false ?請繼續往下看 ViewGroup事件的分發機制

流程2: ViewGroup的事件分發機制

從上面Activity的事件分發機制可知,在Activity.dispatchTouchEvent()實現了將事件從Activity->ViewGroup的傳遞,ViewGroup的事件分發機制從dispatchTouchEvent()開始。

原始碼分析

/**

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

 // 僅貼出關鍵程式碼
 ...

 if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
 // 分析1:ViewGroup每次事件分發時,都需呼叫onInterceptTouchEvent()詢問是否攔截事件
   // 判斷值1-disallowIntercept:是否禁用事件攔截的功能(預設是false),可透過呼叫requestDisallowInterceptTouchEvent()修改
   // 判斷值2-!onInterceptTouchEvent(ev) :對onInterceptTouchEvent()返回值取反
       // a. 若在onInterceptTouchEvent()中返回false,即不攔截事件,從而進入到條件判斷的內部
       // b. 若在onInterceptTouchEvent()中返回true,即攔截事件,從而跳出了該條件判斷
       // c. 關於onInterceptTouchEvent() ->>分析1

 // 分析2
   // 1\. 透過for迴圈,遍歷當前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\. 判斷當前遍歷的View是不是正在點選的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;  

               // 3\. 條件判斷的內部呼叫了該View的dispatchTouchEvent()
               // 即 實現了點選事件從ViewGroup到子View的傳遞(具體請看下面章節介紹的View事件分發機制)
               if (child.dispatchTouchEvent(ev))  {

               // 呼叫子View的dispatchTouchEvent後是有返回值的
               // 若該控制元件可點選,那麼點選時dispatchTouchEvent的返回值必定是true,因此會導致條件判斷成立
               // 於是給ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
               // 即該子View把ViewGroup的點選事件消費掉了

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

 ...

 return super.dispatchTouchEvent(ev);
 // 若無任何View接收事件(如點選空白處)/ViewGroup本身攔截了事件(複寫了onInterceptTouchEvent()返回true)
 // 會呼叫ViewGroup父類的dispatchTouchEvent(),即View.dispatchTouchEvent()
 // 因此會執行ViewGroup的onTouch() -> onTouchEvent() -> performClick() -> onClick(),即自己處理該事件,事件不會往下傳遞
 // 具體請參考View事件分發機制中的View.dispatchTouchEvent()

 ...

}

/**
 * 分析1:ViewGroup.onInterceptTouchEvent()
 * 作用:是否攔截事件
 * 說明:
 *     a. 返回false:不攔截(預設)
 *     b. 返回true:攔截,即事件停止往下傳遞(需手動複寫onInterceptTouchEvent()其返回true)
 */
 public boolean onInterceptTouchEvent(MotionEvent ev) {  

   // 預設不攔截
   return false;

 }
 // 回到呼叫原處

原始碼總結

Android 事件分發傳遞到Acitivity後,總是先傳遞到 ViewGroup 、再傳遞到 View 。流程總結如下:(假設已經經過了Acitivity事件分發傳遞並傳遞到ViewGroup)

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

核心方法總結

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

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

例項分析

1 . 佈局說明

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

2 . 測試程式碼

佈局檔案: activity _ main.xml

<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="
   android:id="@+id/my_layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   xmlns:app="
   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佈局設定監聽事件
     myLayout.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View v) {
             Log.d("TAG", "點選了ViewGroup");
         }
     });

     // 2\. 為按鈕1設定監聽事件
     button1.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View v) {
             Log.d("TAG", "點選了button1");
         }
     });

     // 3\. 為按鈕2設定監聽事件
     button2.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View v) {
             Log.d("TAG", "點選了button2");
         }
     });
  }
}

3 . 測試結果

// 點選按鈕1,輸出如下

點選了button1

// 點選按鈕2,輸出如下
點選了button2

// 點選空白處,輸出如下
點選了ViewGroup

4 . 結果分析

  • 點選Button時,因為ViewGroup預設不攔截,所以事件會傳遞到子View Button,於是執行Button.onClick()。

  • 此時ViewGroup. dispatchTouchEvent()會直接返回true,所以ViewGroup自身不會處理該事件,於是ViewGroupLayout的dispatchTouchEvent()不會執行,所以註冊的onTouch()不會執行,即onTouchEvent() -> performClick() -> onClick()整個鏈路都不會執行,所以最後不會執行ViewGroup設定的onClick()裡。

  • 點選空白區域時,ViewGroup. dispatchTouchEvent()裡遍歷所有子View希望找到被點選子View時找不到,所以ViewGroup自身會處理該事件,於是執行onTouchEvent() -> performClick() -> onClick(),最終執行ViewGroupLayout的設定的onClick()

    • *

流程3:View的事件分發機制

從上面 ViewGroup 事件分發機制知道, View 事件分發機制從 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個條件都為真,dispatchTouchEvent()才返回true;否則執行onTouchEvent()
 //   1\. (mViewFlags & ENABLED_MASK) == ENABLED
 //   2\. mOnTouchListener != null
 //   3\. mOnTouchListener.onTouch(this, event)
 // 下面對這3個條件逐個分析

/**
 * 條件1:(mViewFlags & ENABLED_MASK) == ENABLED
 * 說明:
 *    1\. 該條件是判斷當前點選的控制元件是否enable
 *    2\. 由於很多View預設enable,故該條件恆定為true(除非手動設定為false)
 */

/**
 * 條件2:mOnTouchListener != null
 * 說明:
 *   1\. mOnTouchListener變數在View.setOnTouchListener()裡賦值
 *   2\. 即只要給控制元件註冊了Touch事件,mOnTouchListener就一定被賦值(即不為空)
 */
 public void setOnTouchListener(OnTouchListener l) {

   mOnTouchListener = l;  

}

/**
 * 條件3:mOnTouchListener.onTouch(this, event)
 * 說明:
 *   1\. 即回撥控制元件註冊Touch事件時的onTouch();
 *   2\. 需手動複寫設定,具體如下(以按鈕Button為例)
 */
 button.setOnTouchListener(new OnTouchListener() {  
     @Override  
     public boolean onTouch(View v, MotionEvent event) {  

       return false;  
       // 若在onTouch()返回true,就會讓上述三個條件全部成立,從而使得View.dispatchTouchEvent()直接返回true,事件分發結束
       // 若在onTouch()返回false,就會使得上述三個條件不全部成立,從而使得View.dispatchTouchEvent()中跳出If,執行onTouchEvent(event)
       // onTouchEvent()原始碼分析 -> 分析1
     }  
 });

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

   ... // 僅展示關鍵程式碼

   // 若該控制元件可點選,則進入switch判斷中
   if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {  

       // 根據當前事件型別進行判斷處理
       switch (event.getAction()) {

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

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

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

           // d. 事件型別=滑動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;  
       }  

       // 若該控制元件可點選,就一定返回true
       return true;  
   }  
 // 若該控制元件不可點選,就一定返回false
 return false;  
}

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

     if (mOnClickListener != null) {
         // 只要透過setOnClickListener()為控制元件View註冊1個點選事件
         // 那麼就會給mOnClickListener變數賦值(即不為空)
         // 則會往下回撥onClick() & performClick()返回true
         playSoundEffect(SoundEffectConstants.CLICK);  
         mOnClickListener.onClick(this);  
         return true;  
     }  
     return false;  
 }  

原始碼總結

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

image

這裡需要特別注意的是, onTouch() 的執行 先於 onClick()

核心方法總結

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

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

例項分析

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

  1. 註冊Touch事件監聽 且 在onTouch()返回false

  2. 註冊Touch事件監聽 且 在onTouch()返回true

分析1:註冊Touch事件監聽 且 在onTouch()返回false

程式碼示例
// 1\. 註冊Touch事件監聽setOnTouchListener 且 在onTouch()返回false

button.setOnTouchListener(new View.OnTouchListener() {

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

// 2\. 註冊點選事件OnClickListener()
button.setOnClickListener(new View.OnClickListener() {

     @Override
     public void onClick(View v) {
         System.out.println("執行了onClick()");
     }
});
測試結果
執行了onTouch(), 動作是:0

執行了onTouch(), 動作是:1
執行了onClick()
測試結果說明
  • 點選按鈕會產生兩個型別的事件-按下View與抬起View,所以會回撥兩次onTouch();

  • 因為onTouch()返回了false,所以事件無被消費,會繼續往下傳遞,即呼叫View.onTouchEvent();

  • 呼叫View.onTouchEvent()時,對於抬起View事件,在呼叫performClick()時,因為設定了點選事件,所以會回撥onClick()。

分析2:註冊Touch事件監聽 且 在onTouch()返回true

程式碼示例
// 1\. 註冊Touch事件監聽setOnTouchListener 且 在onTouch()返回false

button.setOnTouchListener(new View.OnTouchListener() {

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

// 2\. 註冊點選事件OnClickListener()
button.setOnClickListener(new View.OnClickListener() {

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

 });
測試結果
執行了onTouch(), 動作是:0

執行了onTouch(), 動作是:1
測試結果說明
  • 點選按鈕會產生兩個型別的事件-按下View與抬起View,所以會回撥兩次onTouch();

  • 因為onTouch()返回了true,所以事件被消費,不會繼續往下傳遞,View.dispatchTouchEvent()直接返回true;

  • 所以最終不會呼叫View.onTouchEvent(),也不會呼叫onClick()。

至此,關於Android事件分發機制的內容已經講解完畢,即Activity、ViewGroup、View的事件分發機制。

即: Activity ViewGroup View 的事件分發機制


4 . 總結

在本章節中,將採用大量的圖表從各個角度對Android事件分發機制進行總結。主要包括:

  • 工作流程總結

  • 業務流程總結

  • 以分發物件為核心的總結

  • 以方法為核心的總結

4.1 工作流程-總結

Android事件分發流程 = Activity -> ViewGroup -> View,即:1個點選事件發生後,事件先傳到Activity、再傳到ViewGroup、最終再傳到View。

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

4.2 以分發物件為核心-總結

分發物件主要包括:Activity、ViewGroup、View。

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

4.3 以方法為核心-總結

事件分發的方法主要包括:dispatchTouchEvent()、onInterceptTouchEvent()和onTouchEvent()。

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

這裡需要特別注意的是:

  • 注意點1:左側虛線代表具備相關性及逐層返回;

  • 注意點2:各層dispatchTouchEvent() 返回true的情況保持一致(圖中虛線)   原因是:上層dispatchTouchEvent() 的返回true情況 取決於 下層dispatchTouchEvent() 是否返回ture,如Activity.dispatchTouchEvent() 返回true的情況 = ViewGroup.dispatchTouchEvent() 返回true

  • 注意點3:各層dispatchTouchEvent() 與 onTouchEvent()的返回情況保持一致   原因:最下層View的dispatchTouchEvent()的返回值 取決於 View.onTouchEvent()的返回值;結合注意點1,逐層往上返回,從而保持一致。

下面,將針對該3個方法,分別針對預設執行邏輯、返回true、返回false的三種情況進行流程圖示意。

方法1:dispatchTouchEvent()

預設執行邏輯、返回true、返回false 這三種情況的返回邏輯分別如下所示。

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

方法2:onInterceptTouchEvent()

預設執行邏輯、返回true、返回false 這三種情況的返回邏輯分別如下所示。

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

這裡需要特別注意的是: Activity View 都無該方法,僅 ViewGroup 特有。

方法3:onTouchEvent()

預設執行邏輯、返回true、返回false 這三種情況的返回邏輯分別如下所示。

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

三者關係

下面,我用一段虛擬碼來闡述上述3個方法的關係 & 事件傳遞規則

// 點選事件產生後

// 步驟1:呼叫dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {

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

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

   } else {

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

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

}

5 . 常見事件分發場景

下面,我將透過例項說明 常見的事件傳遞情況 & 流程

5.1 背景描述

  • 討論的佈局如下:

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

  • 情景

    1. 使用者先觸控到螢幕上 View C 上的某個點(圖中黃區)

    Action_DOWN 事件在此處產生

    1. 使用者移動手指

    2. 最後離開螢幕

5.2 一般的事件傳遞情況

一般的事件傳遞場景有:

  • 預設情況

  • 處理事件

  • 攔截 DOWN 事件

  • 攔截後續事件( MOVE UP

場景1:預設

  • 即不對控制元件裡的方法( dispatchTouchEvent() onTouchEvent() onInterceptTouchEvent() )進行重寫 或 更改返回值

  • 那麼呼叫的是這3個方法的預設實現:呼叫下層的方法 & 逐層返回

  • 事件傳遞情況:(呈 U 型)

    1. 從上往下呼叫dispatchTouchEvent()

    Activity A ->> ViewGroup B ->> View C

    1. 從下往上呼叫onTouchEvent()

    View C ->> ViewGroup B ->> Activity A

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

注:雖然 ViewGroup B onInterceptTouchEvent ()對 DOWN 事件返回了 false ,但後續的事件 (MOVE、UP) 依然會傳遞給它的 onInterceptTouchEvent()   這一點與 onTouchEvent() 的行為是不一樣的:不再傳遞 & 接收該事件列的其他事件

場景2:處理事件

View C 希望處理該點選事件,即:設定 View C 為可點選的 (Clickable) 或 複寫其 onTouchEvent() 返回 true

最常見的:設定 Button 按鈕來響應點選事件

事件傳遞情況:(如下圖)

  • DOWN 事件被傳遞給C的 onTouchEvent 方法,該方法返回 true ,表示處理該事件

  • 因為 View C 正在處理該事件,那麼 DOWN 事件將不再往上傳遞給ViewGroup B 和 Activity A onTouchEvent()

  • 該事件列的其他事件 (Move、Up) 也將傳遞給 View C onTouchEvent()

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

會逐層往 dispatchTouchEvent() 返回,最終事件分發結束

場景3:攔截DOWN事件

假設 ViewGroup B 希望處理該點選事件,即 ViewGroup B 複寫了 onInterceptTouchEvent() 返回 true onTouchEvent() 返回 true   事件傳遞情況:(如下圖)

  • DOWN 事件被傳遞給 ViewGroup B onInterceptTouchEvent() ,該方法返回 true ,表示攔截該事件,即自己處理該事件(事件不再往下傳遞)

  • 呼叫自身的 onTouchEvent() 處理事件( DOWN 事件將不再往上傳遞給 Activity A onTouchEvent()

  • 該事件列的其他事件 (Move、Up) 將直接傳遞給 ViewGroup B onTouchEvent()

注:

  1. 該事件列的其他事件 (Move、Up) 將不會再傳遞給 ViewGroup B onInterceptTouchEvent ();因:該方法一旦返回一次 true ,就再也不會被呼叫

  2. 逐層往 dispatchTouchEvent() 返回,最終事件分發結束

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

場景4:攔截DOWN的後續事件

結論

  • ViewGroup 攔截了一個半路的事件(如 MOVE ),該事件將會被系統變成一個 CANCEL 事件 & 傳遞給之前處理該事件的子 View

  • 該事件不會再傳遞給 ViewGroup onTouchEvent()

  • 只有再到來的事件才會傳遞到 ViewGroup onTouchEvent()

場景描述   ViewGroup B 無攔截 DOWN 事件(還是 View C 來處理 DOWN 事件),但它攔截了接下來的 MOVE 事件

DOWN 事件傳遞到 View C onTouchEvent() ,返回了 true

例項講解

  • 在後續到來的MOVE事件, ViewGroup B onInterceptTouchEvent() 返回 true 攔截該 MOVE 事件,但該事件並沒有傳遞給 ViewGroup B ;這個 MOVE 事件將會被系統變成一個 CANCEL 事件傳遞給 View C onTouchEvent()

  • 後續又來了一個 MOVE 事件,該 MOVE 事件才會直接傳遞給 ViewGroup B onTouchEvent()

後續事件將直接傳遞給 ViewGroup B onTouchEvent() 處理,而不會再傳遞給 ViewGroup B onInterceptTouchEvent() ,因該方法一旦返回一次true,就再也不會被呼叫了。

  • View C 再也不會收到該事件列產生的後續事件

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

至此,關於 Android 常見的事件傳遞情況 & 流程已經講解完畢。


6 . 特殊說明

6.1 Touch事件的後續事件(MOVE、UP)層級傳遞

  • 若給控制元件註冊了 Touch 事件,每次點選都會觸發一系列 action 事件(ACTION _ DOWN,ACTION _ MOVE,ACTION _ UP等)

  • dispatchTouchEvent() 事件分發時,只有前一個事件(如ACTION _ DOWN)返回true,才會收到後一個事件(ACTION _ MOVE和ACTION _ UP)

即如果在執行ACTION _ DOWN時返回false,後面一系列的ACTION _ MOVE、ACTION _ UP事件都不會執行

從上面對事件分發機制分析知:

  • dispatchTouchEvent()、 onTouchEvent() 消費事件、終結事件傳遞(返回true)

  • 而onInterceptTouchEvent 並不能消費事件,它相當於是一個分叉口起到分流導流的作用,對後續的ACTION _ MOVE和ACTION _ UP事件接收起到非常大的作用

請記住:接收了ACTION _ DOWN事件的函式不一定能收到後續事件(ACTION _ MOVE、ACTION _ UP)

這裡給出ACTION _ MOVE和ACTION _ UP事件的傳遞結論

  • 結論1   若物件(Activity、ViewGroup、View)的dispatchTouchEvent()分發事件後消費了事件(返回true),那麼收到ACTION _ DOWN的函式也能收到ACTION _ MOVE和ACTION _ UP

黑線:ACTION _ DOWN事件傳遞方向   紅線:ACTION _ MOVE 、 ACTION _ UP事件傳遞方向

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

  • 結論2   若物件(Activity、ViewGroup、View)的onTouchEvent()處理了事件(返回true),那麼ACTION _ MOVE、ACTION _ UP的事件從上往下傳到該 View 後就不再往下傳遞,而是直接傳給自己的 onTouchEvent() & 結束本次事件傳遞過程。

黑線:ACTION _ DOWN事件傳遞方向   紅線:ACTION _ MOVE、ACTION _ UP事件傳遞方向

Android高階進階之路【二】十分鐘徹底弄明白 View 事件分發機制

6.2 onTouch()和onTouchEvent()的區別

  • 該2個方法都是在 View.dispatchTouchEvent() 中呼叫

  • onTouch() 優先於 onTouchEvent 執行;若手動複寫在 onTouch() 中返回 true (即 將事件消費掉),將不會再執行 onTouchEvent()

注:若1個控制元件不可點選(即非 enable ),那麼給它註冊 onTouch 事件將永遠得不到執行,具體原因看如下程式碼

// &&為短路與,即如果前面條件為false,將不再往下執行

//  故:onTouch()能夠得到執行需2個前提條件:
    // 1\. mOnTouchListener的值不能為空
    // 2\. 當前點選的控制元件必須是enable的
mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&  
           mOnTouchListener.onTouch(this, event)

// 對於該類控制元件,若需監聽它的touch事件,就必須透過在該控制元件中重寫onTouchEvent()來實現

文章轉自 如有侵權,請聯絡刪除。


相關影片:

Android進階系統學習——Gradle入門與專案實戰_嗶哩嗶哩_bilibili
Android網路架構搭建與原理解析(一)——透過一個網路請求一步一步見證網路模組的成長_嗶哩嗶哩_bilibili
【Android進階課程】——colin Compose的繪製原理講解(一)_嗶哩嗶哩_bilibili
【 Android進階教程】——Framework面試必問的Handler原始碼解析_嗶哩嗶哩_bilibili
【 Android進階教程】——熱修復原理解析_嗶哩嗶哩_bilibili
【 Android進階教程】——如何解決OOM問題與LeakCanary原理解析_嗶哩嗶哩_bilibili


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2840820/,如需轉載,請註明出處,否則將追究法律責任。

相關文章