Android事件分發機制詳解

如下文章轉載 Android事件分發機制詳解:史上最全面、最易懂 這位大神的博客,最近總結了一下Android的分發機制,由於這篇文章真的很全面,爲了方便之後查看,因此決定轉載,很是感謝原做者。若是感受整篇文章篇幅過長,能夠查看個人Android事件分發機制我的總結

前言

  • Android事件分發機制是Android開發者必須瞭解的基礎
  • 網上有大量關於Android事件分發機制的文章,但存在一些問題:內容不全、思路不清晰、無源碼分析、簡單問題複雜化等等
  • 今天,我將全面總結Android的事件分發機制,我能保證這是市面上的最全面、最清晰、最易懂的
  1. 本文秉着「結論先行、詳細分析在後」的原則,即先讓你們感性認識,再經過理性分析從而理解問題;
  2. 因此,請各位讀者先記住結論,再往下繼續看分析;
  • 文章較長,閱讀須要較長時間,建議收藏等充足時間再進行閱讀

目錄

示意圖

1. 基礎認知

1.1 事件分發的對象是誰?

答:點擊事件(Touch事件)php

  • 定義
    當用戶觸摸屏幕時(ViewViewGroup派生的控件),將產生點擊事件(Touch事件)

Touch事件的相關細節(發生觸摸的位置、時間等)被封裝成MotionEvent對象java

  • 事件類型(4種)
事件類型 具體動做
MotionEvent.ACTION_DOWN 按下View(全部事件的開始)
MotionEvent.ACTION_UP 擡起View(與DOWN對應)
MotionEvent.ACTION_MOVE 滑動View
MotionEvent.ACTION_CANCEL 結束事件(非人爲緣由)
  • 特別說明:事件列
    從手指接觸屏幕 至 手指離開屏幕,這個過程產生的一系列事件

注:通常狀況下,事件列都是以DOWN事件開始、UP事件結束,中間有無數的MOVE事件,以下圖:
android

事件列

即當一個點擊事件(MotionEvent )產生後,系統需把這個事件傳遞給一個具體的 View 去處理。app

1.2 事件分發的本質

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

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

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

答:Activity、ViewGroup、View源碼分析

  • AndroidUI界面由ActivityViewGroupView 及其派生類組成
    UI界面
示意圖

1.4 事件分發的順序

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

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

示意圖

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

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

示意圖

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

1.6 總結

示意圖
  • 至此,相信你們已經對 Android的事件分發有了感性的認知
  • 下面,我將詳細介紹Android事件分發機制

2. 事件分發機制 源碼分析

  • 請謹記:Android事件分發流程 = Activity -> ViewGroup -> View

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

示意圖
  • 從上可知,要想充分理解Android分發機制,本質上是要理解:
    1. Activity對點擊事件的分發機制
    2. ViewGroup對點擊事件的分發機制
    3. View對點擊事件的分發機制
  • 下面,我將經過源碼,全面解析 事件分發機制

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

2.1 Activity的事件分發機制

當一個點擊事件發生時,事件最早傳到ActivitydispatchTouchEvent()進行事件分發

2.1.1 源碼分析

