Android事件分發機制詳解:史上最全面、最易懂


前言

  • Android事件分發機制是每個Android開發者必須了解的基礎知識
  • 網上有大量關於Android事件分發機制的文章,但存在一些問題:內容不全、思路不清晰、無源碼分析、簡單問題復雜化等等
  • 今天,我將全面總結Android的事件分發機制,我能保證這是市面上的最全面、最清晰、最易懂的

    1. 本文秉着“結論先行、詳細分析在后”的原則,即先讓大家感性認識,再通過理性分析從而理解問題;
    2. 所以,請各位讀者先記住結論,再往下繼續看分析;
  • 文章較長,閱讀需要較長時間,建議收藏等充足時間再進行閱讀


目錄

目錄


1. 基礎認知

1.1 事件分發的對象是誰?

答:事件

  • 當用戶觸摸屏幕時(View或ViewGroup派生的控件),將產生點擊事件(Touch事件)。

    Touch事件相關細節(發生觸摸的位置、時間、歷史記錄、手勢動作等)被封裝成MotionEvent對象

  • 主要發生的Touch事件有如下四種:

    • MotionEvent.ACTION_DOWN:按下View(所有事件的開始)
    • MotionEvent.ACTION_MOVE:滑動View
    • MotionEvent.ACTION_CANCEL:非人為原因結束本次事件
    • MotionEvent.ACTION_UP:抬起View(與DOWN對應)
  • 事件列:從手指接觸屏幕至手指離開屏幕,這個過程產生的一系列事件 
    任何事件列都是以DOWN事件開始,UP事件結束,中間有無數的MOVE事件,如下圖: 
    事件列

即當一個MotionEvent 產生后,系統需要把這個事件傳遞給一個具體的 View 去處理,

1.2 事件分發的本質

答:將點擊事件(MotionEvent)向某個View進行傳遞並最終得到處理

即當一個點擊事件發生后,系統需要將這個事件傳遞給一個具體的View去處理。這個事件傳遞的過程就是分發過程。

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

答:Activity、ViewGroup、View

一個點擊事件產生后,傳遞順序是:Activity(Window) -> ViewGroup -> View

  • Android的UI界面是由Activity、ViewGroup、View及其派生類組合而成的 
    UI界面

  • View是所有UI組件的基類

    一般Button、ImageView、TextView等控件都是繼承父類View

  • ViewGroup是容納UI組件的容器,即一組View的集合(包含很多子View和子VewGroup),

    1. 其本身也是從View派生的,即ViewGroup是View的子類
    2. 是Android所有布局的父類或間接父類:項目用到的布局(LinearLayout、RelativeLayout等),都繼承自ViewGroup,即屬於ViewGroup子類。
    3. 與普通View的區別:ViewGroup實際上也是一個View,只不過比起View,它多了可以包含子View和定義布局參數的功能。

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

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

事件分發相關方法

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

1.5 總結

  • Android事件分發機制的本質是要解決:點擊事件由哪個對象發出,經過哪些對象,最終達到哪個對象並最終得到處理。 

    這里的對象是指Activity、ViewGroup、View

  • Android中事件分發順序:Activity(Window) -> ViewGroup -> View
  • 事件分發過程由dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()三個方法協助完成


經過上述3個問題,相信大家已經對Android的事件分發有了感性的認知,接下來,我將詳細介紹Android事件分發機制。


2. 事件分發機制方法&流程介紹

  • 事件分發過程由dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()三個方法協助完成,如下圖:

方法詳細介紹

  • Android事件分發流程如下:(必須熟記) 

    Android事件分發順序:Activity(Window) -> ViewGroup -> View


事件分發機制詳細流程其中:

  • super:調用父類方法
  • true:消費事件,即事件不繼續往下傳遞
  • false:不消費事件,事件也不繼續往下傳遞 / 交由給父控件onTouchEvent()處理

接下來,我將詳細介紹這3個方法及相關流程。

2.1 dispatchTouchEvent()

屬性 介紹
使用對象 Activity、ViewGroup、View
作用 分發點擊事件
調用時刻 當點擊事件能夠傳遞給當前View時,該方法就會被調用
返回結果 是否消費當前事件,詳細情況如下:

1. 默認情況:根據當前對象的不同而返回方法不同

對象 返回方法 備注
Activity super.dispatchTouchEvent() 即調用父類ViewGroup的dispatchTouchEvent()
ViewGroup onIntercepTouchEvent() 即調用自身的onIntercepTouchEvent()
View onTouchEvent() 即調用自身的onTouchEvent()

流程解析

2. 返回true

  • 消費事件
  • 事件不會往下傳遞
  • 后續事件(Move、Up)會繼續分發到該View
  • 流程圖如下:

流程圖

3. 返回false

  • 不消費事件
  • 事件不會往下傳遞
  • 將事件回傳給父控件的onTouchEvent()處理

    Activity例外:返回false=消費事件

  • 后續事件(Move、Up)會繼續分發到該View(與onTouchEvent()區別)

  • 流程圖如下: 
    流程圖

2.2 onTouchEvent()

屬性 介紹
使用對象 Activity、ViewGroup、View
作用 處理點擊事件
調用時刻 在dispatchTouchEvent()內部調用
返回結果 是否消費(處理)當前事件,詳細情況如下:

與dispatchTouchEvent()類似

1. 返回true

  • 自己處理(消費)該事情
  • 事件停止傳遞
  • 該事件序列的后續事件(Move、Up)讓其處理;
  • 流程圖如下: 
    流程圖

2. 返回false(同默認實現:調用父類onTouchEvent())

  • 不處理(消費)該事件
  • 事件往上傳遞給父控件的onTouchEvent()處理
  • 當前View不再接受此事件列的其他事件(Move、Up);
  • 流程圖如下: 
    流程圖

2.3 onInterceptTouchEvent()

屬性 介紹
使用對象 ViewGroup(注:Activity、View都沒該方法)
作用 攔截事件,即自己處理該事件
調用時刻 在ViewGroup的dispatchTouchEvent()內部調用
返回結果 是否攔截當前事件,詳細情況如下:

返回結果

  • 流程圖如下:

流程圖

2.4 三者關系

下面將用一段偽代碼來闡述上述三個方法的關系和點擊事件傳遞規則

// 點擊事件產生后,會直接調用dispatchTouchEvent()方法 public boolean dispatchTouchEvent(MotionEvent ev) { //代表是否消耗事件 boolean consume = false; if (onInterceptTouchEvent(ev)) { //如果onInterceptTouchEvent()返回true則代表當前View攔截了點擊事件 //則該點擊事件則會交給當前View進行處理 //即調用onTouchEvent ()方法去處理點擊事件 consume = onTouchEvent (ev) ; } else { //如果onInterceptTouchEvent()返回false則代表當前View不攔截點擊事件 //則該點擊事件則會繼續傳遞給它的子元素 //子元素的dispatchTouchEvent()就會被調用,重復上述過程 //直到點擊事件被最終處理為止 consume = child.dispatchTouchEvent (ev) ; } return consume; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

2.5 總結

  • 對於事件分發的3個方法,你應該清楚了解
  • 接下來,我將開始介紹Android事件分發的常見流程

3. 事件分發場景介紹

下面我將利用例子來說明常見的點擊事件傳遞情況

3.1 背景描述

我們將要討論的布局層次如下: 
布局層次

  • 最外層:Activiy A,包含兩個子View:ViewGroup B、View C
  • 中間層:ViewGroup B,包含一個子View:View C
  • 最內層:View C

假設用戶首先觸摸到屏幕上View C上的某個點(如圖中黃色區域),那么Action_DOWN事件就在該點產生,然后用戶移動手指並最后離開屏幕。

3.2 一般的事件傳遞情況

一般的事件傳遞場景有:

  • 默認情況
  • 處理事件
  • 攔截DOWN事件
  • 攔截后續事件(MOVE、UP)

3.2.1 默認情況

  • 即不對控件里的方法(dispatchTouchEvent()、onTouchEvent()、onInterceptTouchEvent())進行重寫或更改返回值
  • 那么調用的是這3個方法的默認實現:調用父類的方法
  • 事件傳遞情況:(如圖下所示) 
    • 從Activity A—->ViewGroup B—>View C,從上往下調用dispatchTouchEvent()
    • 再由View C—>ViewGroup B —>Activity A,從下往上調用onTouchEvent()