/** * 源碼分析:Activity.dispatchTouchEvent() */ public boolean dispatchTouchEvent(MotionEvent ev) { // 通常事件列開始都是DOWN事件 = 按下事件,故此處基本是true if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); // ->>分析1 } // ->>分析2 if (getWindow().superDispatchTouchEvent(ev)) { return true; // 若getWindow().superDispatchTouchEvent(ev)的返回true // 則Activity.dispatchTouchEvent()就返回true,則方法結束。即 :該點擊事件中止往下傳遞 & 事件傳遞過程結束 // 不然:繼續往下調用Activity.onTouchEvent } // ->>分析4 return onTouchEvent(ev); } /** * 分析1:onUserInteraction() * 做用:實現屏保功能 * 注: * a. 該方法爲空方法 * b. 當此activity在棧頂時,觸屏點擊按home,back,menu鍵等都會觸發此方法 */ public void onUserInteraction() { } // 回到最初的調用原處 /** * 分析2:getWindow().superDispatchTouchEvent(ev) * 說明: * a. getWindow() = 獲取Window類的對象 * b. Window類是抽象類,其惟一實現類 = PhoneWindow類;即此處的Window類對象 = PhoneWindow類對象 * c. Window類的superDispatchTouchEvent() = 1個抽象方法,由子類PhoneWindow類實現 */ @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); // mDecor = 頂層View(DecorView)的實例對象 // ->> 分析3 } /** * 分析3: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的事件分發機制 } // 回到最初的調用原處 /** * 分析4:Activity.onTouchEvent() * 定義:屬於頂層View(DecorView) * 說明: * a. DecorView類是PhoneWindow類的一個內部類 * b. DecorView繼承自FrameLayout,是全部界面的父類 * c. FrameLayout是ViewGroup的子類,故DecorView的間接父類 = ViewGroup */ public boolean onTouchEvent(MotionEvent event) { // 當一個點擊事件未被Activity下任何一個View接收 / 處理時 // 應用場景:處理髮生在Window邊界外的觸摸事件 // ->> 分析5 if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; // 即 只有在點擊事件在Window邊界外才會返回true,通常狀況都返回false,分析完畢 } /** * 分析5:mWindow.shouldCloseOnTouch(this, event) */ public boolean shouldCloseOnTouch(Context context, MotionEvent event) { // 主要是對於處理邊界外點擊事件的判斷:是不是DOWN事件,event的座標是否在邊界內等 if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event) && peekDecorView() != null) { return true; } return false; // 返回true:說明事件在邊界外,即 消費事件 // 返回false:未消費(默認) } // 回到分析4調用原處 /** * 源碼分析:Activity.dispatchTouchEvent() */ public boolean dispatchTouchEvent(MotionEvent ev) { // 通常事件列開始都是DOWN事件 = 按下事件,故此處基本是true if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); // ->>分析1 } // ->>分析2 if (getWindow().superDispatchTouchEvent(ev)) { return true; // 若getWindow().superDispatchTouchEvent(ev)的返回true // 則Activity.dispatchTouchEvent()就返回true,則方法結束。即 :該點擊事件中止往下傳遞 & 事件傳遞過程結束 // 不然:繼續往下調用Activity.onTouchEvent } // ->>分析4 return onTouchEvent(ev); } /** * 分析1:onUserInteraction() * 做用:實現屏保功能 * 注: * a. 該方法爲空方法 * b. 當此activity在棧頂時,觸屏點擊按home,back,menu鍵等都會觸發此方法 */ public void onUserInteraction() { } // 回到最初的調用原處 /** * 分析2:getWindow().superDispatchTouchEvent(ev) * 說明: * a. getWindow() = 獲取Window類的對象 * b. Window類是抽象類,其惟一實現類 = PhoneWindow類;即此處的Window類對象 = PhoneWindow類對象 * c. Window類的superDispatchTouchEvent() = 1個抽象方法,由子類PhoneWindow類實現 */ @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); // mDecor = 頂層View(DecorView)的實例對象 // ->> 分析3 } /** * 分析3: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的事件分發機制 } // 回到最初的調用原處 /** * 分析4:Activity.onTouchEvent() * 定義:屬於頂層View(DecorView) * 說明: * a. DecorView類是PhoneWindow類的一個內部類 * b. DecorView繼承自FrameLayout,是全部界面的父類 * c. FrameLayout是ViewGroup的子類,故DecorView的間接父類 = ViewGroup */ public boolean onTouchEvent(MotionEvent event) { // 當一個點擊事件未被Activity下任何一個View接收 / 處理時 // 應用場景:處理髮生在Window邊界外的觸摸事件 // ->> 分析5 if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; // 即 只有在點擊事件在Window邊界外才會返回true,通常狀況都返回false,分析完畢 } /** * 分析5:mWindow.shouldCloseOnTouch(this, event) */ public boolean shouldCloseOnTouch(Context context, MotionEvent event) { // 主要是對於處理邊界外點擊事件的判斷:是不是DOWN事件,event的座標是否在邊界內等 if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event) && peekDecorView() != null) { return true; } return false; // 返回true:說明事件在邊界外,即 消費事件 // 返回false:未消費(默認) } // 回到分析4調用原處

2.1.2 總結

  • 當一個點擊事件發生時,從Activity的事件分發開始(Activity.dispatchTouchEvent()
示意圖
  • 方法總結
示意圖

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


2.2 ViewGroup事件的分發機制

從上面Activity事件分發機制可知,ViewGroup事件分發機制從dispatchTouchEvent()開始

2.2.1 源碼分析

  1. Android 5.0後,ViewGroup.dispatchTouchEvent()的源碼發生了變化(更加複雜),但原理相同;
  2. 本文爲了讓讀者容易理解,故採用Android 5.0前的版本
/** * 源碼分析:ViewGroup.dispatchTouchEvent() */ public boolean dispatchTouchEvent(MotionEvent ev) { ... // 僅貼出關鍵代碼 // 重點分析1:ViewGroup每次事件分發時,都需調用onInterceptTouchEvent()詢問是否攔截事件 if (disallowIntercept || !onInterceptTouchEvent(ev)) { // 判斷值1:disallowIntercept = 是否禁用事件攔截的功能(默認是false),可經過調用requestDisallowInterceptTouchEvent()修改 // 判斷值2: !onInterceptTouchEvent(ev) = 對onInterceptTouchEvent()返回值取反 // a. 若在onInterceptTouchEvent()中返回false(即不攔截事件),就會讓第二個值爲true,從而進入到條件判斷的內部 // b. 若在onInterceptTouchEvent()中返回true(即攔截事件),就會讓第二個值爲false,從而跳出了這個條件判斷 // c. 關於onInterceptTouchEvent() ->>分析1 ev.setAction(MotionEvent.ACTION_DOWN); final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; // 重點分析2 // 經過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); // 判斷當前遍歷的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; // 條件判斷的內部調用了該View的dispatchTouchEvent() // 即 實現了點擊事件從ViewGroup到子View的傳遞(具體請看下面的View事件分發機制) if (child.dispatchTouchEvent(ev)) { mMotionTarget = child; return true; // 調用子View的dispatchTouchEvent後是有返回值的 // 若該控件可點擊,那麼點擊時,dispatchTouchEvent的返回值一定是true,所以會致使條件判斷成立 // 因而給ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出 // 即把ViewGroup的點擊事件攔截掉 } } } } } } boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } final View target = mMotionTarget; // 重點分析3 // 若點擊的是空白處(即無任何View接收事件) / 攔截事件(手動複寫onInterceptTouchEvent(),從而讓其返回true) 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); // 調用ViewGroup父類的dispatchTouchEvent(),即View.dispatchTouchEvent() // 所以會執行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick(),即本身處理該事件,事件不會往下傳遞(具體請參考View事件的分發機制中的View.dispatchTouchEvent()) // 此處需與上面區別:子View的dispatchTouchEvent() } ... } /** * 分析1:ViewGroup.onInterceptTouchEvent() * 做用:是否攔截事件 * 說明: * a. 返回true = 攔截,即事件中止往下傳遞(需手動設置,即複寫onInterceptTouchEvent(),從而讓其返回true) * b. 返回false = 不攔截(默認) */ public boolean onInterceptTouchEvent(MotionEvent ev) { return false; } // 回到調用原處 /** * 源碼分析:ViewGroup.dispatchTouchEvent() */ public boolean dispatchTouchEvent(MotionEvent ev) { ... // 僅貼出關鍵代碼 // 重點分析1:ViewGroup每次事件分發時,都需調用onInterceptTouchEvent()詢問是否攔截事件 if (disallowIntercept || !onInterceptTouchEvent(ev)) { // 判斷值1:disallowIntercept = 是否禁用事件攔截的功能(默認是false),可經過調用requestDisallowInterceptTouchEvent()修改 // 判斷值2: !onInterceptTouchEvent(ev) = 對onInterceptTouchEvent()返回值取反 // a. 若在onInterceptTouchEvent()中返回false(即不攔截事件),就會讓第二個值爲true,從而進入到條件判斷的內部 // b. 若在onInterceptTouchEvent()中返回true(即攔截事件),就會讓第二個值爲false,從而跳出了這個條件判斷 // c. 關於onInterceptTouchEvent() ->>分析1 ev.setAction(MotionEvent.ACTION_DOWN); final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; // 重點分析2 // 經過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); // 判斷當前遍歷的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; // 條件判斷的內部調用了該View的dispatchTouchEvent() // 即 實現了點擊事件從ViewGroup到子View的傳遞(具體請看下面的View事件分發機制) if (child.dispatchTouchEvent(ev)) { mMotionTarget = child; return true; // 調用子View的dispatchTouchEvent後是有返回值的 // 若該控件可點擊,那麼點擊時,dispatchTouchEvent的返回值一定是true,所以會致使條件判斷成立 // 因而給ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出 // 即把ViewGroup的點擊事件攔截掉 } } } } } } boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } final View target = mMotionTarget; // 重點分析3 // 若點擊的是空白處(即無任何View接收事件) / 攔截事件(手動複寫onInterceptTouchEvent(),從而讓其返回true) 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); // 調用ViewGroup父類的dispatchTouchEvent(),即View.dispatchTouchEvent() // 所以會執行ViewGroup的onTouch() ->> onTouchEvent() ->> performClick() ->> onClick(),即本身處理該事件,事件不會往下傳遞(具體請參考View事件的分發機制中的View.dispatchTouchEvent()) // 此處需與上面區別:子View的dispatchTouchEvent() } ... } /** * 分析1:ViewGroup.onInterceptTouchEvent() * 做用:是否攔截事件 * 說明: * a. 返回true = 攔截,即事件中止往下傳遞(需手動設置,即複寫onInterceptTouchEvent(),從而讓其返回true) * b. 返回false = 不攔截(默認) */ public boolean onInterceptTouchEvent(MotionEvent ev) { return false; } // 回到調用原處

2.2.2 總結

  • 結論:Android事件分發老是先傳遞到ViewGroup、再傳遞到View
  • 過程:當點擊了某個控件時
示意圖
  • 核心方法總結
示意圖

2.2.3 Demo講解

  • 佈局以下


    佈局層次
  • 測試代碼

佈局文件: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

/** * ViewGroup佈局(myLayout)中有2個子View = 2個按鈕 */ 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"); } }); } } /** * ViewGroup佈局(myLayout)中有2個子View = 2個按鈕 */ 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"); } }); } }
  • 結果測試


    示意圖