流程圖

注:雖然ViewGroup B的onInterceptTouchEvent方法對DOWN事件返回了false,后續的事件(MOVE、UP)依然會傳遞給它的onInterceptTouchEvent()

這一點與onTouchEvent的行為是不一樣的。

3.2.2 處理事件

假設View C希望處理這個點擊事件,即C被設置成可點擊的(Clickable)或者覆寫了C的onTouchEvent方法返回true。

最常見的:設置Button按鈕來響應點擊事件

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

  • DOWN事件被傳遞給C的onTouchEvent方法,該方法返回true,表示處理這個事件
  • 因為C正在處理這個事件,那么DOWN事件將不再往上傳遞給B和A的onTouchEvent();
  • 該事件列的其他事件(Move、Up)也將傳遞給C的onTouchEvent()

流程圖

3.2.3 攔截DOWN事件

假設ViewGroup B希望處理這個點擊事件,即B覆寫了onInterceptTouchEvent()返回true、onTouchEvent()返回true。 
事件傳遞情況:(如下圖)

  • DOWN事件被傳遞給B的onInterceptTouchEvent()方法,該方法返回true,表示攔截這個事件,即自己處理這個事件(不再往下傳遞)
  • 調用onTouchEvent()處理事件(DOWN事件將不再往上傳遞給A的onTouchEvent())

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

    該事件列的其他事件(Move、Up)將不會再傳遞給B的onInterceptTouchEvent方法,該方法一旦返回一次true,就再也不會被調用了。

流程圖

3.2.4 攔截DOWN的后續事件

假設ViewGroup B沒有攔截DOWN事件(還是View C來處理DOWN事件),但它攔截了接下來的MOVE事件。

  • DOWN事件傳遞到C的onTouchEvent方法,返回了true。
  • 在后續到來的MOVE事件,B的onInterceptTouchEvent方法返回true攔截該MOVE事件,但該事件並沒有傳遞給B;這個MOVE事件將會被系統變成一個CANCEL事件傳遞給C的onTouchEvent方法
  • 后續又來了一個MOVE事件,該MOVE事件才會直接傳遞給B的onTouchEvent()

    1. 后續事件將直接傳遞給B的onTouchEvent()處理
    2. 后續事件將不會再傳遞給B的onInterceptTouchEvent方法,該方法一旦返回一次true,就再也不會被調用了。
  • C再也不會收到該事件列產生的后續事件。 
    流程圖

特別注意:

  • 如果ViewGroup A 攔截了一個半路的事件(如MOVE),這個事件將會被系統變成一個CANCEL事件並傳遞給之前處理該事件的子View;
  • 該事件不會再傳遞給ViewGroup A的onTouchEvent()
  • 只有再到來的事件才會傳遞到ViewGroup A的onTouchEvent()

3.3 總結

  • 對於Android的事件分發機制,你應該已經非常清楚了
  • 如果你只是希望了解Android事件分發機制而不想深入了解,那么你可以離開這篇文章了
  • 對於程序猿來說,知其然還需要知其所以然,接下來,我將通過源碼分析來深入了解Android事件分發機制

4. Android事件分發機制源碼分析

  • Android中事件分發順序:Activity(Window) -> ViewGroup -> View,再次貼出下圖:

事件分發機制詳細流程

其中:

  • super:調用父類方法
  • true:消費事件,即事件不繼續往下傳遞
  • false:不消費事件,事件繼續往下傳遞 / 交由給父控件onTouchEvent()處理

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

  • Activity對點擊事件的分發機制
  • ViewGroup對點擊事件的分發機制
  • View對點擊事件的分發機制

接下來,我將通過源碼分析詳細介紹Activity、View和ViewGroup的事件分發機制


4.1 Activity的事件分發機制

4.1.1 源碼分析

  • 當一個點擊事件發生時,事件最先傳到Activity的dispatchTouchEvent()進行事件分發 
     具體是由Activity的Window來完成
  • 我們來看下Activity的dispatchTouchEvent()的源碼
public boolean dispatchTouchEvent(MotionEvent ev) { //關注點1 //一般事件列開始都是DOWN,所以這里基本是true if (ev.getAction() == MotionEvent.ACTION_DOWN) { //關注點2 onUserInteraction(); } //關注點3 if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

關注點1

一般事件列開始都是DOWN(按下按鈕),所以這里返回true,執行onUserInteraction()

關注點2

先來看下onUserInteraction()源碼

  /**
     * Called whenever a key, touch, or trackball event is dispatched to the * activity. Implement this method if you wish to know that the user has * interacted with the device in some way while your activity is running. * This callback and {@link #onUserLeaveHint} are intended to help * activities manage status bar notifications intelligently; specifically, * for helping activities determine the proper time to cancel a notfication. * * <p>All calls to your activity's {@link #onUserLeaveHint} callback will * be accompanied by calls to {@link #onUserInteraction}. This * ensures that your activity will be told of relevant user activity such * as pulling down the notification pane and touching an item there. * * <p>Note that this callback will be invoked for the touch down action * that begins a touch gesture, but may not be invoked for the touch-moved * and touch-up actions that follow. * * @see #onUserLeaveHint() */ public void onUserInteraction() { }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

從源碼可以看出:

  • 該方法為空方法
  • 從注釋得知:當此activity在棧頂時,觸屏點擊按home,back,menu鍵等都會觸發此方法
  • 所以onUserInteraction()主要用於屏保

關注點3

  • Window類是抽象類,且PhoneWindow是Window類的唯一實現類
  • superDispatchTouchEvent(ev)是抽象方法,返回的是一個Window對象
  • 通過PhoneWindow類中看一下superDispatchTouchEvent()的作用
@Override
public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); //mDecor是DecorView的實例 //DecorView是視圖的頂層view,繼承自FrameLayout,是所有界面的父類 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 接下來我們看mDecor.superDispatchTouchEvent(event):
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); //DecorView繼承自FrameLayout //那么它的父類就是ViewGroup 而super.dispatchTouchEvent(event)方法,其實就應該是ViewGroup的dispatchTouchEvent() }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