從上面的測試結果發現:

  • 點擊Button時,執行Button.onClick(),但ViewGroupLayout註冊的onTouch()不會執行
  • 只有點擊空白區域時,纔會執行ViewGroupLayoutonTouch()
  • 結論:ButtononClick()將事件消費掉了,所以事件不會再繼續向下傳遞。

2.3 View事件的分發機制

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

2.3.1 源碼分析

/** * 源碼分析:View.dispatchTouchEvent() */ public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); } // 說明:只有如下3個條件都爲真,dispatchTouchEvent()才返回true;不然執行onTouchEvent() // 1. mOnTouchListener != null // 2. (mViewFlags & ENABLED_MASK) == ENABLED // 3. mOnTouchListener.onTouch(this, event) // 下面對這3個條件逐個分析 /** * 條件1:mOnTouchListener != null * 說明:mOnTouchListener變量在View.setOnTouchListener()方法裏賦值 */ public void setOnTouchListener(OnTouchListener l) { mOnTouchListener = l; // 即只要咱們給控件註冊了Touch事件,mOnTouchListener就必定被賦值(不爲空) } /** * 條件2:(mViewFlags & ENABLED_MASK) == ENABLED * 說明: * a. 該條件是判斷當前點擊的控件是否enable * b. 因爲不少View默認enable,故該條件恆定爲true */ /** * 條件3:mOnTouchListener.onTouch(this, event) * 說明:即 回調控件註冊Touch事件時的onTouch();需手動複寫設置,具體以下(以按鈕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) /** * 源碼分析:View.dispatchTouchEvent() */ public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); } // 說明:只有如下3個條件都爲真,dispatchTouchEvent()才返回true;不然執行onTouchEvent() // 1. mOnTouchListener != null // 2. (mViewFlags & ENABLED_MASK) == ENABLED // 3. mOnTouchListener.onTouch(this, event) // 下面對這3個條件逐個分析 /** * 條件1:mOnTouchListener != null * 說明:mOnTouchListener變量在View.setOnTouchListener()方法裏賦值 */ public void setOnTouchListener(OnTouchListener l) { mOnTouchListener = l; // 即只要咱們給控件註冊了Touch事件,mOnTouchListener就必定被賦值(不爲空) } /** * 條件2:(mViewFlags & ENABLED_MASK) == ENABLED * 說明: * a. 該條件是判斷當前點擊的控件是否enable * b. 因爲不少View默認enable,故該條件恆定爲true */ /** * 條件3:mOnTouchListener.onTouch(this, event) * 說明:即 回調控件註冊Touch事件時的onTouch();需手動複寫設置,具體以下(以按鈕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(event)的源碼分析

  1. 詳情請看註釋
  2. Android 5.0View.onTouchEvent()源碼發生了變化(更加複雜),但原理相同;
  3. 本文爲了讓讀者更好理解,因此採用Android 5.0前的版本
/** * 源碼分析:View.onTouchEvent() */ public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { 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)) { switch (event.getAction()) { // a. 若當前的事件 = 擡起View(主要分析) case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; ...// 通過種種判斷,此處省略 // 執行performClick() ->>分析1 performClick(); break; // b. 若當前的事件 = 按下View case MotionEvent.ACTION_DOWN: if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPrivateFlags |= PREPRESSED; mHasPerformedLongPress = false; postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); break; // c. 若當前的事件 = 結束事件(非人爲緣由) case MotionEvent.ACTION_CANCEL: mPrivateFlags &= ~PRESSED; 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)) { // 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; } /** * 分析1:performClick() */ public boolean performClick() { if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; // 只要咱們經過setOnClickListener()爲控件View註冊1個點擊事件 // 那麼就會給mOnClickListener變量賦值(即不爲空) // 則會往下回調onClick() & performClick()返回true } return false; } /** * 源碼分析:View.onTouchEvent() */ public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { 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)) { switch (event.getAction()) { // a. 若當前的事件 = 擡起View(主要分析) case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; ...// 通過種種判斷,此處省略 // 執行performClick() ->>分析1 performClick(); break; // b. 若當前的事件 = 按下View case MotionEvent.ACTION_DOWN: if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPrivateFlags |= PREPRESSED; mHasPerformedLongPress = false; postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); break; // c. 若當前的事件 = 結束事件(非人爲緣由) case MotionEvent.ACTION_CANCEL: mPrivateFlags &= ~PRESSED; 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)) { // 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; } /** * 分析1:performClick() */ public boolean performClick() { if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; // 只要咱們經過setOnClickListener()爲控件View註冊1個點擊事件 // 那麼就會給mOnClickListener變量賦值(即不爲空) // 則會往下回調onClick() & performClick()返回true } return false; }

2.3.2 總結

  • 每當控件被點擊時:
示意圖

注:onTouch()的執行 先於 onClick()

  • 核心方法總結
示意圖

2.3.3 Demo講解

下面我將用Demo驗證上述的結論

/** * 結論驗證1:在回調onTouch()裏返回false */ // 1. 經過OnTouchListener()複寫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()爲控件設置點擊事件,爲mOnClickListener變量賦值(即不爲空),從而往下回調onClick() button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println("執行了onClick()"); } }); /** * 結論驗證2:在回調onTouch()裏返回true */ // 1. 經過OnTouchListener()複寫onTouch(),從而手動設置返回true button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { System.out.println("執行了onTouch(), 動做是:" + event.getAction()); return true; } }); // 2. 經過 OnClickListener()爲控件設置點擊事件,爲mOnClickListener變量賦值(即不爲空) // 但因爲dispatchTouchEvent()返回true,即事件再也不向下傳遞,故不調用onClick()) button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println("執行了onClick()"); } }); /** * 結論驗證1:在回調onTouch()裏返回false */ // 1. 經過OnTouchListener()複寫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()爲控件設置點擊事件,爲mOnClickListener變量賦值(即不爲空),從而往下回調onClick() button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println("執行了onClick()"); } }); /** * 結論驗證2:在回調onTouch()裏返回true */ // 1. 經過OnTouchListener()複寫onTouch(),從而手動設置返回true button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { System.out.println("執行了onTouch(), 動做是:" + event.getAction()); return true; } }); // 2. 經過 OnClickListener()爲控件設置點擊事件,爲mOnClickListener變量賦值(即不爲空) // 但因爲dispatchTouchEvent()返回true,即事件再也不向下傳遞,故不調用onClick()) button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println("執行了onClick()"); } });
  • 測試結果


    示意圖

2.4 總結

示意圖

若您已經看到此處,那麼恭喜你,你已經能很是熟悉掌握Android的事件分發機制了

即:ActivityViewGroupView 的事件分發機制


3. 工做流程 總結

  • 在本節中,我將結合源碼,梳理出1個事件分發的工做流程總結,具體以下:
示意圖

左側虛線:具有相關性 & 逐層返回

  • 以角色爲核心的圖解說明
示意圖
  • 以方法爲核心的圖解說明
示意圖

4. 核心方法總結

  • 已知事件分發過程的核心方法爲:dispatchTouchEvent()onInterceptTouchEvent()onTouchEvent()

    示意圖

  • 下面,我將結合總結的工做流程,再次詳細講解該3個方法

4.1 dispatchTouchEvent()

  • 簡介
示意圖
示意圖
  • 返回狀況說明

狀況1:默認

示意圖

示意圖

狀況2:返回true

示意圖

示意圖

狀況3:返回false

示意圖

示意圖

4.2 onInterceptTouchEvent()

  • 簡介
示意圖

注:ActivityView都無該方法

示意圖
  • 返回狀況說明

狀況1:true

示意圖
示意圖

狀況2:false(默認)

示意圖
示意圖

4.3 onTouchEvent()

  • 簡介
示意圖
示意圖
  • 返回狀況說明

狀況1:返回true

示意圖
示意圖

狀況2:返回false(default)

示意圖

示意圖

4.4 三者關係

下面,我用一段僞代碼來闡述上述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; } /** * 點擊事件產生後 */ // 步驟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 背景描述

  • 討論的佈局以下:
示意圖
  • 情景
    1. 用戶先觸摸到屏幕上View C上的某個點(圖中黃區)

    Action_DOWN事件在此處產生

    1. 用戶移動手指
    2. 最後離開屏幕

5.2 通常的事件傳遞狀況

通常的事件傳遞場景有:

  • 默認狀況
  • 處理事件
  • 攔截DOWN事件
  • 攔截後續事件(MOVEUP

場景1:默認

  • 即不對控件裏的方法(dispatchTouchEvent()onTouchEvent()onInterceptTouchEvent())進行重寫 或 更改返回值
  • 那麼調用的是這3個方法的默認實現:調用下層的方法 & 逐層返回
  • 事件傳遞狀況:(呈U型)
    1. 從上往下調用dispatchTouchEvent()

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

    1. 從下往上調用onTouchEvent()

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

示意圖

注:雖然ViewGroup BonInterceptTouchEvent()對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 AonTouchEvent()
  • 該事件列的其餘事件(Move、Up)也將傳遞給View ConTouchEvent()
示意圖

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

場景3:攔截DOWN事件

假設ViewGroup B但願處理該點擊事件,即ViewGroup B複寫了onInterceptTouchEvent()返回trueonTouchEvent()返回true
事件傳遞狀況:(以下圖)

  • DOWN事件被傳遞給ViewGroup BonInterceptTouchEvent(),該方法返回true,表示攔截該事件,即本身處理該事件(事件再也不往下傳遞)

  • 調用自身的onTouchEvent()處理事件(DOWN事件將再也不往上傳遞給Activity AonTouchEvent()

  • 該事件列的其餘事件(Move、Up)將直接傳遞給ViewGroup BonTouchEvent()

注:

  1. 該事件列的其餘事件(Move、Up)將不會再傳遞給ViewGroup BonInterceptTouchEvent();因:該方法一旦返回一次true,就不再會被調用
  2. 逐層往dispatchTouchEvent() 返回,最終事件分發結束
示意圖

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

結論

  • ViewGroup 攔截了一個半路的事件(如MOVE),該事件將會被系統變成一個CANCEL事件 & 傳遞給以前處理該事件的子View
  • 該事件不會再傳遞給ViewGrouponTouchEvent()
  • 只有再到來的事件纔會傳遞到ViewGrouponTouchEvent()

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

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

實例講解

  • 在後續到來的MOVE事件,ViewGroup BonInterceptTouchEvent()返回true攔截該MOVE事件,但該事件並無傳遞給ViewGroup B ;這個MOVE事件將會被系統變成一個CANCEL事件傳遞給View ConTouchEvent()
  • 後續又來了一個MOVE事件,該MOVE事件纔會直接傳遞給ViewGroup BonTouchEvent()

後續事件將直接傳遞給ViewGroup BonTouchEvent()處理,而不會再傳遞給ViewGroup BonInterceptTouchEvent(),因該方法一旦返回一次true,就不再會被調用了。

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

至此,關於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事件傳遞方向

流程講解
  • 結論2
    若對象(Activity、ViewGroup、View)的onTouchEvent()處理了事件(返回true),那麼ACTION_MOVE、ACTION_UP的事件從上往下傳到該View後就再也不往下傳遞,而是直接傳給本身的onTouchEvent()& 結束本次事件傳遞過程。

黑線:ACTION_DOWN事件傳遞方向
紅線:ACTION_MOVE、ACTION_UP事件傳遞方向

流程講解

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()來實現 // &&爲短路與,即若是前面條件爲false,將再也不往下執行 // 故:onTouch()可以獲得執行需2個前提條件: // 1. mOnTouchListener的值不能爲空 // 2. 當前點擊的控件必須是enable的 mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event) // 對於該類控件,若需監聽它的touch事件,就必須經過在該控件中重寫onTouchEvent()來實現

7. 總結


相關文章閱讀
Android開發:最全面、最易懂的Android屏幕適配解決方案
Android開發:史上最全的Android消息推送解決方案
Android開發:最全面、最易懂的Webview詳解
Android開發:JSON簡介及最全面解析方法!
Android四大組件:Service服務史上最全面解析
Android四大組件:BroadcastReceiver史上最全面解析

相關文章
相關標籤/搜索