所以:

  • 執行getWindow().superDispatchTouchEvent(ev)實際上是執行了ViewGroup.dispatchTouchEvent(event)
  • 再回到最初的代碼:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { //關注點2 onUserInteraction(); } //關注點3 if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

由於一般事件列開始都是DOWN,所以這里返回true,基本上都會進入getWindow().superDispatchTouchEvent(ev)的判斷

  • 所以,執行Activity.dispatchTouchEvent(ev)實際上是執行了ViewGroup.dispatchTouchEvent(event)
  • 這樣事件就從 Activity 傳遞到了 ViewGroup

4.1.2 匯總

當一個點擊事件發生時,調用順序如下

  1. 事件最先傳到Activity的dispatchTouchEvent()進行事件分發
  2. 調用Window類實現類PhoneWindow的superDispatchTouchEvent()
  3. 調用DecorView的superDispatchTouchEvent()
  4. 最終調用DecorView父類的dispatchTouchEvent(),即ViewGroup的dispatchTouchEvent()

4.1.3 結論

  • 當一個點擊事件發生時,事件最先傳到Activity的dispatchTouchEvent()進行事件分發,最終是調用了ViewGroup的dispatchTouchEvent()方法 
     如果ViewGroup的dispatchTouchEvent()返回true就不執行Activity的onTouchEvent()方法;如果返回false,就執行。
  • 這樣事件就從 Activity 傳遞到了 ViewGroup

4.1.4 疑問

那么,ViewGroup的dispatchTouchEvent()什么時候返回true,什么時候返回false?

答:請繼續往下看 ViewGroup事件的分發機制


4.2 ViewGroup事件的分發機制

在講解ViewGroup事件的分發機制之前我們先來看個Demo

4.2.1 Demo講解

布局如下: 
布局層次

結果測試 
只點擊Button 
只點擊Button 
再點擊空白處 
點擊空白處

從上面的測試結果發現:

  • 當點擊Button時,執行Button的onClick(),但ViewGroupLayout注冊的onTouch()不會執行
  • 只有點擊空白區域時才會執行ViewGroupLayout的onTouch();
  • 結論:Button的onClick()將事件消費掉了,因此事件不會再繼續向下傳遞。

接下來,我們開始進行ViewGroup事件分發的源碼分析

4.2.2 源碼分析

ViewGroup的dispatchTouchEvent()源碼分析

  1. 詳情請看注釋
  2. Android 5.0后ViewGroup的dispatchTouchEvent()的源碼發生了變化(更加復雜),但原理相同;
  3. 本文為了讓讀者更好理解dispatchTouchEvent()源碼分析,所以采用Android 5.0前的版本
public boolean dispatchTouchEvent(MotionEvent ev) { final int action = ev.getAction(); final float xf = ev.getX(); final float yf = ev.getY(); final float scrolledXFloat = xf + mScrollX; final float scrolledYFloat = yf + mScrollY; final Rect frame = mTempRect; boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (action == MotionEvent.ACTION_DOWN) { if (mMotionTarget != null) { mMotionTarget = null; } //看這個If判斷語句 //第一個判斷值disallowIntercept:是否禁用事件攔截的功能(默認是false) //可以通過調用requestDisallowInterceptTouchEvent方法對這個值進行修改。 //第二個判斷值: !onInterceptTouchEvent(ev):對onInterceptTouchEvent()返回值取反 //如果我們在onInterceptTouchEvent()中返回false,就會讓第二個值為true,從而進入到條件判斷的內部 //如果我們在onInterceptTouchEvent()中返回true,就會讓第二個值為false,從而跳出了這個條件判斷。 //關於onInterceptTouchEvent()請看下面分析(關注點1) if (disallowIntercept || !onInterceptTouchEvent(ev)) { ev.setAction(MotionEvent.ACTION_DOWN); final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; //通過for循環,遍歷了當前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 //如果是,則進入條件判斷內部 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; //關注點2 //條件判斷的內部調用了該View的dispatchTouchEvent()方法(具體請看下面的View事件分發機制) //實現了點擊事件從ViewGroup到View的傳遞 if (child.dispatchTouchEvent(ev)) { //調用子View的dispatchTouchEvent后是有返回值的 //如果這個控件是可點擊的話,那么點擊該控件時,dispatchTouchEvent的返回值必定是true //因此會導致條件判斷成立 mMotionTarget = child; //於是給ViewGroup的dispatchTouchEvent方法直接返回了true,這樣就導致后面的代碼無法執行,直接跳出 //即把ViewGroup的touch事件攔截掉 return true; } } } } } } boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } final View target = mMotionTarget; //關注點3 //沒有任何View接收事件的情況,即點擊空白處情況 if (target == null) { ev.setLocation(xf, yf); if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; } //調用ViewGroup的父類View的dispatchTouchEvent() //因此會執行ViewGroup的onTouch()、onTouchEvent() //實現了點擊事件從ViewGroup到View的傳遞 return super.dispatchTouchEvent(ev); } //之后的代碼在一般情況下是走不到的了,我們也就不再繼續往下分析。 if (!disallowIntercept && onInterceptTouchEvent(ev)) { final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; ev.setAction(MotionEvent.ACTION_CANCEL); ev.setLocation(xc, yc); if (!target.dispatchTouchEvent(ev)) { } mMotionTarget = null; return true; } if (isUpOrCancel) { mMotionTarget = null; } final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; ev.setLocation(xc, yc); if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; mMotionTarget = null; } return target.dispatchTouchEvent(ev); } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110

關注點1(onInterceptTouchEvent()源碼分析)

ViewGroup每次在做分發時,需要調用onInterceptTouchEvent()是否攔截事件;源碼分析如下:

public boolean onInterceptTouchEvent(MotionEvent ev) { return false; } 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
  • 返回false =不攔截(默認),允許事件繼續往下傳遞(向子View傳遞); 
     因為子View也需要該事件,所以onInterceptTouchEvent攔截器return super.onInterceptTouchEvent()和return false是一樣的 = 不會攔截
  • 返回true = 攔截(手動設置),即自己處理該事件(執行自己的onTouchEvent()),事件不會繼續往下傳遞

關注點2

當點擊了某個控件:

  1. 調用該控件所在布局(ViewGroup)的dispatchTouchEvent()
  2. 在布局的dispatchTouchEvent()中找到被點擊的相應控件
  3. 再去調用該控件的dispatchTouchEvent() 
    1. 實現了點擊事件從ViewGroup到View的傳遞 
    2. 此處是關於View.dispatchTouchEvent()的分析,詳情請看下面的View事件分發機制。

結論

  • Android事件分發是先傳遞到ViewGroup,再由ViewGroup傳遞到View
  • 在ViewGroup中通過onInterceptTouchEvent()對事件傳遞進行攔截 
    1. onInterceptTouchEvent方法返回true代表攔截事件,即不允許事件繼續向子View傳遞; 
    2. 返回false代表不攔截事件,即允許事件繼續向子View傳遞;(默認返回false) 
    3. 子View中如果將傳遞的事件消費掉,ViewGroup中將無法接收到任何事件。

 



4.3 View事件的分發機制

View中dispatchTouchEvent()的源碼分析

public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)) { return true; } return onTouchEvent(event); } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

從上面可以看出:

  • 只有以下三個條件都為真,dispatchTouchEvent()才返回true;否則執行onTouchEvent(event)方法
第一個條件:mOnTouchListener != null; 第二個條件:(mViewFlags & ENABLED_MASK) == ENABLED; 第三個條件:mOnTouchListener.onTouch(this, event);
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3
  • 下面,我們來看看下這三個判斷條件:

第一個條件:mOnTouchListener!= null


//mOnTouchListener是在View類下setOnTouchListener方法里賦值的 public void setOnTouchListener(OnTouchListener l) { //即只要我們給控件注冊了Touch事件,mOnTouchListener就一定被賦值(不為空) mOnTouchListener = l; } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

第二個條件:(mViewFlags & ENABLED_MASK) == ENABLED

  • 該條件是判斷當前點擊的控件是否enable
  • 由於很多View默認是enable的,因此該條件恆定為true

第三個條件:mOnTouchListener.onTouch(this, event)

  • 回調控件注冊Touch事件時的onTouch方法
//手動調用設置 button.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return false; } });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 如果在onTouch方法返回true,就會讓上述三個條件全部成立,從而整個方法直接返回true。
  • 如果在onTouch方法里返回false,就會去執行onTouchEvent(event)方法。

接下來,我們繼續看:onTouchEvent(event)的源碼分析

  1. 詳情請看注釋
  2. Android 5.0后View的onTouchEvent()的源碼發生了變化(更加復雜),但原理相同;
  3. 本文為了讓讀者更好理解onTouchEvent()源碼分析,所以采用Android 5.0前的版本
public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } //如果該控件是可以點擊的就會進入到下兩行的switch判斷中去; if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { //如果當前的事件是抬起手指,則會進入到MotionEvent.ACTION_UP這個case當中。 switch (event.getAction()) { case MotionEvent.ACTION_UP: boolean prepressed = (mPrivateFlags & PREPRESSED) != 0; // 在經過種種判斷之后,會執行到關注點1的performClick()方法。 //請往下看關注點1 if ((mPrivateFlags & PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (!mHasPerformedLongPress) { // This is a tap, so remove the longpress check removeLongPressCallback(); // Only perform take click actions if we were in the pressed state if (!focusTaken) { // Use a Runnable and post this rather than calling // performClick directly. This lets other visual state // of the view update before click actions start. if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { //關注點1 //請往下看performClick()的源碼分析 performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { mPrivateFlags |= PRESSED; refreshDrawableState(); postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } break; case MotionEvent.ACTION_DOWN: if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPrivateFlags |= PREPRESSED; mHasPerformedLongPress = false; postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); break; case MotionEvent.ACTION_CANCEL: mPrivateFlags &= ~PRESSED; refreshDrawableState(); removeTapCallback(); break; case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); // Be lenient about moving outside of buttons int slop = mTouchSlop; if ((x < 0 - slop) || (x >= getWidth() + slop) || (y < 0 - slop) || (y >= getHeight() + slop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); // Need to switch from pressed to not pressed mPrivateFlags &= ~PRESSED; refreshDrawableState(); } } break; } //如果該控件是可以點擊的,就一定會返回true return true; } //如果該控件是可以點擊的,就一定會返回false return false; } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102

關注點1: 
performClick()的源碼分析

public boolean performClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); if (mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnClickListener.onClick(this); return true; } return false; } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 只要mOnClickListener不為null,就會去調用onClick方法;
  • 那么,mOnClickListener又是在哪里賦值的呢?請繼續看:
public void setOnClickListener(OnClickListener l) { if (!isClickable()) { setClickable(true); } mOnClickListener = l; } 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 當我們通過調用setOnClickListener方法來給控件注冊一個點擊事件時,就會給mOnClickListener賦值(不為空),即會回調onClick()。

結論

 

 

  1. onTouch()的執行高於onClick()
  2. 每當控件被點擊時: 

 

  • 如果在回調onTouch()里返回false,就會讓dispatchTouchEvent方法返回false,那么就會執行onTouchEvent();如果回調了setOnClickListener()來給控件注冊點擊事件的話,最后會在performClick()方法里回調onClick()。 

    onTouch()返回false(該事件沒被onTouch()消費掉) = dispatchTouchEvent()返回false(繼續向下傳遞) = 執行onTouchEvent() = 執行OnClick()

  • 如果在回調onTouch()里返回true,就會讓dispatchTouchEvent方法返回true,那么將不會執行onTouchEvent(),即onClick()也不會執行; 
    onTouch()返回true(該事件被onTouch()消費掉) = dispatchTouchEvent()返回true(不會再繼續向下傳遞) = 不會執行onTouchEvent() = 不會執行OnClick()


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

Demo論證

1. Demo1:在回調onTouch()里返回true

Demo流程

//設置OnTouchListener() button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { System.out.println("執行了onTouch(), 動作是:" + event.getAction()); return true; } }); //設置OnClickListener button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println("執行了onClick()"); } });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

點擊Button,測試結果如下: 
測試結果

2. Demo2:在回調onTouch()里返回false

Demo流程

//設置OnTouchListener() button.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { System.out.println("執行了onTouch(), 動作是:" + event.getAction()); return false; } }); //設置OnClickListener button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { System.out.println("執行了onClick()"); } });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

點擊Button,測試結果如下:

測試結果

總結:onTouch()返回true就認為該事件被onTouch()消費掉,因而不會再繼續向下傳遞,即不會執行OnClick()。

如果你看到此處,那么恭喜你,你已經能非常熟悉掌握Android的事件分發機制了(Activity、ViewGroup、View的事件分發機制)


5. 思考點

5.1 onTouch()和onTouchEvent()的區別

  • 這兩個方法都是在View的dispatchTouchEvent中調用,但onTouch優先於onTouchEvent執行。
  • 如果在onTouch方法中返回true將事件消費掉,onTouchEvent()將不會再執行。

  • 特別注意:請看下面代碼

//&&為短路與,即如果前面條件為false,將不再往下執行 //所以,onTouch能夠得到執行需要兩個前提條件: //1. mOnTouchListener的值不能為空 //2. 當前點擊的控件必須是enable的。 mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnTouchListener.onTouch(this, event)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 因此如果你有一個控件是非enable的,那么給它注冊onTouch事件將永遠得不到執行。對於這一類控件,如果我們想要監聽它的touch事件,就必須通過在該控件中重寫onTouchEvent方法來實現。

5.2 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事件的傳遞結論

  • 如果在某個對象(Activity、ViewGroup、View)的dispatchTouchEvent()消費事件(返回true),那么收到ACTION_DOWN的函數也能收到ACTION_MOVE和ACTION_UP 

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


流程講解

  • 如果在某個對象(Activity、ViewGroup、View)的onTouchEvent()消費事件(返回true),那么ACTION_MOVE和ACTION_UP的事件從上往下傳到這個View后就不再往下傳遞了,而直接傳給自己的onTouchEvent()並結束本次事件傳遞過程。 

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


流程講解

6. 總結

  • 通過閱讀本文,相信你已經全面了解Android事件分發機制;
  • 接下來我將繼續介紹與Android事件分發最相關的知識–自定義View,有興趣可以繼續關注Carson_Ho的安卓開發筆記

請點贊!因為你們的贊同/鼓勵是我寫作的最大動力!

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


歡迎關注Carson_Ho的簡書!

不定期分享關於安卓開發的干貨,追求短、平、快,但卻不缺深度


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM